# Kvitt on the Kvitt Pi (SPEC §10.2, D-125): a self-contained compose project in /home/jacobhagstedt/kvitt. # That folder holds this file, .env (from .env.example), secrets/ and the bind-mounted folders data/, backups/ # and import/; nothing outside it is used. Run every command there (infra/README.md "Pi: what the owner does"). # Both containers run as the owner's uid/gid (KVITT_UID, KVITT_GID in .env), so every file they write in data/ and # backups/ belongs to the owner. Create data/ (and data/apk), backups/ and import/ as the owner before the first # start: a folder Docker creates itself belongs to root and the containers cannot write to it. # TLS terminates in Nginx Proxy Manager on the Home Assistant Pi (SPEC §10.5, D-120), which forwards plain HTTP # over the LAN to the server's published port 8080, the only published port. name: kvitt x-logging: &logging driver: json-file options: max-size: "10m" max-file: "3" services: server: image: ${KVITT_REGISTRY:?set KVITT_REGISTRY in .env}/kvitt-server:${KVITT_TAG:?set KVITT_TAG in .env} command: ["serve"] restart: unless-stopped user: "${KVITT_UID:?set KVITT_UID in .env}:${KVITT_GID:?set KVITT_GID in .env}" read_only: true tmpfs: - /tmp:size=16m cap_drop: [ALL] security_opt: - no-new-privileges:true mem_limit: 160m ports: - "8080:8080" environment: TZ: Europe/Stockholm KVITT_DATA_DIR: /data KVITT_PUBLIC_URL: https://${KVITT_PUBLIC_HOST:?set KVITT_PUBLIC_HOST in .env} # NPM's LAN address: only a request whose TCP peer is this address may set X-Forwarded-For, whose last entry # is then the client IP of the rate limits (SPEC §8.5, §8.6, D-120). An IPv4 address, not a host name. KVITT_TRUSTED_PROXY: ${HA_HOST:?set HA_HOST in .env} # The other server settings keep their SPEC §10.4 defaults (KVITT_FCM_CREDENTIALS # /secrets/fcm-service-account.json, KVITT_FCM_PROJECT_ID kvitt-1337, ...). volumes: # SQLite database, pictures and APKs (data/apk/). - ./data:/data # Only the FCM key, not rclone.conf (D-117). create_host_path false: a missing file stops the start with # "bind source path does not exist" instead of Docker creating a root-owned directory in its place. - type: bind source: ./secrets/fcm-service-account.json target: /secrets/fcm-service-account.json read_only: true bind: create_host_path: false # Empty except during the one-off Splitwise import (SPEC §10.8, D-025). - ./import:/import:ro healthcheck: test: ["CMD", "kvitt-server", "healthcheck"] interval: 30s timeout: 5s retries: 3 start_period: 20s logging: *logging backup: image: ${KVITT_REGISTRY}/kvitt-backup:${KVITT_TAG} restart: unless-stopped # The server's uid, so sqlite3 .backup can open the WAL database files the server owns (D-105). user: "${KVITT_UID}:${KVITT_GID}" read_only: true # backup.sh's private copy of rclone.conf and the start marker of the healthcheck live here. tmpfs: - /tmp:size=16m cap_drop: [ALL] security_opt: - no-new-privileges:true mem_limit: 96m environment: TZ: Europe/Stockholm # The owner's uid has no account in the image; rclone keeps its cache under HOME. HOME: /tmp # The [gdrive] remote in secrets/rclone.conf. RCLONE_REMOTE: "gdrive:" RCLONE_FOLDER_ID: ${RCLONE_FOLDER_ID:?set RCLONE_FOLDER_ID in .env} volumes: - ./data:/data - ./backups:/backups # Only rclone.conf (Drive token), read-only; backup.sh copies it to /tmp because rclone writes refreshed # tokens back (D-105). - type: bind source: ./secrets/rclone.conf target: /config/rclone/rclone.conf read_only: true bind: create_host_path: false healthcheck: test: ["CMD", "/healthcheck.sh"] interval: 5m timeout: 10s retries: 1 start_period: 30s logging: *logging