Welcome to the final lesson in our module on Docker Compose!
In the previous lessons, you've learned to build a docker-compose.yml file piece by piece. You've defined services with their own Dockerfiles, managed persistent data with volumes, handled configuration with environment variables, controlled startup order with depends_on, and even scaled services to handle more load.
Now, we're going to put all of those skills together. This lesson is the capstone for this module, where we focus on the day-to-day reality of working with multi-container applications. Our goal is to launch, manage, and troubleshoot a full multi-tier application using Docker Compose. You'll learn the essential commands for controlling the application's lifecycle and, crucially, how to diagnose and fix the common problems that arise.
By the end of this lesson, you will be able to confidently operate a complete application stack defined in Docker Compose, a core competency for any DevOps role.
1. Launching a Multi-Tier Application
Real-world applications are rarely a single component. A typical web application has at least three tiers: a frontend (what the user sees), a backend or API (the business logic), and a database (for storing data). Docker Compose is designed to manage exactly this kind of setup.
To see how this works in practice, let's watch a video that walks through creating a full three-tier application from scratch. This will tie together many of the concepts we've already discussed.
What is Docker Compose? Multiple Containers & Docker Networking
The video 'What is Docker Compose? Multiple Containers & Docker Networking' by Monis Yousuf provides an excellent, clear walkthrough of containerizing a full-stack application (frontend, backend, database).
Watch the entire video (approximately 14 minutes). Pay close attention to how: Separate Dockerfiles are created for each service. The docker-compose.yml file is built up service by service. Services communicate with each other using their service names (e.g., the backend connecting to the database). depends_on and healthcheck are used to ensure services start in the correct order and are ready.
As you saw in the video, launching the entire application stack is as simple as running a single command from the directory containing your docker-compose.yml file:
# --build tells Compose to build any images from Dockerfiles before starting
docker compose up --build
2. Managing the Application Lifecycle
Once your application is running, you need a way to manage it. Docker Compose provides a suite of commands for this purpose. They are your primary tools for interacting with your application stack.
This guide from Cubepath is an excellent reference for these commands. We will focus on the most critical ones for day-to-day management.
Docker Compose: Complete Guide with Practical Examples
The article 'Docker Compose: Complete Guide with Practical Examples' offers a fantastic command reference. We'll use it to explore the key lifecycle management commands.
Please read the section titled 'Docker Compose Commands'. Focus on the subsections for 'Starting Services', 'Stopping Services', 'Viewing Status', and 'Executing Commands'. This will serve as a great reference sheet.
Here is a summary of the most essential commands you'll use constantly:
Viewing Status
docker compose ps: Lists all the running containers for your application, their status, and port mappings. It's your first stop to see "what's running?"docker compose logs [service-name]: Shows the log output from your services. This is the most important command for debugging.- Use
docker compose logs -fto "follow" the logs in real-time. - Specify a service name (
docker compose logs api) to see logs from just that service.
- Use
Starting and Stopping
docker compose up: Starts (and creates, if needed) all services. Use the-dflag (up -d) to run in detached (background) mode.docker compose stop: Stops running containers without removing them. Their state is preserved.docker compose start: Restarts containers that were previously stopped.docker compose down: The "cleanup" command. It stops and removes all containers, networks, and default volumes created byup.- Use
docker compose down -vto also remove named volumes (use with caution, as this deletes data!).
- Use
Interacting with Services
docker compose exec <service-name> <command>: Executes a command inside a running container. This is invaluable for debugging. For example,docker compose exec backend shwill give you a shell inside your backend container, allowing you to look around the filesystem, test network connections, etc.
These commands form the core of your interaction with any Docker Compose-based application.
3. Troubleshooting Common Issues
Things will inevitably go wrong. A service might not start, or two services might fail to communicate. A systematic approach to troubleshooting is a critical skill for a DevOps engineer.
The article "How to Debug Docker Compose Service Dependencies" provides a fantastic, in-depth guide to solving these exact problems.
How to Debug Docker Compose Service Dependencies
This guide from OneUptime is a masterclass in debugging Compose applications. It covers the most frequent failure modes and how to solve them.
Read the sections 'Diagnosing Dependency Issues', 'Common Issues and Solutions', and 'Debugging Techniques'. Focus on the symptoms described and the step-by-step solutions provided for each.
Let's synthesize the key troubleshooting workflows from that guide.
Workflow 1: A Service Fails to Start or Keeps Crashing
- Symptom:
docker compose psshows a service asExited,Restarting, orunhealthy. - Your First Step: Check the logs.
The error message at the end of the log output will almost always tell you what's wrong (e.g., "database connection refused," "port is already allocated," "file not found").docker compose logs <failed-service-name>
Workflow 2: Services Can't Communicate (Network Error)
- Symptom: The application log shows errors like "Host not found," "Connection refused," or "Connection timed out" when one service tries to reach another.
- Your First Step: Get a shell inside the source container and test connectivity manually.
- Get a shell:
docker compose exec <source-service-name> sh - From inside the container, try to reach the destination service using its service name and port. Use a tool like
ping,curl, ornc(netcat).# Can the service name be resolved to an IP? ping <destination-service-name> # Is the port open and listening? nc -zv <destination-service-name> <port>
- If
pingfails, there's a DNS or network-level issue. - If
pingworks butncfails, the destination container is reachable, but the application inside isn't listening on that port, or a firewall is blocking it.
- Get a shell:
Workflow 3: Application Fails on Startup (Race Condition)
-
Symptom: The application stack fails to start correctly the first time but works if you run
docker compose restart. This usually means a dependent service (like a web app) started before its dependency (like a database) was fully initialized and ready to accept connections. -
Your First Step: Implement robust dependency checking.
- Add a
healthcheckto the dependency service (e.g., the database). This defines a command that Docker can run to see if the service is truly healthy. - Update the
depends_onblock in the dependent service to use theservice_healthycondition.
Example:
services: app: build: . depends_on: database: condition: service_healthy # Wait for the healthcheck to pass database: image: postgres:15 healthcheck: # This command checks if the Postgres server is ready test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 - Add a
Test your understanding!
You have a multi-tier application with a web service and a database service. When you run docker compose up, the web service immediately exits. docker compose logs web shows the error "FATAL: could not connect to database: connection refused". docker compose ps shows the database container is running.
What is the most likely cause of this issue, and what is your first step to fixing it in the docker-compose.yml file?
Show answer
Cause: This is a classic race condition. The database container is running, but the PostgreSQL process inside it hasn't finished initializing and isn't ready to accept connections when the web service tries to connect.
Solution: The first step is to add a healthcheck to the database service and change the web service's depends_on to wait for the service_healthy condition. This ensures the web application doesn't start until the database is truly ready.
Conclusion
You have now reached the end of the Docker Compose module. You've gone from defining a single service to orchestrating, managing, and troubleshooting a complete, realistic application stack.
Key Takeaways:
- A multi-tier application's services, networks, and volumes are defined in a single
docker-compose.ymlfile. - The essential lifecycle commands are
up,down,ps,logs, andexec. Mastering these is key to your daily workflow. - Troubleshooting is a systematic process: always start with
docker compose logs, then test connectivity withdocker compose exec, and finally, fix startup order issues withhealthcheckanddepends_on.
Next Up
We've solidified how to build and run applications locally in a containerized environment. The next step in your DevOps journey is to manage the source code for these applications effectively in a team setting. In our next module, "Advanced Git and Collaborative Workflows," we will explore powerful Git strategies like rebase, compare different branching workflows like GitFlow, and learn best practices for collaborating on projects using GitHub.