recipe-manager/docs/docker-compose.md

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:

  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

# 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

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:

  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:

# 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_PORT in .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:

  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:

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:

  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.


  • 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)