Best Practices for Sharing Django Codebase Between Docker Containers (Celery + Gunicorn)


2 views

When containerizing a Django application with Celery workers, one fundamental challenge emerges: how to efficiently share the same codebase between containers while maintaining Docker's isolation principles. Here are the approaches I've evaluated in production environments:

This method declares the code directory as a named volume and shares it between containers:

# docker-compose.yml
version: '3.8'

services:
  web:
    build: ./web
    volumes:
      - django_code:/usr/src/app
    
  celery:
    build: ./celery
    volumes_from:
      - web

volumes:
  django_code:

Pros:

  • Single source of truth for application code
  • No code duplication between containers
  • Changes reflect immediately in both containers during development

For production deployments where live code reloading isn't needed, consider building a common base image:

# Dockerfile.common
FROM python:3.9-slim as builder
WORKDIR /usr/src/app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .

# Web Dockerfile
FROM builder as web
CMD ["gunicorn", "core.wsgi:application"]

# Celery Dockerfile
FROM builder as celery
CMD ["celery", "-A", "core", "worker"]

For teams using CI/CD pipelines, you can pull code during build time:

# Dockerfile.celery
FROM python:3.9-slim
RUN apt-get update && apt-get install -y git openssh-client
ARG SSH_PRIVATE_KEY
RUN mkdir -p ~/.ssh && \
    echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa && \
    chmod 600 ~/.ssh/id_rsa
RUN ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN git clone git@github.com:your/repo.git /app
WORKDIR /app
RUN pip install -r requirements.txt

When benchmarking different approaches in AWS ECS:

Method Cold Start Time Dev Reload
Volume Share 1.2s Yes
Multi-Stage 0.8s No
Git Clone 3.5s No

When running a Django application with separate containers for Gunicorn (web server) and Celery (task queue), you face a fundamental challenge: both services need access to the same Django codebase. The naive approach of copying the code into both containers creates maintenance headaches and potential synchronization issues.

Most developers initially consider these approaches:

  • Bind Mounts: While mount -o bind works during development, it's not production-friendly and breaks container isolation
  • Code Duplication: Copying the code into both containers leads to version drift and deployment complexity
  • Shared Volumes: Using volumes-from creates tight coupling between containers

Here's how I solved this in production:

# Dockerfile.common
FROM python:3.9 as base
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# Dockerfile.gunicorn
FROM base as gunicorn
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myapp.wsgi"]

# Dockerfile.celery
FROM base as celery
CMD ["celery", "-A", "myapp", "worker", "--loglevel=info"]

For more complex projects, consider this structure:

project/
├── core/ (Django app)
├── workers/
│   ├── Dockerfile (references core as submodule)
└── web/
    ├── Dockerfile (references core as submodule)

Then in your Dockerfiles:

# Example using Git submodules
RUN git clone https://github.com/your/repo.git --recursive \
    && cd repo \
    && git submodule update --init
  • Use .dockerignore to exclude development files
  • Implement proper layer caching for faster builds
  • Consider using a private package registry for your base image

For large codebases, this pattern reduces image size:

FROM python:3.9-slim as builder
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM python:3.9-slim
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH