Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 94

Docker Compose for Multi-Tier Applications

Recommended resources

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.

Docker Compose Multi-Container Application Workflow
This diagram shows the core concept of Docker Compose: a single `docker-compose.yml` file orchestrates multiple services (like a web server, an application, and a database), building and running them as a cohesive unit.

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 -f to "follow" the logs in real-time.
    • Specify a service name (docker compose logs api) to see logs from just that service.

Starting and Stopping

  • docker compose up: Starts (and creates, if needed) all services. Use the -d flag (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 by up.
    • Use docker compose down -v to also remove named volumes (use with caution, as this deletes data!).

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 sh will 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 ps shows a service as Exited, Restarting, or unhealthy.
  • Your First Step: Check the logs.
    docker compose logs <failed-service-name>
    
    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").

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.
    1. Get a shell:
      docker compose exec <source-service-name> sh
      
    2. From inside the container, try to reach the destination service using its service name and port. Use a tool like ping, curl, or nc (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 ping fails, there's a DNS or network-level issue.
    • If ping works but nc fails, the destination container is reachable, but the application inside isn't listening on that port, or a firewall is blocking it.

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.

    1. Add a healthcheck to the dependency service (e.g., the database). This defines a command that Docker can run to see if the service is truly healthy.
    2. Update the depends_on block in the dependent service to use the service_healthy condition.

    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
    
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.yml file.
  • The essential lifecycle commands are up, down, ps, logs, and exec. Mastering these is key to your daily workflow.
  • Troubleshooting is a systematic process: always start with docker compose logs, then test connectivity with docker compose exec, and finally, fix startup order issues with healthcheck and depends_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.