services: postgres: image: postgres:16 container_name: devfolio_postgres environment: POSTGRES_USER: devfolio POSTGRES_PASSWORD: devfolio POSTGRES_DB: devfolio volumes: - postgres_data:/var/lib/postgresql/data ports: - '15432:5432' healthcheck: test: [ 'CMD-SHELL', 'pg_isready -U devfolio' ] interval: 10s timeout: 5s retries: 5 restart: unless-stopped redis: image: redis:7-alpine container_name: devfolio_redis command: redis-server --appendonly yes volumes: - redis_data:/data ports: - '6379:6379' healthcheck: test: [ 'CMD', 'redis-cli', 'ping' ] interval: 10s timeout: 5s retries: 5 restart: unless-stopped api: image: ${IMAGE_API:-devfolio-api} build: context: . dockerfile: apps/api/Dockerfile container_name: devfolio_api # Resolve DNS via public resolvers rather than the host/cloud resolver, whose # aggressive caching can return a stale challenge TXT and fail domain # verification right after a user updates their records. dns: - 1.1.1.1 - 8.8.8.8 env_file: .env environment: NODE_ENV: production DATABASE_URL: postgresql://devfolio:devfolio@postgres:5432/devfolio REDIS_URL: redis://redis:6379 REDIS_HOST: redis STORAGE_LOCAL_PATH: /app/uploads ATS_ENGINE_URL: http://ats-engine:8000 OLLAMA_URL: http://ollama:11434 volumes: - uploads:/app/uploads ports: - '3001:3001' depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: [ 'CMD-SHELL', 'wget -qO- http://localhost:3001/api/v1/health || exit 1' ] interval: 15s timeout: 5s retries: 5 start_period: 30s restart: unless-stopped # Stateless resume ↔ job-description matcher (services/ats-engine). # Internal network only — no published ports; the API is its only caller. ats-engine: image: ${IMAGE_ATS_ENGINE:-devfolio-ats-engine} build: context: services/ats-engine container_name: devfolio_ats_engine restart: unless-stopped # Self-hosted LLM for the AI Writing feature (Improve/Grammar/Shorter). # Free, private, no API key — internal network only, the API is its only # caller via Ollama's OpenAI-compatible /v1/chat/completions endpoint. # # OPT-IN via Compose profile — a plain `docker compose up -d` will NOT # start these, on purpose. llama3.2:3b needs ~2-3GB RAM and pulls ~4GB of # image+model on first boot; a small VPS (e.g. a 1GB EC2 t3.micro already # running Postgres/Redis/API/Web/ats-engine) does not have that headroom, # and a plain `up` used to try the pull anyway and could fill the disk. # Enable with: `docker compose --profile ollama up -d`, or set # `COMPOSE_PROFILES=ollama` in .env. Servers that can't spare the RAM # should set AI_PROVIDER=openrouter instead (see README > AI Writing Setup) # and never need this profile at all. ollama: image: ollama/ollama:latest container_name: devfolio_ollama profiles: [ 'ollama' ] volumes: - ollama_data:/root/.ollama healthcheck: test: [ 'CMD-SHELL', 'ollama list || exit 1' ] interval: 10s timeout: 5s retries: 5 start_period: 20s restart: unless-stopped # One-shot: pulls the default model on first boot, then exits. Re-running # `docker compose up` is a no-op once the model is already on the volume. ollama-init: image: ollama/ollama:latest container_name: devfolio_ollama_init profiles: [ 'ollama' ] entrypoint: [ '/bin/sh', '-c', 'OLLAMA_HOST=ollama:11434 ollama pull ${OLLAMA_MODEL:-llama3.2:3b}' ] depends_on: ollama: condition: service_healthy restart: 'no' web: image: ${IMAGE_WEB:-devfolio-web} build: context: . dockerfile: apps/web/Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3001} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} NEXT_PUBLIC_PRIMARY_HOST: ${NEXT_PUBLIC_PRIMARY_HOST:-devfolioapp.cloud} container_name: devfolio_web env_file: .env environment: PORT: '3000' # Server-side (SSR) calls run inside this container, where the API is the # `api` service on the compose network — not localhost. The browser still # uses NEXT_PUBLIC_API_URL (baked at build time). INTERNAL_API_URL: http://api:3001 ports: - '3000:3000' depends_on: - api restart: unless-stopped caddy: image: caddy:2 container_name: devfolio_caddy # Terminates TLS for the primary hosts (named certs) and for user custom # domains (on-demand certs, gated by the API's /portfolios/domain-check). # Reaches the app over the compose network via the `web`/`api` service names. ports: - '80:80' - '443:443' volumes: - ./infra/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config environment: ACME_EMAIL: ${ACME_EMAIL:-admin@devfolioapp.cloud} depends_on: - web - api restart: unless-stopped export-worker: build: context: . dockerfile: workers/export/Dockerfile container_name: devfolio_export_worker env_file: .env environment: DATABASE_URL: postgresql://devfolio:devfolio@postgres:5432/devfolio REDIS_URL: redis://redis:6379 REDIS_HOST: redis STORAGE_LOCAL_PATH: /app/uploads volumes: - uploads:/app/uploads depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped volumes: postgres_data: redis_data: uploads: caddy_data: caddy_config: ollama_data: