# 06 — Docker / Unraid Design ## Containers | Container | Image | Role | |---|---|---| | `notizarr` | built locally from `apps/server` (multi-stage, bundles the built `apps/web` static assets into the final image) | app + web UI | | `notizarr-postgres` | `postgres:16-alpine` (pinned to a specific digest/patch tag at Phase 2) | database | No Redis, no worker, no reverse proxy container — Pangolin is external and already run separately per the user's existing setup. ## Networking - Single user-defined bridge network (`notizarr_internal`), so `notizarr` reaches Postgres via the service name `notizarr-postgres:5432` and Postgres is never bound to a host port. - `notizarr` publishes two host ports: - `8080` → web/PWA + OIDC + admin. In Unraid's port mapping, this should be bound to the LAN interface (or left for Pangolin to reach over the Docker network directly, if Pangolin also runs in Docker — recommended, see note below) rather than published on `0.0.0.0` if the host has any direct WAN exposure. - `8081` → LAN ingestion only. **Must** be bound to a LAN-only address in Unraid's port mapping (e.g. `192.168.1.10:8081:8081`, not `0.0.0.0:8081:8081`), and/or restricted at the router/firewall level. The app additionally enforces `INGESTION_ALLOWED_CIDRS` as defense in depth (see threat model). - **Recommended topology if Pangolin also runs under Docker on the same Unraid host:** attach `notizarr` to Pangolin's Docker network as well (or put both on a shared network) so Pangolin reaches `notizarr:8080` directly over the internal network without needing 8080 published to the host at all. This is the cleanest setup and is called out as the recommended option in the deployment README written in Phase 2, while still supporting a published-port fallback for other reverse-proxy layouts. ## Volumes | Host path (under `/mnt/tank/appdata/notizarr/`) | Container path | Purpose | |---|---|---| | `postgres/` | `/var/lib/postgresql/data` | Postgres data directory (named volume backed by this bind mount) | | `app/` | `/data` | reserved for any future app-writable state; empty/unused in v1 since VAPID keys and settings live in Postgres | | `backups/` | (used by the documented backup script, not mounted into either container at runtime) | `pg_dump` output destination | | `logs/` | (optional bind mount if file logging is enabled) | stdout JSON logs are the default and are what `docker logs` / Unraid's log viewer shows; this folder exists for operators who want to additionally pipe logs to a file via `docker compose`'s logging driver | ## Healthchecks ```yaml notizarr-postgres: healthcheck: test: ["CMD-SHELL", "pg_isready -U notizarr -d notizarr"] interval: 10s timeout: 5s retries: 5 notizarr: healthcheck: test: ["CMD", "node", "healthcheck.js"] # small script hitting http://127.0.0.1:8080/health interval: 15s timeout: 5s retries: 3 start_period: 20s ``` `notizarr` has `depends_on: notizarr-postgres: condition: service_healthy`, so it never starts serving traffic against a not-yet-ready database. ## Restart policy, non-root, pinned versions - `restart: unless-stopped` on both services. - `notizarr`'s Dockerfile creates and runs as a non-root `appuser` (`USER appuser` in the final stage); the official `postgres:16-alpine` image already drops to the `postgres` user via its entrypoint. - Every image reference is pinned to an explicit tag (and ideally digest) — no `:latest` anywhere in the compose file. ## Environment variables (`.env`) ```env # Identity / URLs PUBLIC_BASE_URL=https://notizarr.example.com PORT_WEB=8080 PORT_INGEST=8081 # Auth AUTH_MODE=oidc # oidc | proxy | disabled OIDC_ISSUER_URL= OIDC_CLIENT_ID= OIDC_CLIENT_SECRET= OIDC_REDIRECT_URL=https://notizarr.example.com/api/v1/auth/oidc/callback OIDC_SCOPES=openid profile email AUTH_PROXY_USER_HEADER= AUTH_PROXY_EMAIL_HEADER= AUTH_PROXY_GROUPS_HEADER= TRUSTED_PROXY_CIDRS= # Database POSTGRES_USER=notizarr POSTGRES_PASSWORD= POSTGRES_DB=notizarr DATABASE_URL=postgres://notizarr:${POSTGRES_PASSWORD}@notizarr-postgres:5432/notizarr # Push VAPID_SUBJECT=mailto:admin@example.com # Ingestion safety INGESTION_ALLOWED_CIDRS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 # Retention / rate limiting (defaults; also editable in Admin > Settings) RETENTION_DEFAULT_DAYS=90 RATE_LIMIT_SUSTAINED_PER_MIN=60 RATE_LIMIT_BURST_PER_10S=20 # Ops LOG_LEVEL=info DOCS_ENABLED=true ``` `.env.example` (committed) documents every variable; the real `.env` on the Unraid host is never committed and is included in the backup procedure. ## Illustrative `docker-compose.yml` (final version written in Phase 2) ```yaml services: notizarr-postgres: image: postgres:16-alpine container_name: notizarr-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - ./postgres:/var/lib/postgresql/data networks: [notizarr_internal] healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 notizarr: image: notizarr:latest # built via `docker compose build` container_name: notizarr restart: unless-stopped depends_on: notizarr-postgres: condition: service_healthy env_file: .env environment: DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@notizarr-postgres:5432/${POSTGRES_DB} ports: - "8080:8080" - "8081:8081" volumes: - ./app:/data networks: [notizarr_internal] healthcheck: test: ["CMD", "node", "healthcheck.js"] interval: 15s timeout: 5s retries: 3 start_period: 20s networks: notizarr_internal: ``` ## Backup / restore - **Backup:** a documented script (`docker compose exec notizarr-postgres pg_dump -U notizarr notizarr | gzip > backups/notizarr-$(date +%F).sql.gz`) — since VAPID keys and all settings live in Postgres, a Postgres backup alone is a complete backup. `.env` is backed up separately (it's a plain file, not a container volume) since it holds the OIDC client secret and DB password. - **Restore:** `docker compose down`, `gunzip -c backup.sql.gz | docker compose exec -T notizarr-postgres psql -U notizarr notizarr`, `docker compose up -d`. Full runnable instructions land in the Phase 2 README.