diff --git a/deploy/cloud_agent_deploy.md b/deploy/cloud_agent_deploy.md index 4a12d79..8364a8e 100644 --- a/deploy/cloud_agent_deploy.md +++ b/deploy/cloud_agent_deploy.md @@ -35,10 +35,15 @@ a token tied to a full user account. 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: +3. Configure SSH to use it for this host. **Note:** this Forgejo's git-SSH + listens on port `2222`, not the default `22` (chosen so it doesn't clash + with the box's own system `sshd`) — connecting on `22` hits system SSH + instead and will fail or hang, not just warn. Don't assume `22` even if + this changes again later; confirm with the operator if unsure. ```bash cat >> ~/.ssh/config <<'EOF' Host git.cygnus.systems + Port 2222 IdentityFile ~/.ssh/daybook_deploy IdentitiesOnly yes EOF @@ -47,6 +52,8 @@ a token tied to a full user account. ```bash ssh -T git@git.cygnus.systems ``` + If you'd rather not rely on `~/.ssh/config`, clone with the port explicit + instead: `ssh://git@git.cygnus.systems:2222/peter/daybook.git`. If SSH access can't be arranged, fall back to an HTTPS access token the operator generates (Forgejo → user Settings → Applications → Generate New @@ -106,25 +113,48 @@ 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. +`deploy/.env` will be read and written by whatever agent runs this runbook. +Treat it as low-stakes: it should never hold anything more sensitive than +this app's own `DJANGO_SECRET_KEY`. If the target host has its own +documented convention for secrets (e.g. composed by reference at execution +time rather than written to a file), follow that instead — ask the operator +if unsure which applies here. + ## 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 this VM is reachable from the public internet, a reverse proxy with TLS +in front of Daybook is not optional — it's the only path.** Bearer tokens are +this app's entire write-auth model, and they'd otherwise go over plaintext +HTTP straight to a port on the public IP. Direct port exposure (below) is +only for private/VPN-only networks with no public exposure. -- 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`. +- **Internet-facing (the common case):** set `DAYBOOK_BIND_ADDR=127.0.0.1` in + `deploy/.env` so the container port is not reachable from outside the VM at + all, then open `80`/`443` in the VM's firewall/security group for the + reverse proxy (see §8). Do not open `8000` externally. +- **Private/VPN-only network:** open TCP `8000` to the addresses that should + reach it (never `0.0.0.0/0`), and leave `DAYBOOK_BIND_ADDR` unset. -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. +Before opening any port, check whether one is already open and in use — +`ss -tlnp | grep -E ':80|:443|:8000'` — rather than assuming a clean slate. +Firewall/security-group configuration 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 +If the target has a separate, larger data volume than its main disk (common +on space-constrained VMs), point Daybook's persistent data at it before +starting: set `DAYBOOK_HOST_DATA_DIR=/path/on/that/volume` in `deploy/.env`. +Ask the operator whether such a volume exists and where before defaulting to +the main disk — filling the main disk on a host running other services is a +bigger problem than it would be on a dedicated box. Create that same path +(the one you just set in `deploy/.env`, or `deploy/data` if you left it +unset) before starting the stack: + ```bash -mkdir -p deploy/data +mkdir -p deploy/data # or the DAYBOOK_HOST_DATA_DIR path if you set one above docker compose -f deploy/docker-compose.yml up -d --build docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser ``` @@ -140,9 +170,13 @@ 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://:8000/` (or through your reverse proxy) with the -superuser from step 5, grab a personal token from Settings → Account, then +response when unauthenticated — it confirms the app is up. This `curl` runs +against `127.0.0.1` from on the VM itself, so it works regardless of whether +`DAYBOOK_BIND_ADDR` is set. Log in from a browser at whichever address is +actually externally reachable at this point in the runbook — `http://:8000/` +if you're on the private/VPN-only path from §4, or `https:///` through +the reverse proxy once §8 is done if you're on the internet-facing path — with +the superuser from step 5, grab a personal token from Settings → Account, then smoke-test a post: ```bash @@ -185,8 +219,8 @@ them — no need to back them up before a routine redeploy. 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. +- Put a reverse proxy 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 @@ -194,7 +228,30 @@ network/VPN), do this before calling the deployment done: - 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): +**Check for an existing reverse proxy before introducing a new one** — +`ss -tlnp | grep -E ':80|:443'`. A VM already running other services almost +always already has one (Apache, Nginx, Traefik) terminating TLS for +everything on it; a second process trying to bind 80/443 will just fail to +start, and stopping the existing proxy to "free the port" would take every +other service on the box down with it. If one exists, add a vhost to it +instead. Apache example: + +```apache + + ServerName daybook.example.com + ProxyPreserveHost On + ProxyPass / http://127.0.0.1:8000/ + ProxyPassReverse / http://127.0.0.1:8000/ + SSLEngine on + SSLCertificateFile /etc/letsencrypt/live/daybook.example.com/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/daybook.example.com/privkey.pem + +``` + +(`certbot --apache` will provision and wire up the certificate for you.) + +Only reach for a standalone proxy like Caddy if the target genuinely has +nothing on 80/443 yet: ```text daybook.example.com { @@ -225,6 +282,9 @@ Before reporting the deployment complete, confirm all of these: - [ ] 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. +- [ ] If the target host has its own service-inventory, monitoring, or + changelog conventions for new services, they've been followed — ask + the operator if unsure what those are. 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 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index ee1dd7a..2303056 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -7,10 +7,12 @@ services: restart: unless-stopped env_file: - .env + mem_limit: 384m + mem_reservation: 128m ports: - - "8000:8000" + - "${DAYBOOK_BIND_ADDR:-0.0.0.0}:8000:8000" volumes: - - ./data:/data + - ${DAYBOOK_HOST_DATA_DIR:-./data}:/data healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/api/capabilities"] interval: 30s diff --git a/deploy/env.example b/deploy/env.example index 19b4cae..133e497 100644 --- a/deploy/env.example +++ b/deploy/env.example @@ -4,3 +4,13 @@ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example DAYBOOK_DATA_DIR=/data DATABASE_PATH=/data/db.sqlite3 ARTIFACTS_ROOT=/data/artifacts_store + +# Optional, read by deploy/docker-compose.yml directly (not by the app): +# DAYBOOK_BIND_ADDR=127.0.0.1 +# Uncomment and set to 127.0.0.1 when a reverse proxy on the same host will +# front this service — this stops the container port from being reachable +# directly from the network. Leave unset (binds all interfaces) only for +# private/VPN-only deployments with no proxy in front. +# DAYBOOK_HOST_DATA_DIR=/mnt/some-larger-volume/daybook-data +# Uncomment and point at a path on a separate/larger volume if the host's +# main disk is space-constrained. Defaults to ./data next to this file.