Configuration
Docker Compose
Cleanmails runs as a six-container Docker Compose stack: PostgreSQL, Redis, API server, background worker, Next.js frontend, and Caddy reverse proxy.
Services
| Service | Image | Purpose | Ports |
|---|---|---|---|
postgres | postgres:16-alpine | Primary database | 5432 (internal) |
redis | redis:7-alpine | Task queue & cache (Asynq) | 6379 (internal) |
api | cleanmails-api:latest | Go API server (Gin) | 8080 (internal) |
worker | cleanmails-worker:latest | Background job processor | None |
frontend | cleanmails-frontend:latest | Next.js dashboard | 3000 (internal) |
caddy | caddy:2-alpine | Reverse proxy + auto SSL | 80, 443 |
Production Compose File
docker-compose.prod.yml
services:
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_DB: cleanmails
POSTGRES_USER: cleanmails
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cleanmails"]
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 512M
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 192M
api:
image: cleanmails-api:latest
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
environment:
- GIN_MODE=release
volumes:
- uploads_data:/app/uploads
- /opt/cleanmails:/opt/cleanmails
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 256M
worker:
image: cleanmails-worker:latest
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
deploy:
resources:
limits:
memory: 256M
frontend:
image: cleanmails-frontend:latest
restart: always
depends_on:
api:
condition: service_healthy
environment:
- HOSTNAME=0.0.0.0
- PORT=3000
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
deploy:
resources:
limits:
memory: 256M
caddy:
image: caddy:2-alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
frontend:
condition: service_healthy
api:
condition: service_healthy
deploy:
resources:
limits:
memory: 64M
volumes:
pgdata:
redis_data:
caddy_data:
caddy_config:
uploads_data:Caddyfile
Caddy routes API requests to the Go backend and all other traffic to the Next.js frontend:
Caddyfile
yourdomain.com {
handle /api/* {
reverse_proxy api:8080
}
handle /health {
reverse_proxy api:8080
}
handle /t/* {
reverse_proxy api:8080
}
handle /unsubscribe/* {
reverse_proxy api:8080
}
handle {
reverse_proxy frontend:3000
}
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Strict-Transport-Security "max-age=31536000"
}
}Caddy auto-provisions Let's Encrypt certificates when a domain is configured.
Volumes
| Volume | Purpose |
|---|---|
pgdata | PostgreSQL database files |
redis_data | Redis AOF persistence |
uploads_data | Uploaded files (logos, CSVs) |
caddy_data | SSL certificates |
caddy_config | Caddy configuration state |
Common Commands
bash
# Start all services
cd /opt/cleanmails
docker compose -f docker-compose.prod.yml up -d
# View logs
docker compose -f docker-compose.prod.yml logs -f
# View logs for a specific service
docker compose -f docker-compose.prod.yml logs -f api
# Stop all services
docker compose -f docker-compose.prod.yml down
# Restart a single service
docker compose -f docker-compose.prod.yml restart api
# Check status
docker compose -f docker-compose.prod.yml ps
# Rebuild after update
docker compose -f docker-compose.prod.yml up -d --force-recreateDocker socket mount
The API container mounts /var/run/docker.sock and /opt/cleanmails. This is required for the self-update feature and system management capabilities.
Health Checks
All services include Docker health checks. The startup order is enforced via depends_on with condition: service_healthy:
- PostgreSQL + Redis start first and must pass health checks
- API + Worker start after database and cache are healthy
- Frontend starts after the API is healthy
- Caddy starts after both frontend and API are healthy
Development Compose
For local development, a simpler docker-compose.yml runs only PostgreSQL and Redis:
docker-compose.yml (dev)
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: cleanmails
POSTGRES_USER: cleanmails
POSTGRES_PASSWORD: cleanmails_secret
ports:
- "5432:5432"
redis:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"Run the API and worker directly with go run during development.