# Docker Compose Setup **Created:** 2026-03-24 **Purpose:** Orchestrate Recipe Manager backend and frontend containers --- ## Overview The `docker-compose.yml` file orchestrates two services: 1. **Backend:** Node.js API server (Express + SQLite) 2. **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=production` - `DATABASE_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 to `http://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 ```bash # 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 ```bash # 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 ```bash # 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:** ```bash docker volume inspect recipe-manager_recipe-data ``` **Backup database:** ```bash # 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:** ```bash # 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: 1. **Remove backend port exposure** (line 12-13) — only frontend should be exposed 2. **Add Caddy reverse proxy** for HTTPS (see ARCHITECTURE.md) 3. **Configure environment-specific variables** via `.env` file 4. **Set up Litestream** for database backups ### Environment Variables Create a `.env` file for customization: ```env # 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: ```yaml ports: - "${FRONTEND_PORT:-8080}:80" ``` --- ## Troubleshooting ### Backend won't start **Check logs:** ```bash docker compose logs backend ``` **Common issues:** - Migration fails → Check schema.sql syntax - Port 3000 already in use → Change `BACKEND_PORT` in .env - Database permission denied → Check volume permissions **Solution:** ```bash # Remove containers and volumes, rebuild docker compose down -v docker compose up -d --build ``` ### Frontend can't reach backend **Check network:** ```bash docker compose exec frontend ping backend ``` **Check nginx config:** ```bash docker compose exec frontend cat /etc/nginx/conf.d/default.conf ``` **Check backend health:** ```bash docker compose exec backend wget -O- http://localhost:3000/api/recipes ``` ### API returns 502 Bad Gateway **Cause:** Nginx can't reach backend service **Check:** 1. Backend container is running: `docker compose ps` 2. Backend health check passing: `docker compose ps` (should show "healthy") 3. Network connectivity: `docker compose exec frontend ping backend` **Fix:** ```bash docker compose restart backend docker compose restart frontend ``` ### Database not persisting **Cause:** Volume not mounted correctly **Check:** ```bash 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 ```bash # 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: 1. **Add Caddy reverse proxy** for HTTPS (production) 2. **Configure Litestream** for database backups 3. **Set up CI/CD** for automated builds 4. **Document deployment** to paje.ca See `ROADMAP.md` for post-MVP features. --- ## Related Documentation - `docker-backend.md` — Backend Dockerfile details - `docker-frontend.md` — Frontend Dockerfile and nginx config - `ARCHITECTURE.md` — Overall system architecture - `docs/deployment.md` — Production deployment guide (TBD)