Docker Compose: Running a Real-World Application with Multiple Containers
DevOpsOct 19, 20264 min read

Docker Compose: Running a Real-World Application with Multiple Containers

Learn Docker Compose with Django, PostgreSQL, Redis, and Celery. Configure services, networking, volumes, and essential commands.


Docker Compose: Running a Real-World Application with Multiple Containers

In Part 1, we explored why Docker exists and how containers solve the “it works on my machine” problem.

In Part 2, we covered Docker images, containers, and Dockerfiles.

Now, let's bring everything together with Docker Compose by running a Django application alongside PostgreSQL, Redis, and a Celery worker.

What Is Docker Compose?

Docker Compose lets you define and manage multiple containers using a single YAML file.

Instead of starting PostgreSQL, Redis, Django, and Celery separately, you define how they work together in a compose.yaml file.

With one command, you can build and start your application stack.

How the Services Work Together

Our example application has four services:

  • Web: Runs the Django application.
  • Database: Stores application data using PostgreSQL.
  • Redis: Acts as a message broker for Celery.
  • Celery: Processes background tasks.

Docker Compose creates a network so the services can communicate using their service names.

For example, Django connects to PostgreSQL using db as the hostname, not localhost.

1. Create the Project Structure

A simple project structure could look like this:

myproject/
├── app/
├── manage.py
├── requirements.txt
├── Dockerfile
├── compose.yaml
├── .env
└── .dockerignore

Your Dockerfile defines how the Django image is built. The same image can be reused for both the web server and the Celery worker.

2. Configure Environment Variables

Create a .env file:

DJANGO_SECRET_KEY=replace-with-a-secure-secret
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
 
POSTGRES_DB=app_db
POSTGRES_USER=app_user
POSTGRES_PASSWORD=replace-with-a-strong-password
 
REDIS_URL=redis://redis:6379/0

Keep .env out of version control. Add it to .gitignore, and never commit real credentials.

For Django's database settings, use the PostgreSQL service name as the host:

"HOST": "db",
"PORT": "5432",

3. Create the Compose File

Create compose.yaml:

services:
  web:
    build: .
    command: >
      sh -c "python manage.py migrate &&
             gunicorn app.wsgi:application --bind 0.0.0.0:8000"
    ports:
      - "8000:8000"
    env_file:
      - .env
    environment:
      POSTGRES_HOST: db
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
 
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  celery:
    build: .
    command: celery -A app worker --loglevel=info
    env_file:
      - .env
    environment:
      POSTGRES_HOST: db
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
 
volumes:
  postgres_data:

Important: Replace app.wsgi:application and celery -A app with the correct Python module paths for your project.

This configuration uses a named volume to preserve PostgreSQL data across container recreation. Redis is configured as a message broker here; if you need Redis data persistence, configure Redis persistence separately.

The Compose file also waits for PostgreSQL and Redis health checks before starting the dependent services. This helps with startup ordering, although applications should still handle connection failures and retries.

4. Start the Application

From the project directory, run:

docker compose up -d --build

Check the running services:

docker compose ps

View the application and Celery logs:

docker compose logs -f web celery

If everything starts correctly, open http://localhost:8000 in your browser.

5. Essential Docker Compose Commands

CommandPurpose
docker compose up -d --buildBuild and start services
docker compose psCheck service status
docker compose logs -fFollow service logs
docker compose exec web python manage.py shellOpen the Django shell
docker compose restart webRestart the web service
docker compose downStop and remove containers and network
docker compose buildRebuild service images

Be careful with docker compose down -v: it also removes named volumes and can permanently delete your local database data.

Common Mistakes to Avoid

Using localhost between containers: Inside a container, localhost refers to that same container. Use service names such as db and redis.

Starting services without checking readiness: A running database container does not always mean PostgreSQL is ready to accept connections. Health checks help address this.

Committing secrets: Keep real credentials and Django secret keys out of Git.

Running migrations carelessly in production: Running migrations in the web startup command is convenient for a tutorial, but production deployments with multiple web replicas should run migrations as a controlled deployment step.

Is Docker Compose Enough for Production?

Docker Compose is useful for local development, testing, and some single-server deployments. It simplifies service configuration and makes environments more reproducible.

However, larger production environments may require additional infrastructure for orchestration, scaling, monitoring, secrets management, and high availability.

The important lesson is to understand your application's needs before adding complexity.

Conclusion

Docker Compose makes multi-container applications easier to build, run, and maintain. By defining Django, PostgreSQL, Redis, and Celery in one configuration, you can manage your development environment with a few commands.

Docker gives you the building blocks. Docker Compose connects them into a working application stack.

Continue the Docker Fundamentals Series

More posts