A multi-container Docker application runs separate parts of an application in separate containers and manages them as one project with Docker Compose. A typical setup may include a frontend, an API, a database, a cache, and background workers.

This tutorial explains how multi-container applications work, how to define services in a Compose file, how containers communicate, how to persist database data, and how to start and troubleshoot the complete application.

Why Docker applications use multiple containers

A container can technically run more than one process, but a multi-container design usually assigns one main responsibility to each service. For example, a web application can place its frontend, backend, and database in separate containers.

  • Frontend service: Serves the user interface or browser application.
  • Backend service: Processes requests, applies application logic, and communicates with other services.
  • Database service: Stores persistent application data.
  • Optional supporting services: Provide caching, queues, scheduled jobs, monitoring, or reverse-proxy functions.

Separating these responsibilities makes it possible to build, replace, restart, and scale services independently. A database restart, for example, does not require the frontend image to be rebuilt.

Docker image layers and multi-container services

A Dockerfile contains instructions used to build an image. Instructions such as FROM, RUN, and COPY contribute to the image’s filesystem layers. Docker can reuse unchanged build layers, which reduces unnecessary work during later builds.

Image layers and application services solve different problems. Layers are part of an image’s build structure, while services are runtime definitions in a Compose application. A multi-container project can build several images, pull existing images, or combine both approaches.

Frontend service in a multi-container Docker application

The frontend is the part users interact with directly. It can be a static HTML, CSS, and JavaScript site, a server-rendered web application, or a browser application created with a framework such as React, Angular, or Vue.

A frontend container may serve compiled files through a web server or run a development server. When it needs application data, it sends requests to the backend API rather than connecting directly to the database.

Backend API service in a multi-container Docker application

The backend contains the application’s server-side logic. It receives requests, validates input, applies business rules, reads or updates stored data, and returns a response to the frontend or another client.

An API is a defined interface through which clients communicate with the backend. In a Compose project, the backend can connect to the database by using the database service name as the hostname. It does not need to know the database container’s changing IP address.

Database service and persistent Docker volumes

A database container runs the database server process, but important database files should not depend on the writable container layer. Containers can be recreated during updates, rebuilds, or configuration changes.

A named Docker volume stores database data separately from the container lifecycle. Recreating the database container can then preserve the data as long as the volume is retained.

Managed database services can also be used instead of a database container. The following existing references describe PostgreSQL services from AWS and Google Cloud:

  • https://aws.amazon.com/rds/postgresql/
  • https://cloud.google.com/sql/docs/postgres

When an externally managed database is used, the backend container connects through the database endpoint supplied by the provider. The database does not need to be declared as a local Compose service.

Docker Compose workflow for a multi-container application

Docker Compose defines a group of related containers in a YAML file and manages them as one application. Current Docker installations use the docker compose command. Older installations may provide the standalone docker-compose command shown in the original examples.

  1. Create a Dockerfile for each service that needs a custom image.
  2. Define the application’s services, networks, volumes, environment values, and port mappings in compose.yaml or docker-compose.yml.
  3. Run docker compose up from the project directory.
  4. Inspect the service status and logs.
  5. Stop and remove the project’s containers with docker compose down.

The following image shows the original Compose file used in this tutorial:

Docker Multi-Container Application Setup

The original accompanying code was placed at Code/Lesson-2/example-docker-compose.yml. The existing code reference is https://goo.gl/11rwXV.

Check Docker Compose before creating the application

Docker Engine and Docker Compose must be installed before you start the project. Run the following commands to verify that the command-line tools are available:

</>
Copy
docker --version
docker compose version

On a system that still uses the legacy standalone Compose tool, the second command may instead be:

</>
Copy
docker-compose --version

Run Docker commands from a user account that has permission to access the Docker daemon. On Linux, the exact permission setup depends on how Docker was installed.

Create a multi-container application with a web service and database

The following example defines a small Python web service and a PostgreSQL database. The web service is built from the current project directory, while the database uses an existing image.

Create the Docker multi-container project files

Create a directory named multi-container-app with this structure:

multi-container-app/
├── app.py
├── Dockerfile
├── requirements.txt
└── compose.yaml

Add the following application code to app.py:

</>
Copy
import os

from flask import Flask, jsonify

app = Flask(__name__)


@app.get("/")
def index():
    return jsonify(
        message="Multi-container application is running",
        database_host=os.getenv("DATABASE_HOST", "not configured"),
    )


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Add the Python dependency to requirements.txt:

</>
Copy
Flask>=3.0,<4.0

Create the following Dockerfile for the web service:

</>
Copy
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

Create compose.yaml with a web service, a database service, and a named volume:

</>
Copy
services:
  web:
    build: .
    ports:
      - "8000:5000"
    environment:
      DATABASE_HOST: db
      DATABASE_NAME: exampledb
      DATABASE_USER: exampleuser
      DATABASE_PASSWORD: examplepassword
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: exampledb
      POSTGRES_USER: exampleuser
      POSTGRES_PASSWORD: examplepassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U exampleuser -d exampledb"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  postgres_data:

The Compose Specification does not require a top-level version field. Older Compose files may contain values such as version: "3", but current Compose implementations use the specification supported by the installed Compose tool.

Start the Docker multi-container application

Open a terminal in the project directory and run:

</>
Copy
docker compose up --build

The --build option rebuilds images before the services are started. Compose also creates the project network and named volume when they do not already exist.

Open http://localhost:8000/ in a browser. The host port 8000 is forwarded to port 5000 inside the web container.

The original tutorial demonstrated the output of two locally built services named js-docker and python-docker:

Docker Multi-Container Application Setup

How Docker Compose service networking works

Compose normally creates a default network for the project. Each service container joins that network and can be reached by its service name.

In the example, the web service uses db as the database hostname because db is the Compose service name. Do not use localhost for this connection. Inside the web container, localhost refers to the web container itself, not the database container.

Container IP addresses can change when containers are recreated. Service-name discovery avoids depending on those addresses.

Docker Compose ports and internal service connections

A Compose port mapping uses the format HOST_PORT:CONTAINER_PORT. The mapping below makes a service listening on container port 5000 available through host port 8000:

</>
Copy
ports:
  - "8000:5000"

Other containers on the Compose network should normally connect to the service’s container port, not its published host port. For example, another service would reach the web service at http://web:5000.

A database port does not need to be published to the host unless a host-side administration tool or another external client must connect to it. The backend can access the database over the private Compose network.

Docker Compose depends_on and service readiness

The depends_on setting defines a dependency between services and controls their startup and shutdown order. Starting a database container does not necessarily mean the database server is immediately ready to accept connections.

A health check can test whether the database is ready. The dependent service can then use condition: service_healthy, as shown in the example. Applications should still handle temporary connection failures because a dependency can restart after the application has already started.

Run WordPress and MySQL as a Docker multi-container application

WordPress provides a practical example of a two-service application. One service runs WordPress and its web server, while another runs MySQL. A named volume stores the database files.

  1. Create a directory named sandbox.
  2. Create a compose.yaml or docker-compose.yml file inside the directory.
  3. Define the WordPress and database services.
  4. Run the project from the same directory.

The following image shows the original WordPress Compose configuration:

Docker Multi-Container Application Setup

The original code was placed at Code/Lesson-2/wordpress-docker-compose.yml. The existing code reference is https://goo.gl/t7UGvy. YAML indentation must be consistent. Use spaces rather than tabs for indentation.

A current Compose example can be written as follows:

</>
Copy
services:
  db:
    image: mysql:8.4
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: change-this-password
      MYSQL_ROOT_PASSWORD: change-this-root-password
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 10

  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: change-this-password
      WORDPRESS_DB_NAME: wordpress

volumes:
  db_data:

For a real deployment, do not commit production passwords to a public repository. Supply sensitive values through an appropriate secrets or environment-management workflow.

Start the WordPress Compose services

Run the following command in the sandbox directory:

</>
Copy
docker compose up -d

The original tutorial showed the foreground Compose startup output in the following screenshot:

Docker Multi-Container Application Setup

List the containers belonging to the current Compose project:

</>
Copy
docker compose ps

You can also run docker ps to list running containers across Docker projects. The original container listing is shown below:

Docker Multi-Container Application Setup

Open http://0.0.0.0:8000/, http://localhost:8000/, or the Docker host’s address on port 8000. The browser should display the WordPress installation screen. Complete the web-based setup to create the site.

Docker Compose file services, networks, and volumes

A Compose file describes the application model. Its primary top-level sections can include services, networks, volumes, configs, and secrets.

Compose sectionPurpose
servicesDefines the application’s containerized components.
networksControls which services can communicate over named networks.
volumesDefines persistent data stores that services can mount.
configsProvides non-sensitive configuration data to services.
secretsDescribes sensitive data made available to services through supported mechanisms.

The original simplified services structure is shown in the following image:

Define a service from an existing Docker image

The image property tells Compose which image to use for a service. Compose uses a local copy when the required image is available and may pull it from the configured registry when needed.

</>
Copy
services:
  db:
    image: postgres:17

Use a specific version tag that matches the application’s compatibility requirements. An explicit tag also makes updates more deliberate than relying on an unpinned moving tag.

Build a Compose service from a Dockerfile

The build property tells Compose to build an image from source. A short value such as build: . uses the current directory as the build context and looks for a file named Dockerfile.

</>
Copy
services:
  web:
    build: .

Use the expanded build form to select another Dockerfile:

</>
Copy
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.web

Persist service data with named volumes

A service mounts a named volume by listing it under the service’s volumes property. The same name is declared in the top-level volumes section.

</>
Copy
services:
  db:
    image: postgres:17
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Use environment variables in Compose services

The environment section passes configuration values into a container. These values can include service hostnames, database names, feature settings, and credentials.

</>
Copy
services:
  web:
    environment:
      DATABASE_HOST: db
      DATABASE_NAME: exampledb

A Compose file can interpolate values from the shell or an environment file. Avoid placing real production secrets directly in version-controlled files.

Docker Compose commands for multi-container applications

CommandPurpose
docker compose configParses and renders the resolved Compose configuration.
docker compose config --servicesLists service names defined by the project.
docker compose buildBuilds images for services that use build.
docker compose upCreates and starts the application services.
docker compose up -dStarts the services in detached mode.
docker compose up --buildBuilds images before starting services.
docker compose psShows containers associated with the Compose project.
docker compose imagesLists images used by the created service containers.
docker compose logsDisplays logs from the project’s services.
docker compose logs -f webFollows logs from the web service.
docker compose exec web shRuns a shell inside a running web service container.
docker compose run --rm web commandRuns a one-off command using the service configuration.
docker compose stopStops containers without removing them.
docker compose downStops and removes the project’s containers and networks.

View logs from one Docker Compose service

Use the service name after the logs command to focus on one component:

</>
Copy
docker compose logs -f db

Press Ctrl+C to stop following the log stream. This does not stop services that were started in detached mode.

Stop a Compose project without deleting database data

Run the following command to remove the project containers and default network:

</>
Copy
docker compose down

Named volumes are retained by the standard down command. Adding --volumes removes the project’s declared named volumes and can delete stored database data:

</>
Copy
docker compose down --volumes

Use the volume-removal option only when the stored data is no longer required or has been backed up.

Run multiple containers from the same Docker image

Multiple containers can run from the same image. Each container receives its own writable layer, process state, and network identity while sharing the image’s read-only layers.

To run more than one container for a stateless Compose service, scale that service:

</>
Copy
docker compose up -d --scale web=3

A service with a fixed host-port mapping cannot normally publish the same host port from several containers. Scaled web containers are commonly placed behind a reverse proxy or load balancer instead of each binding the same host port.

Use separate Compose configurations for development and production

Development and production environments often require different settings. Development may use source-code bind mounts, debugging ports, and automatic reload. Production may use prebuilt images, stricter resource policies, managed secrets, and no source-code mounts.

Compose supports combining multiple files. For example:

</>
Copy
docker compose -f compose.yaml -f compose.dev.yaml up

The later file can override or extend values from the earlier file. Review the resolved configuration before starting the project:

</>
Copy
docker compose -f compose.yaml -f compose.dev.yaml config

Troubleshoot a Docker multi-container application

A Compose service exits immediately

Inspect the service status and logs:

</>
Copy
docker compose ps -a
docker compose logs service-name

Common causes include an invalid command, a missing environment variable, a configuration-file error, an application exception, or a dependency that is not ready.

The backend cannot connect to the database container

  • Use the database service name, such as db, instead of localhost.
  • Use the database’s internal container port.
  • Check that both services are attached to a shared network.
  • Verify the database name, user, and password.
  • Inspect the database health check and startup logs.
  • Confirm that the application retries temporary connection failures.

Docker Compose reports a YAML parsing error

YAML structure depends on indentation. Use spaces consistently, verify that child properties are indented below their parent, and avoid tabs. Run the following command to validate and render the configuration:

</>
Copy
docker compose config

A host port is already allocated

Another process or container is already using the requested host port. Stop the conflicting process or change the host side of the mapping. For example, change 8000:80 to 8080:80, and then open port 8080 in the browser.

Database data disappears after recreating containers

Verify that the database’s data directory is mounted to a named volume and that the volume was not removed with docker compose down --volumes. Also confirm that the mount targets the correct data directory for the selected database image.

Docker multi-container application FAQs

What is a multi-container Docker application?

A multi-container application divides related components into separate containers. Docker Compose can define and manage the services, networks, volumes, environment values, and dependencies as one project.

Can one Docker container run multiple applications?

A container can run multiple processes, but separating unrelated application responsibilities into different containers is generally easier to build, update, scale, observe, and troubleshoot. Some tightly coupled supporting processes may still be packaged together when the design requires it.

How do Docker Compose containers communicate with each other?

Services attached to the same Compose network can communicate by using service names as hostnames. For example, an API service can connect to a database service named db at db:5432 for PostgreSQL.

What is the difference between docker compose and docker-compose?

docker compose is the current Compose command integrated with the Docker CLI. docker-compose is the older standalone command. Existing projects may still show the hyphenated syntax, but current documentation and installations generally use docker compose.

Does docker compose down delete database data?

The standard docker compose down command does not remove named volumes. Running it with --volumes removes the project’s named volumes and can delete the database data stored in them.

Docker multi-container application editorial QA checklist

  • Verify that each Compose service has one clearly described application responsibility.
  • Confirm that inter-container connections use Compose service names rather than fixed container IP addresses.
  • Check that host and container ports are identified correctly in every port mapping.
  • Confirm that database files are mounted to a named volume and that destructive volume-removal commands include a warning.
  • Validate all YAML examples with docker compose config.
  • Confirm that service readiness is distinguished from container startup and that health checks are used where needed.
  • Check that current commands use docker compose while legacy docker-compose references are clearly identified.
  • Verify that example passwords are marked as values that must be changed and are not presented as production credentials.
  • Start the example with docker compose up --build, inspect it with docker compose ps, and stop it with docker compose down.

Docker Compose provides a single application definition for related containers while keeping the frontend, backend, database, and supporting services independently manageable. The Compose file records how the services are built, connected, configured, started, and supplied with persistent storage.