Prompt #204
Back to promptsDeploy webapp to fresh Ubuntu vserver via n8n flow
- Variables
- {'name': 'vserver_host', 'description': 'target IP or DNS'}, {'name': 'ssh_user', 'description': 'NOPASSWD sudo user'}, {'name': 'app_name', 'description': 'slug for container/dirs'}, {'name': 'app_image', 'description': 'docker image:tag'}, {'name': 'app_port', 'description': 'container port'}, {'name': 'app_domain', 'description': 'public domain (DNS already pointing)'}, {'name': 'admin_email', 'description': 'Caddy ACME contact'}
- Tags
- stack-aware,deployment,n8n-flow,ssh,docker,caddy,ubuntu,customer-vserver,skill,executable
- Source
- research-2026-05-01-stack-aware-handcrafted
- Use count
- 0
- Created
- 2026-05-01T18:43:51.499795+00:00
- Updated
- 2026-05-01T18:43:51.499795+00:00
Content
Deploy any docker-image webapp to a fresh Ubuntu vserver β generic recipe used by the n8n flow `webapp-deploy-vserver/webapp-deploy-to-fresh-ubuntu-vserver.json`.
Inputs: vserver_host, ssh_user, app_name, app_image, app_port, app_domain, admin_email, env_vars_json (optional), volume_mounts_json (optional), registry_login (optional).
Pipeline (each step idempotent):
1. **SSH connectivity test** β `ssh -o BatchMode=yes user@host echo ok`. Fail-fast if no SSH.
2. **Bootstrap** (sudo bash on remote):
```
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates gnupg lsb-release ufw rsync jq
command -v docker || curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
command -v caddy || (
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update -qq && apt-get install -y -qq caddy
)
systemctl enable --now caddy
ufw --force default deny incoming; ufw --force default allow outgoing
ufw allow 22/tcp; ufw allow 80/tcp; ufw allow 443/tcp
ufw --force enable
```
3. **Persistent dirs**: `install -d -m 0755 /persistent/<app_name>/data /opt/<app_name>`
4. **docker-compose.yml** at `/opt/<app_name>/docker-compose.yml`:
```yaml
version: '3.9'
services:
app:
image: <app_image>
container_name: <app_name>
restart: unless-stopped
ports:
- "127.0.0.1:<app_port>:<app_port>"
volumes:
- /persistent/<app_name>/data:/data
# plus any extras from volume_mounts_json
environment:
- APP_PORT=<app_port>
- APP_DOMAIN=<app_domain>
# plus any extras from env_vars_json
```
5. **Caddyfile** at `/etc/caddy/Caddyfile`:
```
{ email <admin_email> }
<app_domain> {
encode gzip zstd
reverse_proxy 127.0.0.1:<app_port> {
header_up X-Real-IP {remote}
header_up X-Forwarded-Proto {scheme}
header_up Host {host}
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
}
log { output file /var/log/caddy/<app_name>.log { roll_size 50mb roll_keep 7 } format json }
}
```
Then `caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy`.
6. **Pull + start**: optional `docker login <registry>`, then `cd /opt/<app_name> && docker compose pull && docker compose up -d --remove-orphans`.
7. **Smoke test**: `curl -sI https://<app_domain>/` β expect 200 (or 302/301 if app redirects). Caddy auto-provisions Let's Encrypt cert on first request (~20-30s).
8. **Notify**: `logger -t webapp-deploy "DEPLOY-OK app=<app_name> domain=<app_domain> host=<vserver_host>"`. On failure print `ssh user@host docker logs <app_name> --tail 50` for triage.
Auto-restart on reboot: handled by `restart: unless-stopped` + `systemctl enable docker` (already done in bootstrap).
NO Authelia β public-facing deploy. For internal/admin app_domains, add `import authelia` (requires Authelia running elsewhere; this flow does NOT install Authelia).
Re-runs are safe: bootstrap skips installed binaries, compose+Caddyfile get overwritten (back up if hand-edited), `docker compose pull` upgrades to latest tag.
Variables: {vserver_host} {ssh_user} {app_name} {app_image} {app_port} {app_domain} {admin_email}