Prompt #204

Back to prompts
Deploy webapp to fresh Ubuntu vserver via n8n flow
DevOps Β· claude-3.7-sonnet
5/5
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}