Docker Deployment Guide¶
This guide covers containerized deployment of Coalition Builder using Docker and Docker Compose.
Overview¶
Coalition Builder is designed for containerized deployment with Docker. The application consists of multiple services that can be orchestrated using Docker Compose for development or deployed individually in production environments.
Docker Architecture¶
Service Components¶
docker compose.yml
├── backend (Django API)
├── frontend (React - development only)
├── ssr (Next.js - optional)
├── db (PostgreSQL + PostGIS)
└── nginx (reverse proxy - production)
Quick Start¶
Prerequisites¶
- Docker Engine 20.0+
- Docker Compose 2.0+
- 4GB+ available RAM
- 10GB+ available disk space
Development Environment¶
# Clone repository
git clone https://github.com/lhadjchikh/coalition-builder.git
cd coalition-builder
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
Services will be available at:
- Backend API: http://localhost:8000
- Frontend: http://localhost:3000 (if built)
- SSR: http://localhost:3001 (if enabled)
- Database: localhost:5432
Docker Compose Configuration¶
Main Compose File¶
version: "3.8"
services:
db:
image: postgis/postgis:16-3.4
environment:
POSTGRES_DB: coalition
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/coalition
- DEBUG=True
- ALLOWED_HOSTS=localhost,127.0.0.1,backend
volumes:
- ./backend:/app
- static_volume:/app/static
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "healthcheck.py"]
interval: 30s
timeout: 10s
retries: 3
ssr:
build:
context: ./ssr
dockerfile: Dockerfile
environment:
- API_URL=http://backend:8000
- NEXT_PUBLIC_API_URL=http://localhost:8000/api
- PORT=3000
ports:
- "3001:3000"
depends_on:
- backend
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres_data:
static_volume:
Production Compose Override¶
# docker compose.prod.yml
version: "3.8"
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.prod.conf:/etc/nginx/nginx.conf
- static_volume:/var/www/static
- ./ssl:/etc/nginx/ssl
depends_on:
- backend
- ssr
backend:
environment:
- DEBUG=False
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
volumes: [] # Remove development volume mounts
ssr:
environment:
- NODE_ENV=production
- API_URL=http://backend:8000
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
db:
volumes:
- postgres_data:/var/lib/postgresql/data
# Remove init script volume for production
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"]
SSR (Next.js) Container¶
# ssr/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
# 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/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()
SSR Health Check¶
// ssr/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("SSR health check passed");
process.exit(0);
} else {
console.log(`SSR health check failed: HTTP ${res.statusCode}`);
process.exit(1);
}
});
req.on("error", (err) => {
console.log(`SSR health check error: ${err.message}`);
process.exit(1);
});
req.on("timeout", () => {
console.log("SSR 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/SSR
API_URL=http://backend:8000
NEXT_PUBLIC_API_URL=http://localhost:8000/api
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
# SSR Production
API_URL=http://backend:8000
NEXT_PUBLIC_API_URL=https://yourdomain.com/api
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
Nginx Configuration¶
Development Nginx¶
# nginx.dev.conf
events {
worker_connections 1024;
}
http {
upstream backend {
server backend:8000;
}
upstream ssr {
server ssr:3000;
}
server {
listen 80;
server_name localhost;
# API requests
location /api/ {
proxy_pass http://backend;
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://backend;
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/;
}
# SSR application
location / {
proxy_pass http://ssr;
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 backend {
server backend:8000;
}
upstream ssr {
server ssr: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://backend;
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://backend;
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";
}
# SSR application
location / {
proxy_pass http://ssr;
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 backend python healthcheck.py
docker compose exec ssr 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 SSR service
docker compose up -d --scale ssr=3
# Scale backend service
docker compose up -d --scale backend=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.