Jiwon Min Developer

Building a Production-Ready Local Development Environment with Docker Compose

“It works on my machine.” It’s a phrase every developer has likely experienced or said at least once. Subtle differences between the development environment and the production environment where the actual service runs are a major cause of unexpected bugs and deployment failures. This is due to numerous variables like library versions, operating systems, and system configurations. The technology that emerged to solve these problems is Docker.

Docker packages an application and all its dependencies into an isolated space called a ‘container,’ ensuring it runs identically in any environment. This allows developers to easily set up an environment on their local PC that is nearly identical to production and share a consistent development environment with team members. In this post, we will provide a practical, step-by-step guide to building a multi-container development environment using Docker Compose, consisting of the Python Django web framework, a PostgreSQL database, and a Redis cache server.

Building a Production-Ready Local Development Environment with Docker Compose

© AI Generated by Imagen 4.0


Why Should You Use Docker?

Before we dive into the setup, let’s clarify why you should use Docker.

  • Environmental Consistency: Docker bundles everything needed to run an application (code, runtime, system tools, libraries, etc.) into a single container image. This image guarantees an identical environment across development, staging, and production, fundamentally preventing bugs caused by ‘environmental differences’.
  • Isolated Environments: Each container runs in isolation from the host system and other containers. This prevents conflicts that can arise when different projects require different versions of libraries or databases.
  • Rapid Setup and Deployment: You can automate development environment setup using code-based configuration files like Dockerfile and docker-compose.yml. When a new team member joins, they can set up the entire development environment instantly with just a few commands, without a complex installation process.
  • Cloud-Friendly: Modern cloud services like AWS ECS (Elastic Container Service) and EKS (Elastic Kubernetes Service) are all container-based. Using Docker locally is the first step toward a cloud deployment pipeline.

Designing the Project Structure

For efficient management, we’ll start with the following directory structure.

my-project/
├── .env          # Environment variable file
├── docker-compose.yml # Docker Compose configuration file
└── app/
    ├── Dockerfile    # Dockerfile for the Django app
    ├── manage.py
    ├── myproject/
    │   ├── settings.py
    │   └── ...
    └── requirements.txt # Python dependency list

Step 1: Create the Dockerfile for the Django Application

First, let’s create the Dockerfile that defines the container image for running our Django application. This file will be located in the app/ directory.

app/Dockerfile

# 1. Select the base image
FROM python:3.11-slim

# 2. Set Python-related environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# 3. Create and set the working directory
WORKDIR /app

# 4. Copy and install dependencies
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt

# 5. Copy the application code
COPY . .

# 6. Run the Django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

app/requirements.txt

Django>=4.2
psycopg2-binary
redis
gunicorn

The Dockerfile above is based on the Python 3.11 image. It defines the process of installing the necessary packages, copying the application code, and then running the development server.

Step 2: Orchestrate Services with Docker Compose

Now, we will create a docker-compose.yml file in the project’s root directory to define and connect our multiple services (web, database, cache) at once.

docker-compose.yml

version: '3.8'

services:
  # 1. Django web service
  web:
    build:
      context: ./app
      dockerfile: Dockerfile
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app:/app  # Sync local code with container code in real-time
    ports:
      - "8000:8000" # Map host port 8000 to container port 8000
    env_file:
      - ./.env      # Load environment variables from file
    depends_on:
      - db
      - redis

  # 2. PostgreSQL database service
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_DB=${SQL_DATABASE}
      - POSTGRES_USER=${SQL_USER}
      - POSTGRES_PASSWORD=${SQL_PASSWORD}

  # 3. Redis cache service
  redis:
    image: redis:7

volumes:
  postgres_data: # Volume to persist database data

This configuration file defines three services: web, db, and redis.

  • web: This service is built from the app/Dockerfile. It mounts the local app directory to /app inside the container, ensuring that code changes are reflected immediately.
  • db: This service uses the official PostgreSQL image. It uses a volume named postgres_data to persist data even if the container is removed.
  • redis: This service uses the official Redis image.
  • depends_on: This sets a dependency, ensuring the web service starts only after the db and redis services are running.

Step 3: Manage Environment Variables (the .env file)

For sensitive information like database credentials, it is safer and better practice to use environment variables instead of hardcoding them. Create a .env file in the project root.

.env

# PostgreSQL Settings
SQL_DATABASE=mydb
SQL_USER=myuser
SQL_PASSWORD=mypassword

# Django Settings
SECRET_KEY=your-django-secret-key-here
DEBUG=1

These separated environment variables are injected into the web service via the env_file directive in docker-compose.yml. You will need to modify Django’s settings.py to read these values using os.environ.get().

Running and Verifying

All the configurations are complete. Now, run the following commands in your terminal to start the entire development environment.

# Build and run the container images (add the -d flag to run in the background)
docker-compose up --build

# Check the list of running containers
docker-compose ps

# Run Django database migrations
docker-compose exec web python manage.py migrate

Once the docker-compose up command runs successfully, you can open your web browser and navigate to http://localhost:8000 to verify that the Django application is running correctly and connected to the PostgreSQL database and Redis. After modifying your code, the changes will be reflected in the container immediately without needing a separate build step.

As you can see, Docker Compose allows you to easily manage complex multi-service architectures with just a few configuration files and commands. This is a powerful tool not only for personal projects but also for team projects with multiple developers, as it standardizes the development environment and maximizes productivity. Furthermore, a containerized application like this gives you a significant advantage when migrating to a cloud environment like AWS in the future.

References