# Kvitt production stack on the Kvitt Pi (SPEC §10.2). Deployed by install.sh to /opt/kvitt/compose.yaml together # with /opt/kvitt/Caddyfile and /opt/kvitt/.env (from .env.example). Run every command from /opt/kvitt. # Only caddy publishes a port (8443); the server, Caddy's admin API and the sidecars are reachable only on the # internal network `kvitt` (SPEC §8.9). # Image names: ${KVITT_REGISTRY}/kvitt-, the names the Gitea Actions workflow pushes (D-104). name: kvitt x-logging: &logging driver: json-file options: max-size: "10m" max-file: "3" services: caddy: # Official image, pinned by version and multi-arch index digest (resolved 2026-10-18, infra/README.md). image: docker.io/library/caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af restart: unless-stopped # Non-root (D-117): uid 10002, plus group 1000 (cert-sync's group) to read the 0640 files in the 0750 /certs. # 8443 needs no capability, but the image's /usr/bin/caddy carries the file capability cap_net_bind_service=ep, # and the kernel refuses to exec it when that capability is not in the bounding set (checked 2026-10-18: # "exec container process /usr/bin/caddy: Operation not permitted" with cap_drop ALL alone). So NET_BIND_SERVICE # is the one capability kept. caddy_data (/data) is owned by 10002 (install.sh); /config (autosave of the loaded # config) and /tmp are tmpfs, the rest of the root filesystem is read-only. user: "10002:10002" group_add: ["1000"] read_only: true cap_drop: [ALL] cap_add: [NET_BIND_SERVICE] security_opt: - no-new-privileges:true tmpfs: - /config - /tmp mem_limit: 64m ports: - "8443:8443" environment: TZ: Europe/Stockholm volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - certs:/certs:ro - caddy_data:/data depends_on: cert-sync: condition: service_healthy server: condition: service_started healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:2019/config/ >/dev/null"] interval: 30s timeout: 5s retries: 3 start_period: 10s logging: *logging networks: [kvitt] server: image: ${KVITT_REGISTRY:?set KVITT_REGISTRY in .env}/kvitt-server:${KVITT_TAG:?set KVITT_TAG in .env} command: ["serve"] restart: unless-stopped user: "10001:10001" read_only: true cap_drop: [ALL] security_opt: - no-new-privileges:true mem_limit: 160m environment: TZ: Europe/Stockholm # The remaining server settings keep their SPEC §10.4 defaults (KVITT_PUBLIC_URL # https://icehack.asuscomm.com:8443, KVITT_TRUSTED_PROXY caddy, KVITT_FCM_PROJECT_ID kvitt-1337, ...). KVITT_DATA_DIR: /data volumes: - data:/data - /opt/kvitt/secrets/fcm-service-account.json:/secrets/fcm-service-account.json:ro # Empty except during the one-off Splitwise import (SPEC §10.8, D-025). - /opt/kvitt/import:/import:ro healthcheck: test: ["CMD", "kvitt-server", "healthcheck"] interval: 30s timeout: 5s retries: 3 start_period: 20s logging: *logging networks: [kvitt] cert-sync: image: ${KVITT_REGISTRY}/kvitt-cert-sync:${KVITT_TAG} restart: unless-stopped user: "1000:1000" cap_drop: [ALL] security_opt: - no-new-privileges:true mem_limit: 32m environment: TZ: Europe/Stockholm HA_HOST: ${HA_HOST:?set HA_HOST in .env} HA_PORT: ${HA_PORT:-22} HA_USER: ${HA_USER:?set HA_USER in .env} HA_CERT_DIR: ${HA_CERT_DIR:?set HA_CERT_DIR in .env} volumes: - certs:/certs - ./Caddyfile:/etc/caddy/Caddyfile:ro - /opt/kvitt/secrets/ha_ssh_key:/secrets/ha_ssh_key:ro - /opt/kvitt/secrets/ha_known_hosts:/secrets/known_hosts:ro healthcheck: test: ["CMD-SHELL", "test -s /certs/fullchain.pem && test -s /certs/privkey.pem"] interval: 30s timeout: 5s retries: 3 start_period: 60s logging: *logging networks: [kvitt] backup: image: ${KVITT_REGISTRY}/kvitt-backup:${KVITT_TAG} restart: unless-stopped # uid 10001 like the server, so it can open the WAL database files the server owns; rclone.conf on the Pi is # therefore owned by 10001 (infra/README.md "Backup uid", D-105). user: "10001:10001" cap_drop: [ALL] security_opt: - no-new-privileges:true mem_limit: 96m environment: TZ: Europe/Stockholm RCLONE_REMOTE: ${RCLONE_REMOTE:-gdrive:} RCLONE_FOLDER_ID: ${RCLONE_FOLDER_ID:?set RCLONE_FOLDER_ID in .env} volumes: - data:/data - backups:/backups - /opt/kvitt/secrets/rclone.conf:/config/rclone/rclone.conf:ro healthcheck: test: ["CMD", "/healthcheck.sh"] interval: 5m timeout: 10s retries: 1 start_period: 30s logging: *logging networks: [kvitt] volumes: data: certs: caddy_data: backups: networks: kvitt: driver: bridge