Deploy
SkillCloud & infraShip a build reversibly — to a host you manage or a platform that manages it. Pick the topology first, keep the previous version reachable, gate on health, roll back without rebuilding. Use when shipping anywhere users can reach.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Deploy skill
What this skill tells your AI
The instructions your AI receives, as published by byerlikaya/claude-starter-kit in plugin/skills/deploy/SKILL.md and read by ahel’s review.
Trigger phrases: "deploy", "push to the server", "to the server", "onto the server", "build on the server", "install on the VPS", "ship it to the server", "go to production", "to production", "go live"
A deploy has a single idea, and it does not depend on where you are deploying: make the new version live in a way you can undo. Keep the previous version reachable until the new one has proved itself; if it fails the health gate, go back to the old one and nobody notices.
Two topologies implement that idea differently, and mixing them is where deploys go wrong. On a host you manage, "reversible" means the previous release still sits on disk and the swap is a pointer you can move back. On a platform that manages the host for you, "reversible" means the previous artefact is still addressable and you re-point the environment at it — you do not rebuild it. Phase 0 picks the branch; everything after follows.
Kit adaptation (local, .claude/): Default backend is .NET (Docker recommended). Deploy requires explicit approval (§4.4); a backup before every swap and a health gate after are mandatory. If
.deploy.ymlcarries server credentials it goes into.gitignore. §4 Prohibitions apply.
Phase 0 — Which topology
Answer before anything else, because the rest of the skill forks here. Ask if it is not obvious; do not infer it from the language or the framework.
| A · Self-managed host | B · Managed platform | |
|---|---|---|
| You have | SSH to a machine you own or rent | an API/CLI, and no shell on the box |
| A release is | a directory on that host | an immutable artefact (image digest, build id, bundle) |
| "Deploy" means | put the files in place and restart | promote an existing artefact to an environment |
| Rollback means | move the pointer back to the previous directory | re-point the environment at the previous artefact |
| Owned by | Phases 1–5 below | Phase B1–B4 below |
Signals for B: the platform builds from a push, or the runtime is described as containers/functions/dynos/pods that you do not administer. Signals for A: you are given a host, a user and a port. A Kubernetes cluster is B when a controller reconciles it from a manifest, and A only if you are genuinely hand-placing files on nodes.
If both are in play — a managed platform in front, a self-managed worker behind — run the branches separately and gate each on its own health check. One approval does not cover two topologies.
Checklist (A · self-managed host)
- Topology decided (Phase 0)
- Runtime + deploy method determined
- Config taken from
.deploy.yml/the user, SSH verified - Reverse proxy + (if there is a domain) SSL in place
- Running version backed up to
releases/ - User approved the deploy, new version deployed
- Health gate passed (otherwise automatic rollback performed)
- If requested, single-command scripts (
deploy.sh/adopt.sh) generated
Checklist (B · managed platform)
- Topology decided (Phase 0)
- Artefact built once and addressed immutably (digest / build id, not a moving tag)
- Which artefact is in which environment is recorded
- Config and secrets injected per environment, not baked into the artefact
- User approved the promotion to this environment
- Health gate passed from OUTSIDE — not the platform's own status
- Previous artefact still addressable, and the migration question settled before the deploy
Phase 1 (A) — Surface: method and runtime
if [ -f Dockerfile ] || [ -f docker-compose.yml ]; then METHOD=docker
elif [ -f package.json ]; then METHOD=bare RUNTIME=node
elif [ -f requirements.txt ] || [ -f pyproject.toml ]; then METHOD=bare RUNTIME=python
elif [ -f go.mod ]; then METHOD=bare RUNTIME=go
elif ls ./*.csproj ./*.sln >/dev/null 2>&1; then METHOD=bare RUNTIME=dotnet
elif [ -f pom.xml ] || [ -f build.gradle ]; then METHOD=bare RUNTIME=java
elif [ -f Cargo.toml ]; then METHOD=bare RUNTIME=rust
elif [ -f Gemfile ]; then METHOD=bare RUNTIME=ruby
elif [ -f composer.json ]; then METHOD=bare RUNTIME=php
else METHOD=unknown; fi
If method: docker and there's no Dockerfile, generate one. method: bare → a process manager suited to the runtime; build the release artefact with the runtime's own command (dotnet publish -c Release, mvn package, cargo build --release, …) and run it under systemd. Prefer Docker for any runtime whose bare setup needs a toolchain on the box. If detection is ambiguous, ask the user — never guess a runtime.
Phase 2 (A) — Preparation: config, SSH, proxy, SSL
Config — read .deploy.yml from the root, and if it's missing ask for each field:
host: 192.168.1.100 # VPS IP/hostname (required)
user: deploy # SSH user (required)
ssh_key: ~/.ssh/id_rsa # private key
app_port: 3000 # application port (required)
health_check: /api/health # HTTP path (default /)
deploy_path: /var/www/app # server path (required)
method: auto # auto | docker | bare
domain: app.example.com # for proxy/SSL (optional)
ssl: true # default true if there is a domain
reverse_proxy: auto # auto | nginx | caddy
Verify SSH first — if you can't connect, stop immediately:
ssh -i $SSH_KEY -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new $USER@$HOST "echo OK"
Reverse proxy — detect the one installed on the server (auto → Caddy if present, otherwise Nginx). The application binds only to 127.0.0.1; it is exposed outward only through the proxy.
SSL — skip if ssl: false or there's no domain, and never on a bare IP. On the Nginx path, verify DNS resolves to the host (dig +short $DOMAIN = $HOST) before running Certbot; on the Caddy path the certificate and its renewal are automatic and there is no separate step.
Config file contents for both proxies, the Certbot commands and the renewal timer: references/proxy-ssl.md — read the half for the proxy you found, not both.
Phase 3 (A) — Swap: keep the old, put the new
First back up the running version (timestamped releases/, last 3 kept):
ssh -i $SSH_KEY $USER@$HOST "
mkdir -p $DEPLOY_PATH/releases
[ -d $DEPLOY_PATH/current ] && cp -a $DEPLOY_PATH/current $DEPLOY_PATH/releases/$(date +%Y%m%d_%H%M%S)
cd $DEPLOY_PATH/releases && ls -dt */ | tail -n +4 | xargs -r rm -rf
"
Docker — build the image locally, transfer it, replace the container:
docker build -t $APP:latest .
docker save $APP:latest | gzip | ssh -i $SSH_KEY $USER@$HOST "gunzip | docker load"
ssh -i $SSH_KEY $USER@$HOST "
docker rm -f $APP 2>/dev/null || true
docker run -d --name $APP --restart unless-stopped -p 127.0.0.1:$APP_PORT:$APP_PORT $APP:latest
"
Bare-metal — rsync the source, install dependencies, start with the process manager:
rsync -avz --delete --exclude .git --exclude node_modules --exclude .venv \
-e "ssh -i $SSH_KEY" ./ $USER@$HOST:$DEPLOY_PATH/current/
| Runtime | Dependencies + startup |
|---|---|
| Node | npm ci --production → PM2: `pm2 start ecosystem.config.js --name $APP |
| Python | python3 -m venv venv && venv/bin/pip install -r requirements.txt → systemd (gunicorn/uvicorn or python main.py) |
| Go | build locally GOOS=linux GOARCH=amd64 go build -o $APP → scp → systemd |
systemd unit (for Python/Go, fill in ExecStart per the runtime):
[Unit]
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$DEPLOY_PATH/current
ExecStart=<runtime command>
Restart=always
Environment=PORT=$APP_PORT
[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now $APP.
Phase 4 (A) — Health gate
Right after the swap; retry for ~30 s until a 200 comes back. If it doesn't pass, trigger Phase 5 automatically.
ssh -i $SSH_KEY $USER@$HOST '
for i in $(seq 1 6); do
[ "$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:'"$APP_PORT$HEALTH_CHECK"')" = 200 ] \
&& { echo "Health gate PASSED"; exit 0; }
sleep 5
done
echo "Health gate FAILED"; exit 1
'
Also verify the process is up: docker ps on Docker, otherwise systemctl is-active $APP / pm2 show $APP.
Phase 5 (A) — Rollback
On health-gate failure: put the most recent releases/ version back into current, restart the service, and
re-run the health gate (Phase 4). Guard-safe rollback script (uses rsync --delete, not rm -rf) + the
approval-gate note: references/rollback.md.
Phase B1 — Build once, and know what you built
The artefact is produced once, and the same one moves through every environment. Rebuilding for production means the thing you tested is not the thing you shipped — the inputs moved underneath you (a floating base image, a lockfile resolved a minute later, a different builder). Two consequences:
- Address it immutably. A digest or a build id, never a moving tag.
latest,mainand a branch name all name different bytes on different days, which makes both promotion and rollback unverifiable. - Record which artefact is in which environment. If nobody can answer "what exactly is running in production", rollback is a guess and the health gate has nothing to compare against.
Phase B2 — Promote, do not rebuild
Deploying to the next environment means pointing that environment at an artefact that already exists and has already passed the previous gate. Configuration is injected per environment, never baked into the artefact — the same bytes must be able to run in staging and in production, or you are back to rebuilding. Secrets come from the platform's own store; a secret inside the artefact ships to everyone who can pull it.
Approval is per environment (§4.4), and per topology. Promoting to a user-facing environment is a deploy, whatever the platform calls it.
Phase B3 — Health gate, from outside
Identical in spirit to Phase 4 (A) and worth stating because platforms invite the opposite: the platform reporting "deployed", "healthy" or "active" is not the verification. It reports that its own reconciliation finished. Check the thing users touch — a request through the public entry point, a real response body, the version endpoint returning the version you promoted. Give it the same failure rule: if the gate does not pass, go back before investigating.
Phase B4 — Roll back by re-pointing
Rollback is promoting the previous artefact, not rebuilding the previous commit — a rebuild is a new artefact with new inputs and it may not even reproduce the bug you are escaping. So:
- Keep the previous artefacts addressable for at least as long as you promise to be able to roll back. A retention policy that deletes them is a rollback policy that does not work.
- Rolling back code does not roll back a migration. If the release included an irreversible schema
change, the old artefact may not run against the new schema — that is
db-migration's expand/contract question and it has to be answered before the deploy, not during the incident. - After a rollback, the environment is running an artefact older than the recorded state. Say so plainly to whoever is watching; a silent rollback is how two people fix the same outage twice.
Single-command scripts (optional)
After a successful deploy, ask whether to generate project-specific deploy.sh / adopt.sh (single-command
deploy/update). Derive the steps from the real project (package.json / Dockerfile / Makefile / compose), never a
generic template. Full guidance + what each script contains: references/scripts.md.
Invariant rules
These hold on BOTH branches; where the mechanism differs, the rule does not. 0. The topology is decided, not assumed — Phase 0, once, out loud. Everything downstream depends on it.
- No deploy without approval — show the method/host/domain/port plan, wait for approval.
- Always back up before the swap — timestamped into
releases/; if the backup fails, abort. - Always a health gate after the swap — HTTP + process; if it doesn't pass, automatic rollback.
- Keep the last 3 versions — don't delete them all.
- Don't deploy to prod without knowing the target — host/deploy_path/domain must be approved.
- Verify SSH first — if you can't connect, fail fast.
- Don't expose the port directly — the app binds to
127.0.0.1, outward only through the proxy. - SSL is mandatory when there's a domain — always set it up unless
sslis explicitlyfalse. - Never rebuild to roll back (B) — promote the previous artefact. A rebuild is a different artefact.
- The platform's own status is not the health gate (B) — verify from where a user stands.
- Rolling back code does not roll back a migration — settle the schema question before the deploy.
Signals
- GitHub stars
- 22
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
deploy-byerlikaya- Source
- github.com/byerlikaya/claude-starter-kit