368 lines
9.1 KiB
Markdown
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)
|