9.1 KiB
Docker Compose Setup
Created: 2026-03-24
Purpose: Orchestrate Recipe Manager backend and frontend containers
Overview
The docker-compose.yml file orchestrates two services:
- Backend: Node.js API server (Express + SQLite)
- Frontend: React SPA served by nginx with API proxy
Architecture
┌─────────────────────────────────────────────┐
│ Host Machine (http://localhost:8080) │
└────────────────┬────────────────────────────┘
│
┌────────────────┴────────────────────────────┐
│ Frontend Container (nginx:alpine) │
│ - Serves React SPA on port 80 │
│ - Proxies /api/* to backend service │
│ - Health check: /health endpoint │
└────────────────┬────────────────────────────┘
│ Docker network (recipe-network)
┌────────────────┴────────────────────────────┐
│ Backend Container (node:22-alpine) │
│ - Express API on port 3000 │
│ - SQLite database in /app/data volume │
│ - Runs migrations on startup │
│ - Health check: GET /api/recipes │
└────────────────┬────────────────────────────┘
│
┌────────────────┴────────────────────────────┐
│ Named Volume: recipe-data │
│ - Persists SQLite database │
│ - Survives container restarts │
└─────────────────────────────────────────────┘
Services
Backend Service
Build context: Root directory
Dockerfile: ./Dockerfile
Exposed port: 3000 (optional, can be removed)
Internal port: 3000
Volume mount: recipe-data:/app/data
Environment variables:
NODE_ENV=productionDATABASE_PATH=/app/data/recipes.db
Health check: wget http://localhost:3000/api/recipes
Restart policy: unless-stopped
Frontend Service
Build context: ./frontend
Dockerfile: ./frontend/Dockerfile
Exposed port: 8080 → 80
Internal port: 80
Dependencies: Waits for backend health check to pass
Health check: wget http://localhost/health
Restart policy: unless-stopped
Nginx configuration:
- Serves static files from
/usr/share/nginx/html - Proxies
/api/*requests tohttp://backend:3000 - SPA fallback routing (all routes →
index.html) - Gzip compression for text files
- Security headers (X-Frame-Options, etc.)
- Aggressive caching for static assets (1 year)
Usage
Build and Start
# Build both services
docker compose build
# Start services in detached mode
docker compose up -d
# View logs
docker compose logs -f
# View logs for specific service
docker compose logs -f backend
docker compose logs -f frontend
Access the Application
- Frontend: http://localhost:8080
- Backend API (direct): http://localhost:3000/api
- Health check: http://localhost:8080/health
Stop and Remove
# Stop services
docker compose stop
# Stop and remove containers
docker compose down
# Stop, remove containers, and delete volumes (⚠️ deletes database!)
docker compose down -v
Rebuild After Changes
# Rebuild specific service
docker compose build backend
docker compose build frontend
# Rebuild and restart
docker compose up -d --build
# Force complete rebuild (no cache)
docker compose build --no-cache
Networking
Internal Communication
Services communicate via the recipe-network bridge network:
- Frontend → Backend:
http://backend:3000/api/* - Docker DNS resolves service names automatically
External Access
- Frontend exposed on host port 8080
- Backend exposed on host port 3000 (optional, can be removed for security)
API Routing
From browser:
User → http://localhost:8080/api/recipes
↓
nginx (frontend container) → proxy_pass to http://backend:3000/api/recipes
↓
Express API (backend container)
Local development (npm run dev):
User → http://localhost:5173/api/recipes
↓
Vite dev server → proxy to http://localhost:3000/api/recipes
↓
Express API (running separately)
Volume Management
Recipe Data Volume
Name: recipe-data
Mount point: /app/data in backend container
Contents: recipes.db (SQLite database)
Inspect volume:
docker volume inspect recipe-manager_recipe-data
Backup database:
# Copy database from container to host
docker compose cp backend:/app/data/recipes.db ./backup-recipes.db
# Or access volume directly
docker run --rm -v recipe-manager_recipe-data:/data -v $(pwd):/backup alpine cp /data/recipes.db /backup/recipes.db
Restore database:
# Copy database from host to container
docker compose cp ./backup-recipes.db backend:/app/data/recipes.db
# Restart backend to apply changes
docker compose restart backend
Environment Configuration
Development vs Production
The current docker-compose.yml is configured for local testing. For production deployment:
- Remove backend port exposure (line 12-13) — only frontend should be exposed
- Add Caddy reverse proxy for HTTPS (see ARCHITECTURE.md)
- Configure environment-specific variables via
.envfile - Set up Litestream for database backups
Environment Variables
Create a .env file for customization:
# Backend
NODE_ENV=production
DATABASE_PATH=/app/data/recipes.db
# Frontend (if needed)
COMPOSE_PROJECT_NAME=recipe-manager
# Ports
FRONTEND_PORT=8080
BACKEND_PORT=3000
Update docker-compose.yml to use variables:
ports:
- "${FRONTEND_PORT:-8080}:80"
Troubleshooting
Backend won't start
Check logs:
docker compose logs backend
Common issues:
- Migration fails → Check schema.sql syntax
- Port 3000 already in use → Change
BACKEND_PORTin .env - Database permission denied → Check volume permissions
Solution:
# Remove containers and volumes, rebuild
docker compose down -v
docker compose up -d --build
Frontend can't reach backend
Check network:
docker compose exec frontend ping backend
Check nginx config:
docker compose exec frontend cat /etc/nginx/conf.d/default.conf
Check backend health:
docker compose exec backend wget -O- http://localhost:3000/api/recipes
API returns 502 Bad Gateway
Cause: Nginx can't reach backend service
Check:
- Backend container is running:
docker compose ps - Backend health check passing:
docker compose ps(should show "healthy") - Network connectivity:
docker compose exec frontend ping backend
Fix:
docker compose restart backend
docker compose restart frontend
Database not persisting
Cause: Volume not mounted correctly
Check:
docker volume ls | grep recipe
docker compose exec backend ls -la /app/data
Fix: Ensure volumes: section in docker-compose.yml matches Dockerfile paths
Testing the Setup
Validation Checklist
# 1. Validate docker-compose.yml syntax
docker compose config
# 2. Build services (should succeed without errors)
docker compose build
# 3. Start services
docker compose up -d
# 4. Check service status (both should be "healthy")
docker compose ps
# 5. Test backend API directly
curl http://localhost:3000/api/recipes
# 6. Test frontend
curl http://localhost:8080
# 7. Test API through frontend proxy
curl http://localhost:8080/api/recipes
# 8. Test in browser
open http://localhost:8080
Expected Results
✅ Backend container running and healthy
✅ Frontend container running and healthy
✅ Database created at /app/data/recipes.db
✅ Migrations applied (recipes, tags, recipe_tags tables exist)
✅ Frontend accessible at http://localhost:8080
✅ API returns empty recipes array: {"success":true,"data":[]}
✅ Frontend UI loads without console errors
Next Steps
After verifying this setup works:
- Add Caddy reverse proxy for HTTPS (production)
- Configure Litestream for database backups
- Set up CI/CD for automated builds
- Document deployment to paje.ca
See ROADMAP.md for post-MVP features.
Related Documentation
docker-backend.md— Backend Dockerfile detailsdocker-frontend.md— Frontend Dockerfile and nginx configARCHITECTURE.md— Overall system architecturedocs/deployment.md— Production deployment guide (TBD)