name: SnapOtter # CPU deployment — no GPU required. # Usage: docker compose up -d # GPU: docker compose -f docker-compose-gpu.yml up -d services: SnapOtter: build: context: .. dockerfile: docker/Dockerfile args: SNAPOTTER_ANALYTICS: "on" image: snapotter/snapotter:latest container_name: SnapOtter ports: # For internet-facing deployments, bind to localhost only: # - "127.0.0.1:1349:1349" - "1349:1349" volumes: - SnapOtter-data:/data # AI models, user files - SnapOtter-workspace:/tmp/workspace # Temp processing (auto-cleaned) environment: - AUTH_ENABLED=true - DEFAULT_USERNAME=admin # Set a strong password. Default is 'admin' -- CHANGE THIS for any non-local deployment. # - DEFAULT_PASSWORD=your-strong-password-here - DEFAULT_PASSWORD=admin - SKIP_MUST_CHANGE_PASSWORD=${SKIP_MUST_CHANGE_PASSWORD:-false} - MAX_UPLOAD_SIZE_MB=${MAX_UPLOAD_SIZE_MB:-0} - MAX_BATCH_SIZE=${MAX_BATCH_SIZE:-0} - MAX_MEGAPIXELS=${MAX_MEGAPIXELS:-0} - CONCURRENT_JOBS=${CONCURRENT_JOBS:-0} - MAX_WORKER_THREADS=${MAX_WORKER_THREADS:-0} - PROCESSING_TIMEOUT_S=${PROCESSING_TIMEOUT_S:-0} - MAX_PIPELINE_STEPS=${MAX_PIPELINE_STEPS:-20} - RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-1000} - MAX_USERS=${MAX_USERS:-0} - SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168} - TRUST_PROXY=${TRUST_PROXY:-true} - DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter} - REDIS_URL=redis://:${REDIS_PASSWORD:-snapotter}@redis:6379 # 1.x upgrade: uncomment to import the old SQLite database on first boot; # re-comment after the migration succeeds. # - SQLITE_MIGRATE_PATH=/data/snapotter.db # OIDC Authentication (optional) # - EXTERNAL_URL=https://photos.example.com # - OIDC_ENABLED=false # - OIDC_ISSUER_URL= # - OIDC_CLIENT_ID= # - OIDC_CLIENT_SECRET= # - OIDC_SCOPES=openid profile email # - OIDC_AUTO_CREATE_USERS=true # - OIDC_DEFAULT_ROLE=user # - OIDC_AUTO_LINK_USERS=false # - OIDC_PROVIDER_NAME= # - OIDC_USERNAME_CLAIM=preferred_username # - OIDC_CLOCK_TOLERANCE=30 # - COOKIE_SECRET= # # Docker secrets (_FILE convention): mount secrets as files instead of # passing them as plain-text env vars. Supported for sensitive vars only. # - DEFAULT_PASSWORD_FILE=/run/secrets/snapotter_password # - S3_ACCESS_KEY_ID_FILE=/run/secrets/s3_access_key # - S3_SECRET_ACCESS_KEY_FILE=/run/secrets/s3_secret_key # - OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_secret # - COOKIE_SECRET_FILE=/run/secrets/cookie_secret # - SNAPOTTER_LICENSE_KEY_FILE=/run/secrets/license_key restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy # --- Security hardening --- mem_limit: 6g memswap_limit: 6g cpus: 4 pids_limit: 512 cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID - DAC_OVERRIDE - FOWNER # KILL lets tini (PID 1, root) forward SIGTERM to the gosu-dropped # snapotter process on shutdown. Without it, root minus CAP_KILL cannot # signal a different-UID process, so docker stop is ungraceful # ("[FATAL tini] forwarding signal: Operation not permitted" -> SIGKILL). - KILL # NOTE: security_opt: [no-new-privileges:true] is intentionally omitted. # gosu requires setuid to drop from root to the snapotter user. # Mitigation: cap_drop: ALL limits available capabilities after privilege drop. # NOTE: read_only: true is not set because PUID/PGID remapping requires # writing to /etc/passwd and /etc/group. Consider using Docker --user flag # instead of PUID/PGID for read-only rootfs support. healthcheck: test: ["CMD", "curl", "-sf", "--max-time", "5", "http://localhost:1349/api/v1/health"] interval: 30s timeout: 5s start_period: 60s retries: 3 shm_size: '2gb' logging: driver: json-file options: max-size: "50m" max-file: "5" postgres: image: postgres:17-alpine container_name: SnapOtter-postgres environment: POSTGRES_USER: ${POSTGRES_USER:-snapotter} # Set a strong password. CHANGE THIS for any non-local deployment. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-snapotter} POSTGRES_DB: ${POSTGRES_DB:-snapotter} volumes: - SnapOtter-pgdata:/var/lib/postgresql/data restart: unless-stopped mem_limit: 1g healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter} -d ${POSTGRES_DB:-snapotter}"] interval: 10s timeout: 5s retries: 12 start_period: 15s redis: image: redis:8-alpine container_name: SnapOtter-redis command: >- redis-server --maxmemory-policy noeviction --maxmemory 512mb --appendonly yes --requirepass ${REDIS_PASSWORD:-snapotter} volumes: - SnapOtter-redisdata:/data restart: unless-stopped mem_limit: 1g healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-snapotter}", "--no-auth-warning", "ping"] interval: 10s timeout: 5s retries: 12 start_period: 10s # Uncomment to use Docker secrets (requires Docker Swarm or compose v2.23+): # secrets: # snapotter_password: # file: ./secrets/snapotter_password.txt # oidc_secret: # file: ./secrets/oidc_secret.txt volumes: SnapOtter-data: SnapOtter-workspace: SnapOtter-pgdata: SnapOtter-redisdata: