CodeGym /Kursy /Docker for Spring /File compose.yaml: basic structure

File compose.yaml: basic structure

Docker for Spring
Poziom 15, Lekcja 1
Dostępny

1. compose.yaml as an environment description

If up to this point Docker for you was just a set of commands, Compose adds exactly one key idea: “let the environment live in a file, and let the commands be short.” It is important not to mix things up: Compose does not replace Docker Engine and does not create “a different Docker.” It simply gives you a convenient way to describe containers and their parameters so you do not have to keep them in your head or copy them around in chats.

What does Docker Compose do, in essence? It reads YAML and turns it into the very same actions you would perform by hand: build an image (if needed), create a network (if needed), create a volume (if needed), and start container(s) with the required ports, variables, and mount points. In other words, Compose is not “a second system,” but “a normal wrapper around what you were already doing by hand.”

It is convenient to think of it like this:

flowchart TD
  A["compose.yaml (description)"] --> B["docker compose up"]
  B --> C["Docker Engine: build image (optional)"]
  B --> D["Docker Engine: create network (optional)"]
  B --> E["Docker Engine: create volumes (optional)"]
  B --> F["Docker Engine: run containers"]
  F --> G["You check the API /actuator/health and logs"]

For today, we deliberately keep the environment simple: one service app, no external database, and no “zoo of dependencies.” This will help you understand the mechanics of Compose on a clean example instead of getting lost in what exactly is not starting: the application or the database.

2. Canonical names and the Compose command

When you start looking for examples on the internet, the world splits into two camps: somewhere people show a file named docker-compose.yml, and somewhere else — compose.yaml. And somewhere else again you find the command docker-compose, not docker compose. That is not because people love chaos (though sometimes it seems that way). It is because Compose evolved historically, and today we have the canonical modern path.

In this course, we fix the baseline: the file is named compose.yaml, and the command is docker compose (with a space). This matches modern Compose v2, which ships as part of the Docker CLI. From the standpoint of your future “grown-up” experience, that is beneficial: less confusion, less “but I have a different version,” less “why is the command not found?”

Practically, this means three simple rules.

First rule: the file lives at the root of the repository, next to Dockerfile, .dockerignore, and README.md. Do not hide it in docker/compose/compose.yaml unless you are teaching a separate course on finding files.

Second rule: you run Compose from the project directory (or specify the file explicitly). The most common scenario in a training project will look like this:

cd docker-java-catalog-service  # Move into the project directory (where compose.yaml is located)
docker compose up               # Start the environment based on compose.yaml

Third rule: we are not turning the course into archaeology, but context is useful. The old name docker-compose.yml and the command docker-compose are the legacy path, which you may encounter in other people’s repositories. Being able to read it is useful. But as the main teaching path, we do not use it, so we do not multiply variants.

3. YAML in Compose: indentation is your new “brackets”

A Compose file is YAML. And YAML is the kind of format that looks friendly... until you add one extra space. If in Java you forgot to close a curly brace, the compiler says, “friend, you forgot to close the block.” In YAML, indentation plays the role of “brackets.” And yes, this is one of those rare cases where spaces really matter, rather than just “making it look nice.”

Let us agree on the minimal YAML grammar needed specifically for Compose.

Key-value is a “map” (dictionary). Example:

services:        # Section with environment services
  app:           # Service name (logical name in Compose)
    build: .     # Build image from the current directory (Dockerfile by default)

Here services is the top-level key, and the value is a nested object. services has the key app, and app has the key build.

A list is an “array” with -. Example with ports:

ports:           # List of port mappings
  - "8080:8080"  # host:container (left is the port on your machine, right is inside the container)

ports is a list, where each element is a string. Why put it in quotes? Because that is more reliable and readable. YAML is smart, of course, but we are not competing with it in IQ — we want the file to be readable the same way on all machines and by all students.

And here is an important practical tip: when a file breaks because of indentation, you usually get an error like mapping values are not allowed here or something equally “poetic.” At such moments, you do not need to remember all the sins of the last two weeks. You just need to read the file from top to bottom and check that the nesting levels make sense.

Minimal compose.yaml skeleton

A Compose file is almost always read starting from the services section. That is the entry point. You could say that services is like public static void main in the world of environments: this is where “life” begins.

The smallest possible Compose file that makes sense at all looks like this:

services:      # Always start with the service description
  app:         # Service name (you will reference it in commands: logs, exec, etc.)
    build: .   # Build source for the image (context)

Yes, that is it. Three lines — and the meaning is already there: “there is a service app, and it is built from the current directory.” And now docker compose up --build will do what you would otherwise do by hand: docker build ... and docker run ...

In a real project, you will most often add ports, because otherwise the service will start but will be “visible only to itself” (the container). And then you get this basic minimum:

services:
  app:
    build: .
    ports:
      - "8080:8080"  # Publish the port outward: localhost:8080 -> container:8080

Now there is a “bridge to the outside”: host port 8080 is forwarded to container port 8080.

Pay attention to the fact that a Compose file is not the place where you “invent a new way to start an application.” If you already have a correct ENTRYPOINT in your Dockerfile, Compose does not have to repeat it. Compose should say which image to run and which runtime parameters to add.

4. build and image: do not mix them up

One of the most common early confusions: “should I write build or image?” This is not a matter of taste. These are two different scenarios.

build means: “build the image from source.” Compose will use the build context and the Dockerfile (by default, Dockerfile in the specified directory). Simple example:

services:
  app:
    build: .  # Build the image locally from the Dockerfile and the project context

In other words, we are saying: “take the Dockerfile from the current project and build it.”

image means: “take an already built image.” Usually either you built it earlier (docker build -t ...) or it lives in a registry. Example:

services:
  app:
    image: docker-java-catalog-service:latest  # Take the ready image by name:tag

And this is important: image does not “build”; it “uses.” So if the image does not exist locally and cannot be pulled, the startup will fail.

There is also a third, very practical case: using build and image together — for example, build: . plus image: docker-java-catalog-service. Then build is responsible for the build source, and image — for the result name. For local development, this is convenient: Compose builds the service and simultaneously saves the result under a predictable name that does not depend on the project directory name.

In a training project on the early Compose day, it is most often convenient to start with build: ., because that is fewer steps: you do not need to remember which tag you used when building the image. Compose will build and start it for you. But it is useful to understand both modes, because in real life you will use both: build for local development, image when you want to run a prebuilt, fixed artifact.

If you want to make build a little more explicit (and a little less magical), you can write it like this:

services:
  app:
    build:
      context: .           # Where the build context is located (what we send to the Docker daemon)
      dockerfile: Dockerfile  # Explicitly specify the Dockerfile name

It looks longer, but it reads like a “contract.” At the start of the course, though, that is not mandatory — sometimes brevity is more useful than ideological strictness.

5. Service settings: ports, env, volumes

When you move from docker run to Compose, the nicest moment is that you start seeing startup parameters as structure. Where there used to be a long one-liner, now there is YAML, where your eyes can immediately see: “ports are here, variables are here, volumes are here.”

To help your brain adapt faster, you can keep a small mapping table in mind. This is not a “full Compose reference,” but rather a bridge from the already familiar docker run flags to YAML keys:

What you did in docker run What it looks like in compose.yaml Meaning in plain language
-p 8080:8080 ports: ["8080:8080"] “Open the port on the host and forward it into the container”
-e SPRING_PROFILES_ACTIVE=standalone environment: { SPRING_PROFILES_ACTIVE: standalone } “Put an environment variable inside the container”
-v ./data/exports:/data/exports volumes: ["./data/exports:/data/exports"] “Mount a host directory into the container”

The simplest service with a port and one env var looks like this:

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: standalone

This is already enough to see the structure: ports is responsible for external access into the container, and environment for env vars inside it. If the service needs bind mounts or additional parameters, they are simply added in neighboring volumes and environment sections, while the file’s skeleton remains the same.

Two important points are clear here.

First, “environment settings” are separated by meaning: ports separately, variables separately, volumes separately. You do not have to track \ with your eyes and search in a line for where -e is and where -v is.

Second, Compose does not require you to remember the order of flags. In docker run, you can write -p before -e or after it — Docker will understand. But humans read that poorly. YAML, by nature, is structured: you group by sections, not by order.

6. Top level: networks and volumes

Even if today we have just one service, you can already see that Compose has entities at the “environment level,” not just the “container level.” This is an important idea, because Compose is not “running one container,” but “describing a stack.”

There are top-level sections in the file that often appear:

services — who participates in the environment (containers);
networks — how they connect over the network;
volumes — how state/data is stored through named volumes.

Today it is enough for us to understand the structural point: these sections are written at the same indentation level as services, not inside services.app. So this is correct:

services:
  app:
    build: .

volumes:
  exports-data:  # Declare a named volume at the top level (as an environment entity)

And this is incorrect, and YAML/Compose will not be pleased:

services:
  app:
    build: .
    volumes:
      exports-data:  # Error: here a list of mounts is expected, not a volume declaration

Inside the service, you attach the volume, and outside you declare it. Why is that? Because declaration means “we are creating an environment entity,” while attachment means “we are using the environment entity in a specific service.” The separation is similar to Java: you declare a class separately, and then use it in specific places.

Let me emphasize again: today we are not diving into the network model and DNS. We are simply establishing this: if you see networks: and volumes: at the top level, that is normal; that is how it should be. This is not “extra bureaucracy,” but structure that later saves the project from YAML chaos.

7. Minimal work cycle: up, logs, down

The file itself does not start anything. It is like build.gradle.kts: until you call a command, it is just text. That is why it is important to know the minimal “work cycle” that replaces manual docker build + docker run.

The most basic scenario looks like this:

docker compose up --build  # Start the environment and rebuild the image if necessary

The --build flag is useful when you use build: . and want to ensure that the image is rebuilt if you changed the code or the Dockerfile. At the start of learning, this makes behavior more predictable: fewer chances of “accidentally starting an old image.”

When the stack is up, you will almost immediately want to look at the logs:

docker compose logs app  # Show the logs for the app service (name taken from compose.yaml)

app is the service name, not the container name. That is the beauty of Compose: you think in terms of environment services, not random containers.

When you need to stop the environment, use:

docker compose down  # Stop and remove the created containers/networks (by default)

And here you get a very pleasant feeling: you are not searching for what the container was named, not recalling docker stop ... and docker rm .... Compose knows what it created and cleans it up neatly on its own.

If you like to “keep the terminal window clean,” you can run in the background (detached):

docker compose up -d --build  # Run in the background so the terminal is freed immediately

But at the start of learning, it is usually more convenient without -d, so that you can see the logs directly in the console and connect the action to the symptom faster.

8. Common mistakes when creating and reading compose.yaml

Mistake #1: “I wrote YAML, but Compose complains in incomprehensible words.”
Usually this is indentation. YAML does not forgive a “slightly shifted level,” because the nesting level is the meaning. The most reliable way to fix it is not to guess, but to open the file and read: services → app → service keys. If some key suddenly ended up at a different level, you will see it. And yes, sometimes it is literally one space that costs you 20 minutes and a couple of philosophical questions for the universe.

Mistake #2: confusion between build and image — “I thought it would build itself.”
If you wrote image: docker-java-catalog-service, Compose is not required to build that image. It is required to try to use it. If the image is missing, you get an error. If you want a build from the current repository, use build: .. Conversely, if you want to run a strictly specific ready-made artifact (especially in a team), image becomes more honest and predictable.

Mistake #3: expecting Compose to “understand everything for me,” even if I did not describe the ports.
Compose is not telepathic. If you did not specify ports, the application may work perfectly inside the container, but from the outside you will not expose it via localhost:8080. This is not a broken Spring Boot; it is simply a missing port publication. Compose does not publish ports “by default” because that would be unsafe and unexpected.

Mistake #4: trying to make the first compose.yaml too large.
It is very tempting to immediately add sections “for the future”: networks, volumes, restart-policies, healthchecks, profiles, depends_on, and five more things you saw in someone else’s repository. In practice, this kills learnability. The correct first file is readable at a glance and answers the question: “how do we start our service in the mode we need right now?” Everything else is extra noise that is later hard to distinguish from what is really important.

Mistake #5: mixing “Compose settings” and “application settings” into one soup.
Compose controls how the container starts: ports, environment variables, mount points. Spring Boot controls how the application behaves inside the container: profile, port, datasource, and so on. In the Compose file, you can pass values into the container through environment, but if you start trying to “configure Spring Boot with Compose keys” (or vice versa), you will quickly stop understanding where the cause of the behavior actually lives.

1
Zadanie
Docker for Spring, poziom 15, lekcja 1
Niedostępne
Minimalny `compose.yaml` z `build`
Minimalny `compose.yaml` z `build`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION