recipe-manager/docs/docker-compose.md

368 lines
9.1 KiB
Markdown

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