Local Development with Docker¶
This guide covers running Coalition Builder locally using Docker and Docker Compose for development purposes.
Note: For production deployment, Coalition Builder uses a serverless architecture on AWS Lambda and Vercel. See AWS Serverless Guide and Lambda Deployment for production deployment.
Overview¶
Coalition Builder uses Docker Compose for local development to provide a consistent environment across different machines. The local setup includes all necessary services for development and testing.
Docker Architecture¶
Service Components¶
docker-compose.yml (local development)
├── api (Django Backend)
├── app (Next.js Frontend)
├── db (PostgreSQL + PostGIS)
└── nginx (reverse proxy - optional)
Note: Rate limiting and caching now use PostgreSQL's database cache backend for dev/prod parity.
Quick Start¶
Prerequisites¶
- Docker Engine 20.0+
- Docker Compose 2.0+
- 4GB+ available RAM
- 10GB+ available disk space
Local Development Setup¶
# Clone repository
git clone https://github.com/lhadjchikh/coalition-builder.git
cd coalition-builder
# Copy environment files (optional)
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
Services will be available at:
- Frontend (app): http://localhost:3000
- Backend API: http://localhost:8000
- Nginx Proxy: http://localhost:80 (optional)
- Database: localhost:5432
Docker Compose Configuration¶
Current Compose Configuration¶
The local development setup uses the following services:
services:
db:
image: postgis/postgis:16-3.4
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=${DB_NAME:-coalition}
volumes:
- postgres_data:/var/lib/postgresql/data/
- ./backend/scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DEBUG=${DEBUG:-False}
- SECRET_KEY=${SECRET_KEY:-dev_secret_key_replace_in_production}
- DATABASE_URL=postgis://${APP_DB_USERNAME:-coalition_app}:${APP_DB_PASSWORD:-app_password}@db:5432/${DB_NAME:-coalition}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1,api,nginx,app}
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
app:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
- NODE_ENV=production
- API_URL=http://api:8000
- NEXT_PUBLIC_API_URL=http://localhost:8000
- PORT=3000
ports:
- "3000:3000"
depends_on:
- api
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 10s
timeout: 5s
retries: 3
volumes:
postgres_data:
Environment Configuration¶
The compose file uses environment variables for configuration. Create .env files in the backend/ and frontend/ directories based on the .env.example templates:
# Copy environment templates
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# Edit the files to customize your local setup
Key environment variables:
DEBUG=Truefor development modeSECRET_KEYfor Django security (auto-generated for local development)DATABASE_URLautomatically configured for the local database
Individual Service Containers¶
Backend (Django) Container¶
# backend/Dockerfile
FROM python:3.13-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VENV_IN_PROJECT=1
ENV POETRY_CACHE_DIR=/opt/poetry
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
gdal-bin \
libgdal-dev \
libgeos-dev \
libproj-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Poetry
RUN pip install poetry
# Create and set working directory
WORKDIR /app
# Copy dependency files
COPY pyproject.toml poetry.lock ./
# Install dependencies
RUN poetry install --no-dev
# Copy application code
COPY . .
# Create static files directory
RUN mkdir -p /app/static
# Collect static files
RUN poetry run python manage.py collectstatic --noinput
# Create healthcheck script
COPY healthcheck.py /app/
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python healthcheck.py
# Run application
CMD ["poetry", "run", "gunicorn", "--bind", "0.0.0.0:8000", "coalition.wsgi:application"]
Frontend (Next.js) Container¶
# frontend/Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
# Dependencies stage
FROM base AS deps
RUN npm ci --only=production
# Builder stage
FROM base AS builder
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Runner stage
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Create healthcheck script
COPY healthcheck.js /app/
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node healthcheck.js
CMD ["node", "server.js"]
Database Container¶
Uses the official PostGIS image with initialization script:
#!/bin/bash
# backend/scripts/init-db.sh
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder CASCADE;
EOSQL
echo "PostGIS extensions installed successfully."
Health Checks¶
Backend Health Check¶
# backend/healthcheck.py
#!/usr/bin/env python
import os
import sys
import requests
def check_health():
try:
response = requests.get('http://localhost:8000/api/health/', timeout=5)
if response.status_code == 200:
print("Backend health check passed")
sys.exit(0)
else:
print(f"Backend health check failed: HTTP {response.status_code}")
sys.exit(1)
except Exception as e:
print(f"Backend health check error: {e}")
sys.exit(1)
if __name__ == "__main__":
check_health()
Frontend Health Check¶
// frontend/healthcheck.js
const http = require("http");
const options = {
hostname: "localhost",
port: 3000,
path: "/health/",
method: "GET",
timeout: 5000,
};
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
console.log("Frontend health check passed");
process.exit(0);
} else {
console.log(`Frontend health check failed: HTTP ${res.statusCode}`);
process.exit(1);
}
});
req.on("error", (err) => {
console.log(`Frontend health check error: ${err.message}`);
process.exit(1);
});
req.on("timeout", () => {
console.log("Frontend health check timeout");
req.destroy();
process.exit(1);
});
req.end();
Environment Configuration¶
Development Environment¶
# .env.development
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/coalition
DEBUG=True
SECRET_KEY=development-secret-key-change-in-production
ALLOWED_HOSTS=localhost,127.0.0.1,backend
ORGANIZATION_NAME=Coalition Builder Development
ORG_TAGLINE=Building advocacy partnerships
CONTACT_EMAIL=dev@coalitionbuilder.org
# Frontend
API_URL=http://api:8000
NEXT_PUBLIC_API_URL=http://localhost:8000
PORT=3000
NODE_ENV=development
Production Environment¶
# .env.production
DATABASE_URL=postgresql://user:password@db-host:5432/coalition
DEBUG=False
SECRET_KEY=your-secure-secret-key
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
ORGANIZATION_NAME=Your Organization
ORG_TAGLINE=Your mission statement
CONTACT_EMAIL=info@yourdomain.com
# Frontend Production
API_URL=http://api:8000
NEXT_PUBLIC_API_URL=https://yourdomain.com
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
Nginx Configuration¶
Development Nginx¶
# nginx.dev.conf
events {
worker_connections 1024;
}
http {
upstream api {
server api:8000;
}
upstream frontend {
server frontend:3000;
}
server {
listen 80;
server_name localhost;
# API requests
location /api/ {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Admin interface
location /admin/ {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Static files
location /static/ {
alias /var/www/static/;
}
# Frontend application
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Production Nginx¶
# nginx.prod.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=admin:10m rate=5r/s;
# Gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
upstream api {
server api:8000;
}
upstream frontend {
server frontend:3000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# API requests with rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Admin interface with stricter rate limiting
location /admin/ {
limit_req zone=admin burst=10 nodelay;
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static files with caching
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Frontend application
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Operations¶
Starting Services¶
# Development
docker compose up -d
# Production
docker compose -f docker compose.yml -f docker compose.prod.yml up -d
# Specific service
docker compose up -d backend
# With build
docker compose up --build -d
Monitoring¶
# View logs
docker compose logs -f [service]
# Service status
docker compose ps
# Resource usage
docker stats
# Health status
docker compose exec api python healthcheck.py
docker compose exec frontend node healthcheck.js
Database Operations¶
# Database shell
docker compose exec db psql -U postgres -d coalition
# Run migrations
docker compose exec backend poetry run python manage.py migrate
# Create superuser
docker compose exec backend poetry run python manage.py createsuperuser
# Load test data
docker compose exec backend poetry run python scripts/create_test_data.py
# Database backup
docker compose exec db pg_dump -U postgres coalition > backup.sql
# Database restore
docker compose exec -T db psql -U postgres coalition < backup.sql
Scaling Services¶
# Scale frontend service
docker compose up -d --scale frontend=3
# Scale API service
docker compose up -d --scale api=2
Troubleshooting¶
Common Issues¶
Services not starting:
# Check logs for errors
docker compose logs backend
# Rebuild containers
docker compose build --no-cache
# Reset volumes
docker compose down -v
docker compose up -d
Database connection issues:
# Check database health
docker compose exec db pg_isready -U postgres
# Verify network connectivity
docker compose exec backend ping db
# Check environment variables
docker compose exec backend env | grep DATABASE
Permission issues:
# Fix file permissions
sudo chown -R $USER:$USER .
# Reset Docker permissions
docker compose down
sudo rm -rf volumes/
docker compose up -d
Performance issues:
# Monitor resource usage
docker stats
# Check container health
docker compose ps
# View detailed container info
docker inspect <container_id>
For more detailed troubleshooting, see the main deployment troubleshooting guide.