
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
└── .dockerignoreYour 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/0Keep .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 --buildCheck the running services:
docker compose psView the application and Celery logs:
docker compose logs -f web celeryIf everything starts correctly, open http://localhost:8000 in your browser.
5. Essential Docker Compose Commands
| Command | Purpose |
|---|---|
docker compose up -d --build | Build and start services |
docker compose ps | Check service status |
docker compose logs -f | Follow service logs |
docker compose exec web python manage.py shell | Open the Django shell |
docker compose restart web | Restart the web service |
docker compose down | Stop and remove containers and network |
docker compose build | Rebuild 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
- Part 1: What Is Docker? Why It Exists and How Containers Solved “It Works on My Machine”
- Part 2: Docker Images, Containers and Dockerfiles: Understanding How Docker Works
- Part 3: Docker Compose: Running a Real-World Application with Multiple Containers — You are here.
More posts
Why Your Django Admin Is an Hour Behind and Why Your Database Isn't Wrong
Why Django stores time in UTC, why that's right for production, and how to make the admin and Celery Beat show the time your users actually live in.
Docker Images, Containers and Dockerfiles: Understanding How Docker Works
Understand how Docker works under the hood. Learn about Docker images, containers, Dockerfiles, layers, ports, volumes, networks, and essential Docker commands.

