Deploy skill
Deploy vite apps to your servers!
npx -y skills add archetype2142/deploy-skillAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 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.
What its author says it does
Copied from the file, not written here
Deploy a Vite + React app (or any static/SPA frontend) to the user's own Docker + Traefik server over SSH, on a Cloudflare-managed subdomain with automatic HTTPS. Use this skill whenever the user wants to deploy, ship, publish, push live, or host a Vite/React/frontend project on their server — e.g. "deploy this app", "put this on my server", "ship the dashboard to a subdomain", "host this on a subdomain", "make this live". Also use it to redeploy/update an already-deployed app, list what's deployed, take an app down, or free up disk space on the server. Trigger even when the user doesn't say "Docker", "Traefik", or "Cloudflare" explicitly — if they have a frontend project and want it on the internet on their box, this is the skill.
SKILL.md
12.9 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
Deploy a Vite/React app to a Docker + Traefik server
This skill takes a local frontend project and gets it live at
https://<subdomain>.<root-domain> on the user's own VPS, where <root-domain>
is whatever they set during setup. The server runs Docker with a Traefik reverse
proxy that handles routing and Let's Encrypt TLS. DNS is managed in Cloudflare.
The whole thing is one SSH-driven flow: build the app into a container, ship it,
run it behind Traefik, and point a subdomain at it.
The reason this is a skill rather than ad-hoc steps is that getting a deploy right means matching the server's existing conventions — the Traefik network name, the entrypoint names, the certresolver, where apps live on disk. Guessing those wrong produces a container that runs but never receives traffic, or a cert that never issues. So the core discipline here is: discover the server's setup first, then conform to it, rather than imposing a config that looks right in isolation.
Configuration
All site-specific settings — SSH host/user/key, root domain, server IP, the
Cloudflare token, the reserved-subdomain list, and any pinned Traefik values —
live in ~/.config/deploy-vite-app/config.env, written by install.sh (see
README). Nothing is hardcoded to a particular domain or server: the repo ships
with no real values, which is what makes it safe to publish and portable across
machines and across Claude Code / Cursor. scripts/config.sh loads that file
(environment variables override it) and every script reads from it. If essential
values are missing, the scripts stop with a clear "run install.sh" message rather
than guessing.
To run a one-off remote command, source the config and use the helper:
source scripts/config.sh
remote "docker ps" # runs over SSH with the configured key/user/host
If the user's request mentions different values, prefer those for the session.
Reserved subdomains
The user can mark certain subdomains as off-limits (set during install.sh,
stored as RESERVED_SUBDOMAINS) — typically ones already pointing at live
services, since deploying over them would create a conflicting DNS record or
Traefik router and break something.
Always validate the requested subdomain with scripts/check_subdomain.py <subdomain> before doing anything else. It enforces the configured blocklist
plus DNS label rules (lowercase, alphanumeric + hyphens, no leading/trailing
hyphen) and prints the full hostname when valid. If the user asks for a reserved
one, stop and ask them to pick another. If they don't specify a subdomain at all,
suggest one derived from the project's directory or package.json name and
confirm it before proceeding.
The deploy flow
Work through these in order. The early steps are cheap and catch the failures that are expensive to debug later (a broken build, a taken subdomain, a mismatched Traefik network).
1. Locate and sanity-check the project
Confirm you're pointed at a real Vite project: there should be a package.json
with a build script and (usually) a vite dependency. If you're unsure which
directory the user means, ask. Read package.json to learn the project name and
whether it's a plain static build (the common case) or something that needs a
running Node process (SSR, an API route, serve-style server). Most of the time
it's static — npm run build emits a dist/ of HTML/JS/CSS.
2. Choose and validate the subdomain
Run python3 scripts/check_subdomain.py <subdomain>. It exits non-zero and
explains why if the name is reserved or malformed, and prints the full hostname
(<subdomain>.<root-domain>) when valid. Confirm that hostname with the user.
3. Discover the server's Traefik setup — do not assume
If the server hasn't been provisioned yet, get it ready first — and prefer the no-root path:
- Docker is installed and the deploy user can run it, but there's no Traefik:
run
scripts/bootstrap_traefik.sh. No root needed. It creates the shared network, starts Traefik v3.7 (pinned — older versions break against Docker 29+, seereferences/traefik.md) under the deploy user's$HOME/traefik, and writesTRAEFIK_NETWORK/entrypoint/certresolver into config.deploy.shalso offers to run this automatically when it finds no Traefik. - Fresh box (no Docker, or the user lacks sudo): run
scripts/server_setup.sh(needs root once). It enables passwordless sudo, installs Docker, fixes the containerd content store, then callsbootstrap_traefik.sh.
A quick way to tell what's there: remote "docker ps" either errors (Docker/sudo
not ready → server_setup.sh) or runs but shows no traefik container
(→ bootstrap_traefik.sh).
Otherwise, SSH in and look at how Traefik is actually configured, because the
labels you generate must match it exactly. Read references/traefik.md for what
to look for and how to interpret it. In short, you need three values:
- the Docker network Traefik shares with app containers (often
traefik,proxy, ordokploy-network), - the websecure entrypoint name (usually
websecure, sometimeshttps), - the certresolver name for Let's Encrypt (e.g.
letsencrypt,myresolver).
The fastest way to get all three right is to copy them from an app that's
already deployed and working — inspect a running container's labels with
docker inspect. references/traefik.md gives the exact commands.
4. Build locally (smallest footprint on the VPS)
Build the app on the local machine, not the server. The VPS is expected to host
dozens of apps on a tiny box, so the deploy is designed to never run a Node
toolchain there — only the finished dist/ is shipped. Building locally also
fails fast on a broken project:
npm ci && npm run build # or npm install if there's no package-lock.json
Confirm a dist/ (or the project's configured Vite outDir) appears. If the
output dir differs, pass it to the deploy script with --dist <dir>.
5. Generate the deploy artifacts
deploy.sh renders two files from assets/ into a staging bundle alongside the
built dist/. references/templates.md explains them and the footprint
reasoning in detail.
Dockerfile—FROM static-web-server:2(~5 MB) andCOPY dist/. Serves on port 80 with--page-fallback /public/index.html, i.e. SPA fallback so client-side routes like/aboutreturn 200 on refresh instead of 404. This is the single most common thing people forget with React Router.docker-compose.yml— one service on the Traefik network with labels forHost(<sub>.<root-domain>), the websecure entrypoint, and the certresolver discovered in step 3.
Why this image: the runtime is a single static binary on a scratch base. Every
app pins the same base tag, so Docker stores that ~5 MB layer once and each
extra app costs only its own files — both disk and RAM stay tiny across dozens of
apps. Don't vary the base tag per app or you lose that sharing.
The compose project/container is named app-<sub> so redeploys replace in place
instead of piling up duplicates.
If a project genuinely needs a running Node server (SSR, an API route) rather
than static files, see variants A–C in references/templates.md for the
alternate Dockerfile and the one label that changes (the service port). Keep the
static default for the common case — the Node runtime image is much larger.
6. Create the Cloudflare DNS record
Run scripts/cloudflare_dns.sh <subdomain>. It looks up the root domain's zone
ID, then creates or updates an A record for <subdomain> pointing at the
configured server IP. It needs a CLOUDFLARE_API_TOKEN with Zone:DNS:Edit on
that zone (stored in config by install.sh). If the token is missing, the
script prints exactly how to create one — relay that to the user and pause until
they provide it.
By default the record is created DNS-only (not proxied / grey cloud). This
is deliberate: it lets Traefik complete the Let's Encrypt HTTP-01 challenge and
own the TLS cert directly, which is the simplest setup that reliably works. If
the user specifically wants Cloudflare's proxy (orange cloud) in front, pass
--proxied — but mention that TLS then terminates at Cloudflare and the
Traefik/Let's Encrypt setup may need its SSL mode set to "Full". Default to
DNS-only unless they ask otherwise.
TLS is normally automatic. The compose labels already request a Let's Encrypt
cert via Traefik's certresolver, and Traefik issues + renews it on the first
HTTPS request — you don't run certbot in the common case. If the user wants one
wildcard *.<root-domain> cert for every subdomain (cleaner at scale, and
proxy-compatible), or Traefik isn't configured for ACME, read references/tls.md:
it covers the wildcard-via-Traefik-DNS-01 setup and a certbot fallback
(scripts/certbot_wildcard.sh) with automatic renewal.
7. Ship and run
Ship only the staging bundle (the built dist/, the rendered Dockerfile, and
docker-compose.yml) to /opt/apps/<sub> on the server, then build and start.
The transfer is tiny and the server-side docker build just copies static files
into the ~5 MB base — no Node, negligible CPU/RAM. scripts/deploy.sh does steps
4–8 end to end:
scripts/deploy.sh --app <project-dir> --sub <subdomain> [--proxied] [--dist <dir>]
Read the script before running so you can adjust the app path if step 3 revealed a different server layout.
8. Verify it's actually live
deploy.sh does this automatically (it polls the URL and prints a result
banner), but when checking by hand a container that's "up" isn't the same as a
working site:
apps.sh list(orremote "docker compose -f <appdir>/docker-compose.yml ps") shows the container running.curl -sS -o /dev/null -w '%{http_code}' https://<sub>.<root-domain>returns 200. The very first request after deploy may take a few seconds while Traefik obtains the certificate — if you get a TLS error immediately, wait ~30s and retry before treating it as a failure.- Spot-check a client-side route (e.g.
.../some/path) returns 200 too, confirming the SPA fallback works.
Report the live URL to the user once it's verified. deploy.sh's terminal UI
shows each step (✓/·/!) and ends with a boxed banner containing the live URL,
the image size, and the dist size — relay that outcome to the user.
Managing deployed apps (scripts/apps.sh)
When the user wants to see what's deployed, take something down, or free space on
a full VPS, use apps.sh rather than ad-hoc docker commands:
apps.sh list— table of every app: subdomain, container status, image size, disk usage, and URL.apps.sh space— filesystem +docker system df+ the largest app directories, so you can decide what to remove when disk is tight.apps.sh down <sub>— stop and remove the container but keep its files and image (a redeploy brings it straight back). Good for temporarily freeing RAM.apps.sh remove <sub> [--dns]— fully remove the container, its image, and the app directory to reclaim disk.--dnsalso deletes the Cloudflare record. Confirm with the user before removing, and especially before--dns.apps.sh prune— drop dangling images and build cache; a safe first move when space is low before removing any actual app.
Redeploy / update: just re-run deploy.sh for the same subdomain. The stable
app-<sub> project name means it rebuilds and replaces in place; the DNS record
already exists and is left as-is.
When something doesn't work
- 502 / 404 from Traefik: the container isn't on Traefik's network, or the
service port label doesn't match the port the app listens on (80 for the SWS
image). Re-check step 3's network name and the
loadbalancer.server.portlabel. - Cert never issues / TLS errors persist: the DNS record may be proxied when it should be DNS-only, or the certresolver name doesn't match Traefik's config. Verify against a working app's labels.
- Build fails on the server but passed locally: usually a case-sensitivity
issue (Linux filesystem vs macOS) in an import path, or a dependency missing
from
package.json. Read the build log fromdocker compose ... up --build.
references/traefik.md and references/templates.md go deeper on each of these.
What ships with it: 20 files
65.4 KB alongside SKILL.md, 12 of them executable
assets/
- docker-compose.yml.tmpl1.0 KB
- Dockerfile1.3 KB
- traefik-compose.yml.tmpl1.4 KB
references/
- templates.md3.4 KB
- tls.md3.7 KB
- traefik.md4.7 KB
scripts/
- apps.shruns3.5 KB
- bootstrap_traefik.shruns4.8 KB
- certbot_wildcard.shruns2.2 KB
- check_subdomain.pyruns2.7 KB
- cloudflare_dns_delete.shruns1.3 KB
- cloudflare_dns.shruns2.5 KB
- config.shruns3.1 KB
- deploy.shruns5.3 KB
- discover_traefik.shruns3.0 KB
- server_setup.shruns3.7 KB
- ui.shruns1.7 KB
- .gitignore178 B
- install.shruns8.7 KB
- README.md7.1 KB
Gives 0 of the 12 instructions most containers cloud skills give in ~3.1k 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 server setup before deploying
- validate subdomain before doing anything else
- build app locally instead of on the server
- use DNS-only for Cloudflare records by default
- confirm removal and DNS deletion with the user
- confirm the live URL with the user
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.