Table of Contents
Overview of the Example
In this chapter you will see a complete but minimal example of using Docker Compose to run a web application and a database together. The focus is on how the docker-compose.yml ties the services, networks, and volumes into a working setup, not on writing application code or advanced configuration.
The scenario is a simple web app that connects to a relational database, such as a small API or a demo blog. Both the web app and the database run as separate services, but you manage them as a single application with Docker Compose.
Project Structure
To make the example concrete, imagine a small project folder on your machine. Inside it you have your web application source code and a docker-compose.yml file in the project root. The application itself will be packaged in a custom image, while the database will usually use an official image from a public registry.
The typical minimal layout looks like this, conceptually described in words. You have a docker-compose.yml file at the top level. You have an app directory that holds the web app source code and often a Dockerfile that builds the app image. When Compose runs, it will build the app image from the Dockerfile, start a web service from that image, and create a database service using an official database image.
The important point is that docker-compose.yml is the central file that describes how all these pieces work together. The example in this chapter uses that description to define two services, a shared network between them, and a volume for database data.
Defining the Web and Database Services
In a typical web plus database example, the Compose file defines at least two services. One service is named something like web and the other something like db. Each service has its own image configuration, environment variables, and optional ports.
Below is a minimal but realistic docker-compose.yml that you can adapt. The example assumes a web application that uses a PostgreSQL database.
version: "3.9"
services:
web:
build: ./app
container_name: example_web
ports:
- "8000:8000"
environment:
- DATABASE_HOST=db
- DATABASE_PORT=5432
- DATABASE_USER=example_user
- DATABASE_PASSWORD=example_password
- DATABASE_NAME=example_db
depends_on:
- db
db:
image: postgres:16
container_name: example_db
environment:
- POSTGRES_USER=example_user
- POSTGRES_PASSWORD=example_password
- POSTGRES_DB=example_db
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
In this file, the web service is built from a local Dockerfile inside the app directory. It exposes port 8000 from the container to port 8000 on your machine. The db service uses the official postgres:16 image and stores its data in a named volume called db_data. The depends_on key ensures that the database container is started before the web container.
The most important point is the connection between the web service and the db service. The web service uses environment variables to describe where the database lives and how to access it. The value DATABASE_HOST=db tells the application that the database host is reachable at the Compose service name db. Inside the Compose project, the service name behaves like a hostname, so the web container can connect directly to the database using that name.
Inside a Compose project, containers can reach each other by their service name, for example db for the database service. Avoid using localhost inside containers to reach other containers, use the service name instead.
How the Web App Talks to the Database
This example relies on Docker networking that Compose sets up automatically for the project. When you run docker compose up in the project directory, Compose creates a private network for the project and attaches both services to that network.
On this network, the db service is reachable at the hostname db, and the web service is reachable at the hostname web. Your application code inside the web container should use db as the host in its database connection string. A typical connection string for PostgreSQL inside the web container might look like this:
postgresql://example_user:example_password@db:5432/example_db
The environment variables defined in the web service can be used by the application code to build this connection string at runtime. The application library that you use for database access will read these values and connect to the correct host and port.
The fact that the services share a network and that service names map to hostnames removes the need to know IP addresses. This is one of the most significant conveniences when using Docker Compose for multi service applications.
Persisting Database Data with Volumes
The example also shows how to preserve database data when containers are stopped or recreated. The db service uses a named volume to store data files. In the Compose file, you can see the volumes key under the db service and the top level volumes section that defines db_data.
When the database container writes to /var/lib/postgresql/data inside the container, Docker stores those files in the named volume db_data on the host. When you stop and remove containers with docker compose down, the volume remains unless you explicitly remove it. This means that the next time you start the stack, your database tables and rows are still available.
If you want to start from an empty database, you can remove the volume explicitly. One common way is to run docker volume rm followed by the volume name that Compose created for db_data. The exact volume name can include the project name, which is usually derived from the folder name.
The important idea is that the database data persists across container lifecycles. This is essential in any realistic web application where you expect user data, configuration, or content to survive restarts.
Running the Stack with Docker Compose
Once you have the docker-compose.yml file in place and the application definition ready inside the app directory, you can start the entire stack with one command from the project root. You use:
docker compose up
This command will build the web image, pull the database image if it is not already present, create a network and a volume, and then start both containers. Your terminal will show output from both services, interleaved by timestamp and service name.
If you prefer to run the stack in the background, you can use detached mode and run:
docker compose up -d
With the example configuration, you can then open a web browser and go to http://localhost:8000. The request will reach the web container because the Compose ports mapping passes traffic from the host port 8000 to the container port 8000. The web app will then connect to the database at db:5432 using the environment variables defined in the Compose file.
When you want to stop the stack, you can use:
docker compose down
This command stops and removes the containers and the default network. The named volume db_data is preserved by default.
Initializing the Database
Many real world applications require an initial database schema or seed data. With Docker Compose, there are several common strategies to initialize the database in this web plus database pattern.
One approach is for the web application to apply migrations on startup. In that case, when the web container starts, it connects to the database and runs any pending migrations or schema updates. This is convenient in development environments because it keeps database initialization in the application code.
Another approach is to use database image features. For example, the official PostgreSQL and MySQL images can run initialization scripts placed in specific directories inside the container. You can mount those scripts with volumes or include them in your own customized database image. Compose is then responsible only for providing the volume or using the custom image.
A third option uses a one time helper service in the Compose file, such as a migrate service that runs a migration command once and then exits. It can depend on the db service and use the same network and credentials as the web application. You would run this helper service manually or as part of a script.
In all these approaches, the common theme is that Docker Compose defines the environment where the initialization happens, but it does not define the migration logic itself. That logic still belongs in your application tools or database scripts.
Environment Configuration between Web and Database
The example uses environment variables to configure the connection between the web app and the database. In the db service, the variables POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB instruct the official PostgreSQL image to create an initial user and database with the given credentials. At the same time, the web service defines variables such as DATABASE_USER and DATABASE_PASSWORD to tell the application how to authenticate.
In simple examples, you can define these values directly inside the Compose file. For more realistic setups, you would often move sensitive values out of version control. You might use an environment file that Compose reads, or some other configuration mechanism. The basic pattern, however, remains the same. The web service and database service share compatible credentials, and Compose brings them together.
It is useful to keep the mapping between these variables clear and consistent. The user and database names defined for the database image must match the values your application uses when it connects. A mismatch leads to authentication failures or missing database errors.
Adapting the Example to Other Stacks
Although this chapter focused on a web app with PostgreSQL, the pattern is almost identical for many other combinations. For example, a Node.js application with MongoDB, a Python application with MySQL, or a PHP application with MariaDB can all use the same Compose structure.
You would change the database image to a different official image, adjust the exposed ports, and adapt the environment variables that the database image expects. You would also update the connection string or configuration in your web application to match that database. The services, network communication through service names, and use of a named volume for data persistence remain almost unchanged.
This portability is one of the reasons Docker Compose is widely used for local development and simple deployments. Once you understand the pattern with this web and database example, you can transfer the concept to many different technology stacks with only minor adjustments.