# J621 deployment (Docker + Tailscale) Three compose variants, all behind an **nginx** service and a **Tailscale** sidecar. Nothing is published on the host's ports and no system nginx or reverse proxy is involved: the sidecar shares the nginx service's network namespace and Tailscale Serve/Funnel exposes it. | Compose | Services | Funnel | Serve config | | --- | --- | --- | --- | | `compose.yml` (default) | mariadb, redis, backend, frontend, nginx, tailscale | `https://..ts.net` → nginx → backend + SPA | `serve.default.json` | | `compose.frontend.yml` | frontend, nginx, tailscale | `https://..ts.net` → nginx → SPA only | `serve.frontend.json` | | `compose.backend.yml` | mariadb, redis, backend, nginx, tailscale | `https://..ts.net:8443` → nginx → API | `serve.backend.json` | Funnel can only expose ports **443**, **8443** and **10000**, so the separate backend uses 8443. Change it in `serve.backend.json` (and `.env`) if you prefer 10000. ## Usage ```bash cd deploy cp .env.example .env # fill in TS_AUTHKEY, SECRET_KEY, ALLOWED_HOSTS, ... docker compose up -d # default: everything on one host # or docker compose -f compose.frontend.yml up -d docker compose -f compose.backend.yml up -d ``` The images are pulled from the Gitea registry; append `--build` (or run the push scripts) if you build locally. Data lives in `deploy/data/` (`media/`, `logs/`, `mariadb/`, `redis/`) and the Tailscale node state in `deploy/tailscale-state/`; both are git-ignored. ## First start The SPA asks where the backend is (`/setup`) in production builds: - **Default compose** — leave the field blank (same origin) and everything works through nginx. - **Frontend-only + backend-only** — point the (cross-origin) SPA at the backend's funnel URL, e.g. `https://j621-backend..ts.net:8443`, and give the backend `CORS_ALLOWED_ORIGINS=https://..ts.net` (plus `CSRF_TRUSTED_ORIGINS` for the admin). - Without a backend at all, choose **"Continue without a backend"** to run in local mode (e621 browsing only). Any origin and port is fine — the backend builds its media URLs from the forwarded host/proto (`TRUST_PROXY_HEADERS=true` is set in all composes). ## Images Two Dockerfiles, both built from the repository root: ```bash docker build -f deploy/J621-Frontend -t j621-frontend . docker build --build-arg "GIT_HASH=$(git rev-parse --short HEAD)" \ -f deploy/J621-Backend -t j621-backend . ``` `J621-Frontend` builds the SPA and serves it as static files. `J621-Backend` is Django + gunicorn (migrations run on start) and includes ffmpeg for video thumbnails. The `GIT_HASH` build arg is baked into the backend image so the shell's version pill shows the commit (images have no `.git` directory); the push scripts pass it automatically. Push multi-arch images to the Gitea registry: ```bash ./push_all.sh # or push_frontend.sh / push_backend.sh [sha] ``` They tag `:latest` and `:` and expect `docker login gitea.rainbow-herring.ts.net` to succeed. ## Tests The security/permission suite lives in `backend/apps/core/tests/`: ```bash cd ../backend ./venv/bin/python manage.py test apps.core.tests ``` It needs a one-time grant on the database user (test databases are created from scratch): ```sql GRANT ALL ON `test_j621`.* TO 'j621'@'%'; ``` ## Notes - The nginx service is the only entry point: `/api`, `/admin`, `/static` and `/health` go to the backend, everything else to the SPA. Both upstreams are resolved at request time, so the same config works in all three variants. - The compose healthchecks use `/nginx-health` (nginx itself), `/health` (Django + database) and the frontend's static server, so `depends_on: condition: service_healthy` gates the sidecar on a working stack. - `deploy/data/media/library` is the watched folder; drop files there (or use uploads) and run `manage.py scan_files` inside the backend container.