daybook/deploy/cloud_agent_deploy.md
peter d84753c839 Add cloud deployment runbook for the dedicated deploy agent
Self-contained operational SOP for an autonomous agent deploying and
operating Daybook on a cloud VM: bootstrapping its own scoped SSH deploy
key, cloud-specific network/firewall setup, the git-pull redeploy loop with
rollback, TLS hardening for internet-facing hosts, and a completion
checklist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 22:23:55 +01:00

7.7 KiB

Cloud Deployment Runbook For A Dedicated Deploy Agent

This document is written for an autonomous AI agent responsible for deploying and operating Daybook on a cloud VM, without a human walking through each step. Follow it in order. Where a decision depends on information you don't have (VM IP, domain, whether TLS is required), ask the operator once, then proceed autonomously for routine redeploys.

For background on the app itself and the general (human-facing) version of this workflow, see docs/deployment.md. This file is the operational SOP; that one is the reference.

Source Of Truth

The canonical source is a private Forgejo repository:

https://git.cygnus.systems/peter/daybook.git

Do not deploy from a copied/rsynced tree. Always deploy from a git clone of this repository so git pull is available for fast redeploys.

0. Get Repo Access

You need read access to a private repo. Prefer a repo-scoped SSH deploy key over a personal access token — it can only read this one repo, unlike a token tied to a full user account.

  1. Generate an SSH key pair on the VM (or ask the operator for one to install):
    ssh-keygen -t ed25519 -C "daybook-deploy-agent" -f ~/.ssh/daybook_deploy -N ""
    
  2. Ask the operator to add the public key (~/.ssh/daybook_deploy.pub) as a Deploy Key on the repo in Forgejo (repo → Settings → Deploy Keys → read-only is sufficient), or hand it to them directly — never ask them to give you their personal account credentials.
  3. Configure SSH to use it for this host:
    cat >> ~/.ssh/config <<'EOF'
    Host git.cygnus.systems
      IdentityFile ~/.ssh/daybook_deploy
      IdentitiesOnly yes
    EOF
    
  4. Verify:
    ssh -T git@git.cygnus.systems
    

If SSH access can't be arranged, fall back to an HTTPS access token the operator generates (Forgejo → user Settings → Applications → Generate New Token, read-only repo scope) and clone with:

git clone https://<username>:<token>@git.cygnus.systems/peter/daybook.git /opt/daybook

1. Prerequisites On The VM

Confirm before proceeding; install if missing:

docker --version || curl -fsSL https://get.docker.com | sh
docker compose version
git --version

Confirm outbound network access to pull the Python base image and packages, and confirm you know the VM's public IP or DNS name — you'll need it for DJANGO_ALLOWED_HOSTS and for reporting the final URL back.

2. Clone

sudo mkdir -p /opt/daybook
sudo chown "$USER:$USER" /opt/daybook
git clone git@git.cygnus.systems:peter/daybook.git /opt/daybook
cd /opt/daybook

3. Configure Environment

cp deploy/env.example deploy/.env

Generate a real secret key — do not deploy with the placeholder:

python3 -c "import secrets; print('DJANGO_SECRET_KEY=' + secrets.token_urlsafe(50))"

Edit deploy/.env:

DJANGO_DEBUG=0
DJANGO_SECRET_KEY=<generated-secret>
DJANGO_ALLOWED_HOSTS=<vm-public-ip>,<domain-if-any>,localhost,127.0.0.1
DAYBOOK_DATA_DIR=/data
DATABASE_PATH=/data/db.sqlite3
ARTIFACTS_ROOT=/data/artifacts_store

There is no shared API token to set — Daybook issues a personal token per user from Settings → Account once a user exists (step 5). Never commit deploy/.env; it is already gitignored.

4. Network Access

Cloud VMs default to a locked-down security group/firewall, unlike a home LAN. Before starting the stack, open the port you'll serve on:

  • If exposing Daybook directly: open TCP 8000 to the addresses that should reach it (ideally not 0.0.0.0/0 unless this is meant to be public).
  • If putting a reverse proxy in front (recommended for anything internet-facing — see §8): open 80/443 instead, and keep 8000 closed to everything except localhost.

This is provider-specific (AWS security group, GCP firewall rule, DigitalOcean cloud firewall, or the VM's own ufw/iptables). Ask the operator which provider/console to use if you don't have API access to it yourself.

5. Build, Start, Create A User

mkdir -p deploy/data
docker compose -f deploy/docker-compose.yml up -d --build
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser

The entrypoint runs migrations and collects static files automatically on every start.

6. Verify

docker compose -f deploy/docker-compose.yml ps
curl -fsS http://127.0.0.1:8000/api/capabilities

A 403 with "Login or a valid API token is required" is the expected response when unauthenticated — it confirms the app is up. Log in at http://<vm-ip-or-domain>:8000/ (or through your reverse proxy) with the superuser from step 5, grab a personal token from Settings → Account, then smoke-test a post:

curl -fsS -X POST http://127.0.0.1:8000/api/artifacts \
  -H "Authorization: Bearer <token-from-settings>" \
  -H "Content-Type: application/json" \
  -d '{"source": "deploy-agent", "kind": "markdown", "title": "Deployment Smoke Test", "content": "# OK\n\nDeployed successfully."}'

Confirm the response includes id, url, and api_url, and that opening url in a browser renders the artifact.

7. Redeploy Loop (routine updates)

This is the command you run every time you're asked to ship a new version:

cd /opt/daybook
PREV_SHA=$(git rev-parse HEAD)
git pull
docker compose -f deploy/docker-compose.yml up -d --build

Then repeat the verification in step 6 (capabilities check at minimum). If it fails:

git checkout "$PREV_SHA"
docker compose -f deploy/docker-compose.yml up -d --build

to roll back, then report the failure (build log / migration error / failed health check) back to the operator rather than retrying blindly.

deploy/.env and deploy/data/ are untracked, so git pull never touches them — no need to back them up before a routine redeploy.

8. Hardening For Internet-Facing Deployments

If this VM is reachable from the public internet (not just a private network/VPN), do this before calling the deployment done:

  • Put a reverse proxy (Caddy, Nginx, or the cloud provider's load balancer) in front with TLS. Bearer tokens go over plaintext HTTP otherwise.
  • Add the proxy's public hostname to DJANGO_ALLOWED_HOSTS.
  • Keep DJANGO_DEBUG=0 (already set above).
  • Restrict SSH to key-based auth only; disable password login if you have access to do so.
  • Confirm the VM's firewall only allows the ports actually needed (80/443, and SSH from wherever the operator connects from).

Caddy is the lowest-effort option (automatic TLS via Let's Encrypt):

daybook.example.com {
    reverse_proxy 127.0.0.1:8000
}

9. Backup

tar -czf /tmp/daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data

Copy the resulting archive off the VM (to object storage, the operator's machine, etc.) — a backup that only exists on the VM it's protecting against doesn't help if the VM is lost.

Deployment Checklist

Before reporting the deployment complete, confirm all of these:

  • Cloned via git (not copied), with a working git pull.
  • deploy/.env has a real, non-placeholder DJANGO_SECRET_KEY.
  • DJANGO_ALLOWED_HOSTS includes the actual hostname/IP being used.
  • docker compose -f deploy/docker-compose.yml ps shows daybook healthy.
  • /api/capabilities returns 403 unauthenticated, 200 with a token.
  • At least one superuser exists with a working personal API token.
  • A smoke-test artifact posts and renders successfully.
  • If internet-facing: TLS is in front of the app, not raw HTTP on 8000.
  • A backup of deploy/data/ has been taken and copied off the VM.

Report back to the operator: the app URL, confirmation of the checklist above, and the deploy SSH key fingerprint or token identity used (not the key/token itself) for their records.