Skip to content

Your First Compose File


A Compose file doesn’t need to start with a complicated application. In fact, the best way to understand Compose is to begin with one container, see how Compose describes it, and only then add more pieces.

The smallest useful Compose file

Let’s start with a container that prints a message and then exits.

Create a directory for the example:

mkdir compose-first
cd compose-first

Now create a file named compose.yaml:

compose.yaml
services:
  hello:
    image: hello-world

That’s the entire Compose file.

There is no network configuration, no volume, and no environment variable. We are deliberately starting with the smallest useful example.

The structure is simple:

compose.yaml
    │
    └── services
          │
          └── hello
                │
                └── image: hello-world

The services section describes the containers that make up the application.

Here, we have one service named hello.

The service says:

“Create a container for me using the hello-world image.”

Start the application

From the directory containing compose.yaml, run:

docker compose up

Compose reads compose.yaml and creates the resources required by the configuration.

You should see output from the hello-world container, followed by a message indicating that the container has exited.

The important part is not the message itself. What matters is what just happened:

compose.yaml
     │
     ▼
Compose reads configuration
     │
     ▼
Finds the "hello" service
     │
     ▼
Uses the hello-world image
     │
     ▼
Creates and starts the container
     │
     ▼
Container runs and exits

Notice that you didn’t have to manually create a container.

You described the container in the Compose file, and Compose handled the Docker resource for you.

A service is the important building block

The name hello in the file is a service name:

services:
  hello:
    image: hello-world

Think of a service as the definition of one component of your application.

For this example:

service: hello
        │
        ▼
container created from hello-world

Later, a Compose application might contain several services:

services:
  app:
    ...

  database:
    ...

That gives you:

Compose application
       │
       ├── app service
       │      └── app container
       │
       └── database service
              └── database container

For now, though, one service is enough.

compose.yaml is the configuration

You might be tempted to think that docker compose up is the important part.

It isn’t.

The important part is the relationship between the Compose file and the command:

compose.yaml
     │
     │ describes
     ▼
application
     │
     │ managed by
     ▼
docker compose up

The command doesn’t contain the application’s configuration.

The configuration is in the file.

This is one of the biggest differences from manually typing a sequence of Docker commands.

With manual Docker commands, you might have something like:

Docker command → create this container
Docker command → configure this container
Docker command → start this container

With Compose, the configuration is stored separately:

compose.yaml
     │
     ▼
"this is what my application needs"
     │
     ▼
docker compose up
     │
     ▼
Compose creates/starts it

Add a second service

One service doesn’t really demonstrate why Compose becomes useful.

Let’s add another container.

Replace compose.yaml with:

compose.yaml
services:
  hello:
    image: hello-world

  another:
    image: hello-world

Now the application contains two services:

Compose application
       │
       ├── hello
       │    └── hello-world container
       │
       └── another
            └── hello-world container

Run:

docker compose up

Compose now handles both services as part of the same application.

This is the conceptual jump that Compose is designed for.

Instead of thinking:

“I need to start container A, then container B.”

you can think:

“My application has two services.”

Compose takes care of creating and starting the containers described by those services.

Compose creates an application boundary

When you work with individual Docker commands, it is easy to think only in terms of individual containers.

Compose gives those containers a common application context.

For example:

                 Compose project
                       │
              ┌────────┴────────┐
              │                 │
          hello service     another service
              │                 │
          container          container

As the application becomes more realistic, the same idea extends naturally:

                 Compose project
                       │
          ┌────────────┼────────────┐
          │            │            │
        app         database       cache
          │            │            │
       container    container    container

The containers are still normal Docker containers.

Compose simply gives you a way to define and manage the collection as one application.

What happens if you run it again?

Compose is not simply a script that blindly executes every line every time.

Run:

docker compose up

again.

Compose examines the current configuration and the resources associated with the project.

This is an important property of declarative configuration:

You describe the desired application, and Compose works to bring the Docker resources into that state.

This becomes much more useful once your application contains long-running services, networks, volumes, and configuration.

You don’t need to understand the entire lifecycle yet. That is the subject of the next lesson.

For now, remember that docker compose up is not just shorthand for a fixed list of Docker commands.

Stop the application

When you’re finished, you can stop the Compose application with:

docker compose down

For this simple example, there is not much to clean up, but the command becomes important as soon as the Compose application contains multiple resources.

Conceptually:

docker compose up
       │
       ▼
Create/start application
       │
       ▼
     running
       │
       ▼
docker compose down
       │
       ▼
Remove the application's
managed resources

You’ll explore exactly what up and down do in the lifecycle lesson.

A slightly more realistic example

Let’s make the example a little more interesting without introducing networks, volumes, or other concepts yet.

Create this compose.yaml:

compose.yaml
services:
  first:
    image: alpine

  second:
    image: alpine

Run:

docker compose up

The containers will start and then exit because alpine by itself doesn’t have a long-running process to keep the containers alive.

This is actually useful for understanding something important:

Compose manages services; it does not change how containers behave.

If the process inside a container exits, the container exits.

Compose doesn’t turn it into a permanently running container.

A common beginner mistake

You may see examples where someone writes a large Compose file immediately:

services
├── application
├── database
├── cache
├── worker
└── ...

That can make Compose look complicated.

Don’t start there.

The core idea is much smaller:

services:
  service-name:
    image: image-name

Everything else can be added when the application actually needs it.

For example:

services:
  hello:
    image: hello-world

is enough to introduce the fundamental relationship:

service
   │
   └── image
         │
         ▼
      container

Once that makes sense, adding more services becomes much easier.

Tip

When learning Compose, don’t memorize a large compose.yaml file. Learn what each section represents and add pieces only when your application needs them.

One more important detail: the service is not the container name

It is tempting to think that this:

services:
  hello:

means the container itself is named hello.

That is not the right mental model.

hello is the service name in the Compose configuration.

Compose uses the service definition to manage the corresponding container.

For now, think of the relationship as:

Compose configuration
        │
        ▼
   hello service
        │
        ▼
managed container

The service is the configuration-level concept you’ll work with throughout this chapter.

Clean up

If you used the two-service example, stop and remove the Compose application’s resources:

docker compose down

You can then remove the example directory if you no longer need it:

cd ..
rm -rf compose-first

The exact directory-removal command depends on your operating system. The important Docker cleanup command for this lesson is:

docker compose down

What you should remember

The first Compose file can be tiny:

services:
  hello:
    image: hello-world

The important pieces are:

services
   │
   └── hello
         │
         └── image

And the basic workflow is:

compose.yaml
     │
     ▼
docker compose up
     │
     ▼
Compose manages the service
     │
     ▼
docker compose down

You don’t need to know networks, volumes, environment variables, or advanced YAML yet.

You’ve established the foundation:

A Compose file describes services, and each service describes a container-based component of the application.

In the next lesson, you’ll look at the Compose lifecycle in more detail: what happens when you bring an application up, stop it, start it again, and take it down.

Last updated on