services: postgres: image: postgres:14-alpine container_name: seneca-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} POSTGRES_DB: ${POSTGRES_DB:-finflow} ports: - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] interval: 10s timeout: 5s retries: 5 networks: - finflow-network redis: image: redis:7-alpine container_name: seneca-redis restart: unless-stopped ports: - "127.0.0.1:${REDIS_PORT:-6379}:6379" volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: - finflow-network backend: # Prebuilt images are published by CD for every release (`latest` tracks # the newest v* tag; set IMAGE_TAG to pin one). `docker compose pull` # then starts the stack without building anything locally. image: ghcr.io/stoic-budget/stoic-budget-backend:${IMAGE_TAG:-latest} build: context: ./backend dockerfile: Dockerfile container_name: seneca-backend ports: - "${BACKEND_PORT:-3001}:3001" env_file: - path: .env required: false environment: - NODE_ENV=production - RUN_MIGRATIONS=true - PORT=3001 - FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000} - APP_URL=${APP_URL:-http://localhost:3000} - APP_NAME=${APP_NAME:-Stoic Budget} - REGISTRATION_ENABLED=${REGISTRATION_ENABLED:-true} - DATABASE_URL=postgresql://${POSTGRES_USER:?POSTGRES_USER is required}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-finflow} - REDIS_HOST=redis - REDIS_PORT=6379 # Both the cache and the statement-parsing queue read REDIS_URL. Without it # they fall back to localhost, which does not resolve inside the container. - REDIS_URL=redis://redis:6379 - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:?JWT_REFRESH_SECRET is required} - INTEGRATIONS_ENCRYPTION_KEY=${INTEGRATIONS_ENCRYPTION_KEY:?INTEGRATIONS_ENCRYPTION_KEY is required} # Access-token lifetime intentionally left to the code default (30m, # see auth.service.ts). The long-lived session lives in the refresh token. - JWT_REFRESH_EXPIRES_IN=30d - LOG_LEVEL=info volumes: - backend_uploads:/app/uploads - update_requests:/app/update depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - finflow-network restart: unless-stopped frontend: image: ghcr.io/stoic-budget/stoic-budget-frontend:${IMAGE_TAG:-latest} build: context: ./frontend dockerfile: Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3001/api/v1} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} NEXT_PUBLIC_APP_NAME: ${APP_NAME:-Stoic Budget} API_PROXY_TARGET: http://backend:3001 container_name: seneca-frontend ports: - "${FRONTEND_PORT:-3000}:3000" environment: - NODE_ENV=production - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001/api/v1} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - NEXT_PUBLIC_APP_NAME=${APP_NAME:-Stoic Budget} - NEXT_PUBLIC_ENV=production - API_PROXY_TARGET=http://backend:3001 depends_on: - backend networks: - finflow-network restart: unless-stopped # --- In-app updates (optional, off by default) ------------------------------- # Lets an instance admin run the equivalent of `make upgrade` from # Settings -> Advanced. It drives Docker through the socket, which amounts to # root on the host, so it is opt-in: add COMPOSE_PROFILES=updater to .env. updater: image: docker:28-cli container_name: seneca-updater profiles: ["updater"] entrypoint: ["sh", "/updater.sh"] volumes: - /var/run/docker.sock:/var/run/docker.sock - .:/project - ./infra/updater/updater.sh:/updater.sh:ro - update_requests:/update networks: - finflow-network restart: unless-stopped # --- Receipt maps (optional, off by default) --------------------------------- # Self-hosted tiles and geocoding for the receipt location map: # docker compose --profile maps --profile geocoder up -d # then set TILESERVER_URL=http://tileserver:8080 and GEOCODER_URL=http://nominatim:8080 # for the backend. The first start imports MAP_PBF_URL: minutes for a small # country, hours for a large one, and several GB of disk either way. map-assets: image: alpine:3.22 profiles: ["maps"] command: ["sh", "/scripts/prepare-assets.sh"] volumes: - ./infra/maps/prepare-assets.sh:/scripts/prepare-assets.sh:ro - ./infra/maps/tileserver-config.json:/scripts/tileserver-config.json:ro - map_data:/data restart: "no" map-tiles-init: image: ghcr.io/onthegomap/planetiler:0.10.2 profiles: ["maps"] # Builds OpenMapTiles vector tiles once; later starts see the file and exit. entrypoint: ["sh", "-c"] command: - >- [ -s /data/region.mbtiles ] && echo "Tiles already built" || exec java -cp @/app/jib-classpath-file com.onthegomap.planetiler.Main --download --download_dir=/data/sources --tmpdir=/data/tmp --osm_url="$${MAP_PBF_URL}" --output=/data/region.mbtiles environment: MAP_PBF_URL: ${MAP_PBF_URL:-https://download.geofabrik.de/asia/kazakhstan-latest.osm.pbf} volumes: - map_data:/data restart: "no" tileserver: image: maptiler/tileserver-gl:v5.6.0 container_name: seneca-tileserver profiles: ["maps"] command: ["--config", "/data/config.json", "--port", "8080"] # No published port: the backend proxies tiles to signed-in users. volumes: - map_data:/data depends_on: map-assets: condition: service_completed_successfully map-tiles-init: condition: service_completed_successfully networks: finflow-network: aliases: [tileserver] restart: unless-stopped nominatim: image: mediagis/nominatim:5.3 container_name: seneca-nominatim profiles: ["geocoder"] environment: PBF_URL: ${MAP_PBF_URL:-https://download.geofabrik.de/asia/kazakhstan-latest.osm.pbf} NOMINATIM_PASSWORD: ${NOMINATIM_PASSWORD:-nominatim} shm_size: 1gb # Imports the special phrases that let the backend search by kind of place. command: ["/seneca/start.sh"] volumes: - nominatim_data:/var/lib/postgresql/16/main - ./infra/nominatim:/seneca:ro networks: finflow-network: aliases: [nominatim] restart: unless-stopped volumes: postgres_data: redis_data: backend_uploads: update_requests: map_data: nominatim_data: networks: finflow-network: driver: bridge