version: '3.8' services: backend: build: ./backend ports: - "127.0.0.1:8020:8000" volumes: - ./backend:/app - backend_data:/app/data environment: - DATABASE_URL=${DATABASE_URL:-postgresql://elmeg:elmeg_password@db:5432/elmeg} - SECRET_KEY=${SECRET_KEY:-demo-secret-change-in-production} # Postal SMTP (primary) - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT:-25} - SMTP_USERNAME=${SMTP_USERNAME} - SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_USE_TLS=${SMTP_USE_TLS:-true} # AWS SES (fallback) - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_SES_REGION=${AWS_SES_REGION} # Common - EMAIL_FROM=${EMAIL_FROM:-noreply@elmeg.xyz} - FRONTEND_URL=${FRONTEND_URL:-https://elmeg.xyz} command: sh start.sh depends_on: - db restart: unless-stopped healthcheck: test: [ "CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/docs').raise_for_status()" ] interval: 30s timeout: 10s retries: 3 networks: - elmeg - traefik-public - postal-internal labels: - "traefik.enable=true" - "traefik.http.routers.elmeg-backend.rule=(Host(`elmeg.runfoo.run`) || Host(`elmeg.xyz`)) && PathPrefix(`/api`)" - "traefik.http.routers.elmeg-backend.entrypoints=websecure" - "traefik.http.routers.elmeg-backend.tls.certresolver=letsencrypt" - "traefik.http.routers.elmeg-backend.priority=100" - "traefik.http.middlewares.elmeg-strip.stripprefix.prefixes=/api" - "traefik.http.routers.elmeg-backend.middlewares=elmeg-strip" - "traefik.http.routers.elmeg-backend.service=elmeg-backend-svc" - "traefik.http.services.elmeg-backend-svc.loadbalancer.server.port=8000" - "traefik.docker.network=traefik" # Direct routes for docs (no strip) - "traefik.http.routers.elmeg-backend-docs.rule=(Host(`elmeg.runfoo.run`) || Host(`elmeg.xyz`)) && PathPrefix(`/docs`, `/openapi.json`)" - "traefik.http.routers.elmeg-backend-docs.entrypoints=websecure" - "traefik.http.routers.elmeg-backend-docs.tls.certresolver=letsencrypt" - "traefik.http.routers.elmeg-backend-docs.priority=100" - "traefik.http.routers.elmeg-backend-docs.service=elmeg-backend-svc" frontend: build: context: ./frontend dockerfile: Dockerfile ports: - "127.0.0.1:3020:3000" volumes: - ./frontend:/app - /app/node_modules environment: - NEXT_PUBLIC_API_URL=/api - INTERNAL_API_URL=http://backend:8000 depends_on: backend: condition: service_healthy restart: unless-stopped networks: - elmeg - traefik-public labels: - "traefik.enable=true" - "traefik.http.routers.elmeg-frontend.rule=(Host(`elmeg.runfoo.run`) || Host(`elmeg.xyz`)) && !PathPrefix(`/api`, `/docs`, `/openapi.json`)" - "traefik.http.routers.elmeg-frontend.entrypoints=websecure" - "traefik.http.routers.elmeg-frontend.tls.certresolver=letsencrypt" - "traefik.http.routers.elmeg-frontend.priority=50" - "traefik.http.services.elmeg-frontend.loadbalancer.server.port=3000" - "traefik.docker.network=traefik" # bugs.elmeg.xyz subdomain - redirects to /bugs - "traefik.http.routers.elmeg-bugs.rule=Host(`bugs.elmeg.xyz`)" - "traefik.http.routers.elmeg-bugs.entrypoints=websecure" - "traefik.http.routers.elmeg-bugs.tls.certresolver=letsencrypt" - "traefik.http.middlewares.bugs-redirect.redirectregex.regex=^https://bugs\\.elmeg\\.xyz/(.*)" - "traefik.http.middlewares.bugs-redirect.redirectregex.replacement=https://elmeg.xyz/bugs/$${1}" - "traefik.http.middlewares.bugs-redirect.redirectregex.permanent=true" - "traefik.http.routers.elmeg-bugs.middlewares=bugs-redirect" - "traefik.http.routers.elmeg-bugs.service=elmeg-frontend" db: image: postgres:15-alpine environment: - POSTGRES_USER=elmeg - POSTGRES_PASSWORD=elmeg_password - POSTGRES_DB=elmeg volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: [ "CMD-SHELL", "pg_isready -U elmeg -d elmeg" ] interval: 10s timeout: 5s retries: 5 networks: - elmeg db-backup: image: prodrigestivill/postgres-backup-local:15-alpine restart: unless-stopped volumes: - ./backups:/backups - postgres_data:/var/lib/postgresql/data:ro environment: - POSTGRES_HOST=db - POSTGRES_DB=elmeg - POSTGRES_USER=elmeg - POSTGRES_PASSWORD=elmeg_password - SCHEDULE=@daily - BACKUP_KEEP_DAYS=7 - BACKUP_KEEP_WEEKS=4 - BACKUP_KEEP_MONTHS=6 - HEALTHCHECK_PORT=80 depends_on: - db networks: - elmeg # Weekly digest email cron job (runs Sunday 9am UTC) cron: build: ./backend volumes: - ./backend:/app environment: - DATABASE_URL=${DATABASE_URL:-postgresql://elmeg:elmeg_password@db:5432/elmeg} - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT:-25} - SMTP_USERNAME=${SMTP_USERNAME} - SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_USE_TLS=${SMTP_USE_TLS:-true} - EMAIL_FROM=${EMAIL_FROM:-noreply@elmeg.xyz} - FRONTEND_URL=${FRONTEND_URL:-https://elmeg.xyz} command: > sh -c "echo '0 9 * * 0 cd /app && python services/weekly_digest.py >> /var/log/cron.log 2>&1' | crontab - && crond -f -l 2" depends_on: - db - backend restart: unless-stopped networks: - elmeg - postal-internal umami: image: ghcr.io/umami-software/umami:postgresql-latest restart: unless-stopped environment: - DATABASE_URL=postgresql://umami:umami_password@umami-db:5432/umami - APP_SECRET=${UMAMI_SECRET:-highly-secret-key-change-this} - TRACKER_SCRIPT_NAME=stats depends_on: - umami-db networks: - elmeg - traefik-public labels: - "traefik.enable=true" - "traefik.http.routers.elmeg-umami.rule=Host(`stats.elmeg.xyz`) || Host(`stats.elmeg.runfoo.run`)" - "traefik.http.routers.elmeg-umami.entrypoints=websecure" - "traefik.http.routers.elmeg-umami.tls.certresolver=letsencrypt" - "traefik.http.services.elmeg-umami.loadbalancer.server.port=3000" - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik}" umami-db: image: postgres:15-alpine environment: - POSTGRES_USER=umami - POSTGRES_PASSWORD=umami_password - POSTGRES_DB=umami volumes: - umami_data:/var/lib/postgresql/data restart: unless-stopped networks: - elmeg volumes: postgres_data: backend_data: umami_data: networks: elmeg: traefik-public: name: ${TRAEFIK_NETWORK:-traefik} external: true postal-internal: name: postal_postal-internal external: true