daybook/docs/deployment.md
peter ca8e364010 Fix healthcheck + verify commands; reframe deploy runbook for a capable agent
Real bug: the container healthcheck (and the docs' verify step) hit
/api/capabilities, which returns 403 unauthenticated; curl --fail treats
403 as an error, so a healthy container would report "unhealthy" and the
verify step would look like a failure. Healthcheck now hits /accounts/login/
(200, no auth). Verify commands now print the status code and note not to
use curl -f on the 403.

Also reframed cloud_agent_deploy.md's intro for an Opus-class deploy agent:
it's a guide + Daybook-specific gotchas, not a rigid script — points at the
authoritative sources (repo incl. the git-SSH :2222 note, deploy/ files,
requirements.txt deps auto-installed, entrypoint runs migrate+collectstatic,
docs/deployment.md) and trusts the agent's judgment. Dropped the now-
redundant Source Of Truth section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 14:32:39 +01:00

202 lines
6.3 KiB
Markdown

# Deployment
Daybook deploys the same way on any Linux host with Docker: a cloud VM, a home
server, a Proxmox guest, whatever. It runs as a single Docker container
(Gunicorn) with all persistent state — the SQLite database and artifact files —
on a bind-mounted `./data` directory outside the image.
```text
Target host (cloud VM, home server, ...)
Docker Engine
daybook container
/app application code baked into the image
/data persistent volume: db.sqlite3, artifacts_store/
```
## Files In `deploy/`
| File | Purpose |
|---|---|
| `Dockerfile` | Builds the Daybook image. |
| `docker-compose.yml` | Runs Daybook with a persistent `./data:/data` bind mount. |
| `entrypoint.sh` | Runs migrations, collects static files, then starts Gunicorn. |
| `env.example` | Template for deployment environment variables. |
## Prerequisites On The Target Host
- Docker Engine + the Docker Compose plugin
- Git (for the clone-and-pull workflow below)
```bash
docker --version
docker compose version
```
## First-Time Setup
Clone the repo onto the host at a stable path:
```bash
git clone <REPO_URL> /opt/daybook
cd /opt/daybook
```
Create the environment file:
```bash
cp deploy/env.example deploy/.env
```
Generate a secret key:
```bash
python3 -c "import secrets; print('DJANGO_SECRET_KEY=' + secrets.token_urlsafe(50))"
```
Edit `deploy/.env` and set at minimum:
```text
DJANGO_DEBUG=0
DJANGO_SECRET_KEY=<generated-secret>
DJANGO_ALLOWED_HOSTS=<vm-ip-or-dns-name>,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 configure — Daybook uses per-user tokens
(DRF `Token` model) issued from Settings → Account once a user exists. Do not
commit `deploy/.env`.
Create the persistent data directory and bring the stack up:
```bash
mkdir -p deploy/data
docker compose -f deploy/docker-compose.yml up -d --build
```
Create your first login (superuser) so you have something to log in with and
a personal API token to post through:
```bash
docker compose -f deploy/docker-compose.yml exec daybook python manage.py createsuperuser
```
## Verify Deployment
```bash
docker compose -f deploy/docker-compose.yml ps
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/api/capabilities # expect 403
```
An unauthenticated capabilities check returns 403 (login/token required) —
that's expected; it confirms the app is up and enforcing auth. (Don't add
`curl -f` — it treats the 403 as an error. The container healthcheck uses
the login page, which is 200 without auth.) Open
`http://<vm-ip>:8000/` in a browser and log in with the superuser you just
created, then grab a personal token from Settings → Account and use it for a
smoke-test post:
```bash
curl -fsS -X POST http://127.0.0.1:8000/api/artifacts \
-H "Authorization: Bearer <your-personal-token>" \
-H "Content-Type: application/json" \
-d '{
"source": "deploy-smoke",
"kind": "markdown",
"title": "Deployment Smoke Test",
"content": "# Deployment Smoke Test\n\nDaybook accepted an artifact."
}'
```
## Fast Redeploys
Once the host has a git clone, shipping a new version is two commands:
```bash
cd /opt/daybook
git pull
docker compose -f deploy/docker-compose.yml up -d --build
```
The entrypoint runs Django migrations on every start, so schema changes are
picked up automatically. `deploy/.env` and `deploy/data/` are untouched by
`git pull` since they aren't tracked in the repo.
## Operational Commands
```bash
docker compose -f deploy/docker-compose.yml logs -f daybook
docker compose -f deploy/docker-compose.yml restart daybook
docker compose -f deploy/docker-compose.yml down
docker compose -f deploy/docker-compose.yml up -d
docker compose -f deploy/docker-compose.yml exec daybook python manage.py check
docker compose -f deploy/docker-compose.yml exec daybook sh
```
## Backup And Restore
Back up `deploy/data/` — it contains the SQLite database and all artifact
files:
```bash
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
```
For a consistent snapshot during heavy writes, stop the container first:
```bash
docker compose -f deploy/docker-compose.yml down
tar -czf daybook-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /opt/daybook/deploy data
docker compose -f deploy/docker-compose.yml up -d
```
To restore: stop Daybook, replace `deploy/data/` with the backup contents,
then start it again.
## Reverse Proxy Notes
If putting Daybook behind Nginx, Caddy, Traefik, or a cloud load balancer:
- Proxy to `http://<vm-ip>:8000`.
- Add the public hostname to `DJANGO_ALLOWED_HOSTS`.
- Terminate TLS at the proxy; keep `DJANGO_DEBUG=0`.
- Set `DJANGO_CSRF_TRUSTED_ORIGINS` to the public HTTPS origin, **and**
configure the proxy to forward `X-Forwarded-Proto: https`. Without both,
login and any other POST fails with "CSRF verification failed" — Django
has no way to know the original request was HTTPS once the proxy
terminates TLS and forwards plain HTTP to the container.
```text
DJANGO_ALLOWED_HOSTS=daybook.example.com,localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://daybook.example.com
```
Forwarding the proto header, by proxy:
```text
Nginx: proxy_set_header X-Forwarded-Proto $scheme;
Apache: RequestHeader set X-Forwarded-Proto "https" (requires a2enmod headers)
Caddy: header_up X-Forwarded-Proto https
```
## Deployment Checklist
- `deploy/.env` exists with a real (non-placeholder) `DJANGO_SECRET_KEY`.
- `DJANGO_DEBUG=0`.
- `DJANGO_ALLOWED_HOSTS` includes the hostname/IP being used.
- If behind a reverse proxy: `DJANGO_CSRF_TRUSTED_ORIGINS` is set and the
proxy forwards `X-Forwarded-Proto: https` (login will 403 otherwise).
- `deploy/data/` exists and persists across `git pull` + rebuild.
- `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy.
- `GET /api/capabilities` returns 403 without auth, 200 with a valid token or
session (check the status code — don't use `curl -f`, which errors on 403).
- At least one superuser exists (`manage.py createsuperuser`) with a token
from Settings → Account.
## Known MVP Constraints
- SQLite is used by default — fine for a single-VM self-hosted deployment.
- Per-user ingest tokens; no shared/global token.
- Reader/browser access requires login; feeds are private per user.
- Manual drag/drop upload and content revisions are deferred.