Install
$ agentstack add skill-byerlikaya-claude-starter-kit-vps-deploy ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
VPS Deploy
A solid deploy has a single idea: swap the running version with the new one in a reversible way. That is, when you put the new version in place, don't throw the old one away — keep it aside. If the new version can't pass the health gate, undo the swap and no one notices. This skill builds the deploy around this swap; Docker or bare-metal, a single app or different runtimes, it makes no difference.
> 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.yml carries server > credentials it goes into .gitignore. §4 Prohibitions apply.
Checklist
- [ ] 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
Phase 1 — 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
else METHOD=unknown; fi
If method: docker and there's no Dockerfile, generate one. method: bare → a process manager suited to the runtime. If detection is ambiguous, ask the user. .NET: Docker recommended; if bare is required, run the dotnet publish -c Release output with systemd.
Phase 2 — 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.
Nginx site config
server {
listen 80;
server_name DOMAIN;
location / {
proxy_pass http://127.0.0.1:APP_PORT;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Link it into sites-enabled with ln -s, then nginx -t && systemctl reload nginx.
Caddyfile (SSL included, automatic)
DOMAIN {
reverse_proxy 127.0.0.1:APP_PORT
}
systemctl reload caddy. Caddy obtains/renews the certificate on its own — no extra step.
SSL (Nginx path) — skip if ssl: false or there's no domain; don't do SSL on a bare IP. First verify that DNS resolves to the host (dig +short $DOMAIN = $HOST), then Certbot:
certbot --nginx -d $DOMAIN --non-interactive --agree-tos -m admin@$DOMAIN
systemctl enable certbot.timer # automatic renewal
Phase 3 — 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 || pm2 start npm --name $APP -- start → pm2 save | | 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=
Restart=always
Environment=PORT=$APP_PORT
[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now $APP.
Phase 4 — 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 — Rollback
Undo the swap: put the most recent version in releases/ back into current, restart the service, repeat the health gate (Phase 4).
LATEST=$(ssh -i $SSH_KEY $USER@$HOST "ls -dt $DEPLOY_PATH/releases/*/ 2>/dev/null | head -1")
[ -z "$LATEST" ] && { echo "ERROR: no backup — rollback impossible"; exit 1; }
ssh -i $SSH_KEY $USER@$HOST "
mkdir -p $DEPLOY_PATH/current
rsync -a --delete ${LATEST%/}/ $DEPLOY_PATH/current/ # atomic swap — no 'rm -rf' (guard-safe)
{ docker rm -f $APP 2>/dev/null && docker run -d --name $APP --restart unless-stopped \
-p 127.0.0.1:$APP_PORT:$APP_PORT $APP:previous; } \
|| systemctl restart $APP || pm2 restart $APP
"
> Note: Rollback is done with rsync --delete instead of rm -rf; that way guard-bash (the local rm -rf block) > doesn't block the automatic rollback. The deploy verbs (ssh/rsync/docker) pass through the approval gate via > settings.json permissions.ask — they run with approval, not silently.
Single-command scripts (optional)
After a successful deploy, ask: "Shall I generate deploy.sh and adopt.sh so you can deploy/update with a single command in the future?"
If you generate them, don't use a generic template — analyze the actual project (package.json scripts / Dockerfile / Makefile / docker-compose / .env.example) and derive the exact build+start steps. Two scripts:
deploy.sh— first-time setup + deploy: load config · verify SSH · proxy + SSL · back up toreleases/· project-specific build · transfer · dependencies · start · health gate with retries · rollback on failure.adopt.sh— quick update: config · backup · code sync · (if needed) build/dependencies · restart · health gate · rollback on failure.
Rules: chmod +x; if .deploy.yml carries credentials, ask about .gitignore; don't overwrite an existing script without approval (show a diff); no hardcoded credentials — always read from .deploy.yml; comment and explain every project-specific decision.
Invariant rules
- 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.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: byerlikaya
- Source: byerlikaya/claude-starter-kit
- License: MIT
- Homepage: https://www.npmjs.com/package/@byerlikaya/claude-starter-kit
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.