Skip to content

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:

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=True for development mode
  • SECRET_KEY for Django security (auto-generated for local development)
  • DATABASE_URL automatically 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.