services: postgres: image: postgres:17-alpine restart: always environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine restart: always volumes: - redis_data:/data command: redis-server --save 60 1 --requirepass ${REDIS_PASSWORD} minio: image: minio/minio:latest restart: always environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} # The browser PUTs photo uploads directly to MinIO using a presigned URL # (STORAGE_PUBLIC_URL) — without CORS allowed for that origin, the browser # blocks the request. Restricted to the app's own origin, not "*". MINIO_API_CORS_ALLOW_ORIGIN: ${BETTER_AUTH_URL} ports: # Published so the browser can reach MinIO directly for presigned # up/downloads — same pattern as `web`'s ${WEB_PORT}: your Traefik LXC # routes a (sub)domain to :${STORAGE_PORT}, and that public # URL is what STORAGE_PUBLIC_URL must be set to. - "${STORAGE_PORT:-9000}:9000" volumes: - minio_data:/data command: server /data --console-address ":9001" healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 5 minio-init: image: minio/mc:latest depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}; mc mb --ignore-existing local/epicure-uploads; mc anonymous set download local/epicure-uploads; exit 0; " migrate: build: context: . dockerfile: Dockerfile target: migrator environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} depends_on: postgres: condition: service_healthy restart: "no" web: build: context: . dockerfile: Dockerfile # Explicit target: the Dockerfile's last stage is no longer `runner` now # that `cron` was appended after it, and the default build target is # whatever stage comes last — pin it so this doesn't silently change. target: runner args: NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${NEXT_PUBLIC_VAPID_PUBLIC_KEY} NEXT_PUBLIC_GITHUB_ENABLED: ${NEXT_PUBLIC_GITHUB_ENABLED} NEXT_PUBLIC_DISCORD_ENABLED: ${NEXT_PUBLIC_DISCORD_ENABLED} NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED} STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL} NEXT_PUBLIC_STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL} restart: always environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 STORAGE_ENDPOINT: http://minio:9000 STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL} STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER} STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD} STORAGE_BUCKET: epicure-uploads BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} BETTER_AUTH_URL: ${BETTER_AUTH_URL} ENCRYPTION_SECRET: ${ENCRYPTION_SECRET} NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${NEXT_PUBLIC_VAPID_PUBLIC_KEY} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} NEXT_PUBLIC_GITHUB_ENABLED: ${NEXT_PUBLIC_GITHUB_ENABLED} DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID} DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET} NEXT_PUBLIC_DISCORD_ENABLED: ${NEXT_PUBLIC_DISCORD_ENABLED} AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID} AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET} AUTHENTIK_BASE_URL: ${AUTHENTIK_BASE_URL} NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} SMTP_SECURE: ${SMTP_SECURE} SMTP_USER: ${SMTP_USER} SMTP_PASS: ${SMTP_PASS} SMTP_FROM: ${SMTP_FROM} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} CRON_SECRET: ${CRON_SECRET} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} OPENROUTER_DEFAULT_MODEL: ${OPENROUTER_DEFAULT_MODEL} OPENAI_API_KEY: ${OPENAI_API_KEY} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} OLLAMA_BASE_URL: ${OLLAMA_BASE_URL} ports: # published on portainer LXC host; traefik (other LXC) routes here via :${WEB_PORT} - "${WEB_PORT:-3000}:3000" depends_on: migrate: condition: service_completed_successfully redis: condition: service_started minio: condition: service_healthy minio-init: condition: service_completed_successfully # Fires the weekly digest email send by POSTing to `web`'s internal cron # route on a schedule (see cron/crontab). Tiny alpine+curl+crond # image — not part of the app build, just a periodic HTTP trigger. digest-cron: build: context: . dockerfile: Dockerfile target: cron restart: always environment: CRON_SECRET: ${CRON_SECRET} WEB_INTERNAL_URL: http://web:3000 depends_on: - web volumes: postgres_data: redis_data: minio_data: