Ec2 docker host
Deploy, host, and operate apps on a single AWS EC2 instance (or similar VPS) running Docker. Use this whenever the user points at a specific server — by IP, SSH target, or instance ID — and wants to: deploy a Next.js / Node / NestJS / backend app or service onto it, add a new app to a box already running others without breaking them, see what's currently running, wire up a domain and TLS behind Caddy or a reverse proxy, give each app its own database / host port / secrets, or set up Postgres/Mongo/Redis backups (pg_dump, offsite to S3, restore drills) on that host. Also for standing up a fresh EC2 Docker host from scratch. Covers casual phrasings like "deploy to my server", "put this on my EC2", "set up my VPS", "add this app to my box". NOT for managed AWS (ECS, Fargate, EKS, Kubernetes), local-only dev, writing a Dockerfile by itself, or cloud cost/billing questions.From its SKILL.md
npx -y skills add juncoding/agent-skills --skill ec2-docker-hostAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
14.7 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it
EC2 as a single-host Docker platform
This skill turns one EC2 instance into a small multi-tenant platform: a handful of shared backing services (Postgres, Mongo, Redis), one reverse proxy that terminates TLS, and N independent app containers living alongside each other. You can stand up a fresh host, adopt one that already has apps running, deploy a new project in a few commands, and keep the data backed up.
The prime directive: never disturb a neighbor. Apps already running on the host pay the rent. A
new deploy, a proxy reload, a compose up — none of it may interrupt, restart, or reconfigure another
app. Every instruction here exists to protect that guarantee. When in doubt, choose the additive,
reversible, neighbor-safe option, and discover the live state before you change it.
How the pieces fit
Internet (:80/:443)
│
┌────────────▼────────────┐
│ Reverse proxy (Caddy) │ host systemd service, auto-TLS
│ one vhost block per app │ per-FQDN Let's Encrypt
└─────┬─────────┬──────────┘
127.0.0.1:3001 │ │ 127.0.0.1:3002 ... (unique host port per app)
┌───────────▼──┐ ┌──▼───────────┐
│ app-a (cntr) │ │ app-b (cntr) │ each its own Compose project
└──────┬───────┘ └──────┬───────┘
└──────┬───────────┘
┌─────────────▼──────────────┐ shared Docker bridge network "infra_net"
│ postgres mongo redis │ bind only to 127.0.0.1, never public
└─────────────┬──────────────┘
│
/data (separate EBS volume, bind-mounted)
postgres/ mongo/ redis/ caddy/ backups/ secrets/
Two facts make the whole thing safe and cheap to operate:
- Data lives on a separate volume (
/data, a dedicated EBS volume) via bind mounts, not on the root disk and not in named volumes. Containers, images, even the whole instance can be rebuilt and the data survives, because the volume is independent. Treat/dataas sacred. - The shared services bind only to
127.0.0.1. Database ports are never exposed to the internet; apps reach them over the privateinfra_netDocker network by service name (postgres,mongo,redis). The only things the security group opens to the world are 80/443 (and SSH from your IP).
The four modes — route first
Figure out which situation you're in, then read the matching reference. Most sessions are Deploy.
| Mode | When | Read |
|---|---|---|
| Deploy a project | Host exists; you want to put an app on it (the common case) | references/deploy-project.md |
| Adopt an existing host | First time touching a host (yours or one with apps already running) | references/adopt-host.md |
| Provision a new host | No host yet; stand one up from nothing | references/provision-host.md |
| Maintain | Backups, restore, upgrades, health checks, disk pressure | references/backups-and-maintenance.md |
Cross-cutting references you'll reach for from several modes:
references/app-contract.md— what an app must provide to be hostable (health endpoint, deploy compose, entrypoint, env model). Read before deploying any app that hasn't been deployed here before.references/reverse-proxy-tls.md— how vhosts and TLS work; the validate-then-reload safety dance.references/secrets-and-images.md— the pluggable backends. Default is lightweight (any registry + host-side env files); AWS-native (ECR + SSM + GitHub OIDC) is an opt-in upgrade.
Always discover before you act
The live host is the source of truth — not a local file, not your memory, not this skill. Before any change to an existing host, take an inventory so your decisions (which port is free, does this DB already exist, what vhosts are live) are grounded in reality:
scripts/discover-host.sh <ssh-target> # read-only; prints containers, used ports, networks,
# volumes, proxy type, shared DBs, /data usage
Feed that inventory into every decision below. If discovery shows something you didn't expect (an app you don't recognize, a port you were about to use, a proxy that isn't Caddy), stop and tell the user rather than plowing ahead — unexpected state usually means in-progress work or a different setup, and overwriting it is how neighbors get hurt.
The isolation invariants (the part that protects neighbors)
Every shared resource is namespaced by app + environment. Honor all of these on every deploy; they are the mechanism behind the prime directive. The reasoning is given so you can handle edge cases sensibly.
- Unique host port per app+env. Containers all listen on the same internal port (3000); only the
host-side mapping
127.0.0.1:<host-port>:3000differs. Allocate from a deterministic scheme (3000+offsetfor prod,4000+offsetfor staging) and verify the port is actually free in the live inventory before claiming it —scripts/allocate-port.shdoes both. Two apps on one port is the most common way to take down a neighbor. - Its own Compose project + container names. Each app+env is a separate Compose project in its own
host directory (
~/apps/<app>-<env>/) with explicitname:andcontainer_name. This is what makesdocker compose up -d --remove-orphanssafe:--remove-orphansonly prunes orphans within that one project, so it can never touch another app or the shared DB stack. - Its own database + dedicated role. A per-app Postgres database and a role that can touch only that
database. Prod uses the bare name (
myapp), staging a suffix (myapp_staging). Never reuse a role across apps. - Its own secret namespace. Secrets live under a per-app path/prefix; the app's entrypoint pulls only its own namespace. One app can never read another's secrets.
- Its own Redis key prefix (
<app>:<env>:) when it uses the shared Redis. This is convention-only (shared instance, no per-app ACL), so it must be enforced in the app's config, not assumed. - Additive-only proxy vhosts. Adding an app contributes a new server block; it never edits another app's block. A wildcard DNS record pointing at the host's stable IP means a new app needs no DNS change at all (custom apex domains are the exception — see the proxy reference).
- Validate, then reload — never restart — the proxy. Validate the new config (locally and on the
host into a
.newfile) and only swap + hot-reload if it passes. A malformed vhost must never be able to interrupt the vhosts already serving traffic.
If a requested change can't be made additively (e.g., two apps genuinely need the same port, or an app wants to claim a domain another app serves), that's a real conflict — surface it to the user with the options, don't silently resolve it.
Deploying a project — the workflow
This is the spine of the skill. Full detail (including monorepo vs polyrepo, first-deploy vs redeploy,
the image build/push step) is in references/deploy-project.md; the shape is:
1. Identify the project's shape. A repo is one of:
- Polyrepo / single app — e.g. a standalone Next.js or backend repo → one deployable unit.
- Monorepo — one repo, several deployables (frontend app(s) + backend service(s)). Each deployable is treated as its own app under these invariants: its own port, container, possibly its own DB/secrets. Don't deploy a monorepo as one blob unless it genuinely ships as one container.
Decide how each unit is served (combined frontend+backend in one container, backend-only with the
frontend hosted elsewhere, or host-served static frontend) — see the shapes table in
references/deploy-project.md.
2. Discover the host (above) and confirm the app meets the contract (references/app-contract.md):
a GET /health endpoint, a deploy compose file of the standard shape, an entrypoint that pulls secrets
- runs migrations, config from runtime env. If it doesn't, generate the missing pieces from
assets/templates before going further.
3. Produce a deploy plan and show it to the user before applying. The plan states, per deployable: the allocated host port, the container/project name, the database + role to create (or reuse), the secret namespace + which keys are needed, the proxy vhost(s) + FQDN(s), the image reference, and whether this is a first deploy (creates DB/secrets/vhost) or a redeploy (image swap only). Deploying to a live host is a production action — get a nod on the plan first, especially for anything non-additive.
4. Apply, in the neighbor-safe order:
- First deploy only:
scripts/db-create.sh(DB + role + generated password), then set the app's secrets (see secrets reference). - Build + push the image to the registry (lightweight default: any OCI registry, immutable
<branch>-<sha>tag — refuselatestso rollback is deterministic). See deploy reference. scripts/deploy-app.sh <app> <env> <image-ref> <host-port> <ssh-target>— verifies/pulls the image, copies the compose file, writes the deploy-time.env,compose up -d, waits for/health.- First deploy only: add the vhost and reload the proxy —
scripts/add-vhost.sh(additive, validate-then-reload).
5. Verify and report. Health endpoint green over HTTPS, TLS cert issued, container healthy, and the neighbors are still up (re-run discovery; nothing else changed). Report the URL plus any manual follow-ups (first-admin grant, DNS for a custom apex, email-domain verification).
Provisioning a new host (brief)
When there's no host yet, references/provision-host.md walks the AWS-CLI path: create the instance
(pinned AMI matching your build architecture), an Elastic IP (stable target for DNS, survives stop/start),
a security group (22 from your IP, 80/443 world, DB ports never open), and a dedicated data EBS volume
with deletion protection; then bootstrap the host (install Docker, create infra_net, mount /data
formatting only if blank, bring up the shared DB + proxy stack, install backups). The provisioning
scripts are idempotent — safe to re-run.
Maintenance & backups (brief)
references/backups-and-maintenance.md covers the three backup layers (logical DB dumps with
pg_dumpall / mongodump, volume snapshots, off-host object-store sync with lifecycle), how to verify
and actually restore (do a restore drill — a backup you've never restored is a hope, not a backup), and
why host upgrades and image upgrades are kept as separate deliberate actions rather than silent rolls.
Bundled scripts
All scripts are idempotent and take an SSH target + app/host config; they encode the safety logic above so you don't reinvent it each time. Read a script before running it the first time on a host so you know exactly what it will do.
| Script | Does | Mutates host? |
|---|---|---|
scripts/discover-host.sh | Inventory: containers, ports, networks, volumes, proxy, DBs, /data | no (read-only) |
scripts/allocate-port.sh | Pick + verify a free host port against the live inventory | no |
scripts/db-create.sh | Create per-app DB + role + random password | yes (additive) |
scripts/deploy-app.sh | Copy compose, write .env, pull image, compose up -d, health-check | yes (one project) |
scripts/add-vhost.sh | Append a vhost, validate, hot-reload the proxy | yes (additive) |
scripts/backup.sh | DB dumps + off-host sync | yes (writes to /data/backups) |
scripts/lib.sh | Shared helpers (SSH wrapper, logging) — sourced by the others | n/a |
Exact signatures (this table is the source of truth — the scripts validate their own args, so match these and don't paraphrase). An SSH target is <user>@<host> (e.g. ubuntu@<ip>); set SSH_KEY=<path> in the env if a key is needed:
scripts/discover-host.sh <ssh-target>
scripts/allocate-port.sh <ssh-target> <prod|staging> [desired-offset] # prints OFFSET= and HOST_PORT=
scripts/db-create.sh <app> <env> <ssh-target>
scripts/deploy-app.sh <app> <env> <image-ref> <host-port> <ssh-target> [compose-file] # refuses :latest
scripts/add-vhost.sh <fqdn> <host-port> <ssh-target> [reverse_proxy|static:<dir>]
scripts/backup.sh <ssh-target> # env: BACKUP_S3=s3://bucket/prefix
Assets (fill-in templates)
assets/docker-compose.app.yml— the standard per-app deploy compose (variable substitution).assets/Caddyfile.vhost— one additive vhost block (reverse-proxy / combined / static variants).assets/entrypoint.sh— pulls secrets, aliases the DB URL, runs migrations, execs the app.assets/host.example.yml— connection + backend config for a host (ssh target, data dir, network name, proxy type, registry, secret backend). Connection details only — live state is discovered.
A note on portability
The isolation core (shared network, per-app Compose project + port offsets, additive validate-then-reload
proxy, entrypoint secret-pull, immutable SHA tags, branch-based promotion) is provider-agnostic. Only
four pieces are genuinely AWS-bound and are kept pluggable: the image registry, the secret store,
DNS, and the off-host backup/snapshot target. references/secrets-and-images.md documents the
lightweight default and the AWS-native upgrade for each.
What ships with it: 19 files
61.9 KB alongside SKILL.md, 8 of them executable
assets/
- Caddyfile.vhost1.1 KB
- docker-compose.app.yml1.7 KB
- entrypoint.shruns1.9 KB
- host.example.yml1.4 KB
evals/
- evals.json6.5 KB
references/
- adopt-host.md5.0 KB
- app-contract.md4.5 KB
- backups-and-maintenance.md3.9 KB
- deploy-project.md7.2 KB
- provision-host.md4.7 KB
- reverse-proxy-tls.md4.0 KB
- secrets-and-images.md4.5 KB
scripts/
- add-vhost.shruns2.2 KB
- allocate-port.shruns1.4 KB
- backup.shruns2.3 KB
- db-create.shruns2.8 KB
- deploy-app.shruns2.6 KB
- discover-host.shruns2.5 KB
- lib.shruns2.0 KB
Gives 0 of the 12 instructions most containers cloud skills give in ~3.4k tokens
Counted across 607 of the 657 authors here whose files we hold, read 2026-08-07
- Run containers as a non-root userin 66 of 607, across 46 files
- Use multi-stage buildsin 53 of 607, across 44 files
- Use Promise.all for independent operationsin 47 of 607, across 13 files
- Import directly instead of barrel filesin 46 of 607, across 12 files
- Use ternary instead of AND for conditionalsin 45 of 607, across 12 files
- Use Set or Map for O(1) lookupsin 42 of 607, across 10 files
- Create a .dockerignore filein 41 of 607, across 31 files
- Read individual rule files for detailsin 39 of 607, across 9 files
- Copy dependency files before source codein 36 of 607, across 23 files
- Authenticate server actions like API routesin 35 of 607, across 7 files
- Use next/dynamic for heavy componentsin 34 of 607, across 9 files
- Use React.cache for per-request deduplicationin 34 of 607, across 10 files
Said here and by no other author read
- discover live host state before acting
- stop and tell the user on unexpected state
- verify a host port is actually free before claiming it
- use unique project and container names per app
- create a dedicated database and role per app
- isolate secrets under a per-app namespace
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.