CodeGym /Courses /Docker for Spring /services, networks, volumes and DNS

services, networks, volumes and DNS

Docker for Spring
Level 15, Lesson 2
Available

1. services: roles and containers

When you first open compose.yaml, it is easy to fall into a trap: it seems that services are just a list of containers that Compose will “bring up.” But it is better to think differently: a service is a description of a role in an environment, and a container is only a specific “copy” of that role. It is like a recipe (service) and a baked pizza (container): you can repeat the recipe, and the pizza... well, it usually does not live long.

Service vs image vs container — a short map of concepts

We already have the basic Docker terms in the course, but Compose adds one more — service. Let us turn that into a small table so the brain does not overheat:

Term What it is How to think about it in plain language
Image A built template/artifact “A box with an application” (template)
Container A running process based on an image “A running box” (instance)
Service A description of how to create container(s) “Instruction: how to run this role in the environment”

Compose works with descriptions exactly. It reads YAML and decides: “Okay, here is the app service; it needs this image or build, these variables, these ports, and these volume mounts.”

Service in YAML: a minimal example

Here is a service that takes a ready-made image and publishes a port:

services:
  # Service name (role) — this is what you will rely on in Compose
  app:
    # Which image to run (if build is not specified)
    image: docker-java-catalog-service
    # Publish the port to the host machine: host:container
    ports:
      - "8080:8080"

In these 5 lines there is an important idea: app is the service name, not the container name. Compose will later create a container for this service (and name it according to its own rules), but your point of reference is the service name.

Why container_name is a bad crutch

Compose has a container_name setting, and to a beginner it looks like a lifesaver: “Oh, I will set a fixed container name and refer to it!” But that is like labeling every screw in a piece of furniture with a marker — at first it seems helpful, and then suddenly you cannot assemble the wardrobe from the instructions.

Compose is good because it can manage multiple instances of a service, recreate containers, change the order of creation, and still preserve a predictable model. If you make container_name your mandatory point of reference, you break part of that flexibility and often create future name conflicts for yourself.

In our course, we stick to the rule: the anchor is the service name, not the manually assigned container name.

For now, in our compose.yaml, there is only one service, so the network is easy to mistake for premature theory. But the localhost problem appears precisely when even one neighbor shows up — a database, a cache, or an internal client. It is better to grasp this network minimum now, while there is no noise from dependencies, than to relearn it later on a live multi-container setup.

2. networks: default network and service communication

When you have one container, the network seems unnecessary: you mapped a port — and that is it. But as soon as there are at least two participants in the environment, it becomes important how they communicate. Compose solves this by creating a network for your environment (usually automatically), attaching services to it, and giving them the ability to find one another by name. This is not “magic,” it is just convenient automation of something you could do manually.

Default network: the default network

If you do not describe networks in compose.yaml, Compose will still create a project network. This is called the default network: “the default network for this Compose environment.” Inside it, services get IP addresses, as well as records in the built-in DNS.

An important nuance for beginners: this network is internal; it is for communication between containers. It is not for “access from your machine”; that is what ports are for.

ports is a bridge to the host, not a “permission to communicate inside”

The command and YAML key ports are very often misleading, because it seems: “If I did not specify ports, then the service is unavailable.” That is only true for access from your host machine (for example, from a browser on localhost). Inside the Compose network, things are different: containers communicate through internal ports.

Feel the difference:

ports: "8080:8080" means “from the host machine, you can go to 8080, and that will reach port 8080 in the container.”

For communication inside the network, ports is not required at all: services can reach each other directly because they are on the same network.

A small diagram to make it easier to picture:

flowchart LR
  %% Access from the host machine: through the published port (ports)
  Host["Your machine (host)"] -->|localhost:8080| App["service: app (container)"]

  %% Access between containers: by service name through the built-in DNS
  Client["service: internal-client (container)"] -->|http://app:8080| App

  subgraph ComposeNetwork["default network (Compose)"]
    App
    Client
  end

The idea here is simple: “host → container” goes through ports, while “container → container” goes through the network and DNS by service name.

Explicit networks: when to write networks

We are not building complex topologies today. But it is useful to see that an explicit network in YAML is just a resource declaration and a connection to it:

services:
  app:
    image: docker-java-catalog-service
    networks:
      # Explicitly connect the service to the specified network
      - app-net

networks:
  # Declare the network as a Compose environment resource
  app-net:

These 8 lines say: “Create the app-net network and connect app to it.” For one service, this looks like extra writing, and often that is exactly what it is. But if you want to make the environment more disciplined, or later connect several services to the same network explicitly, this becomes a clear tool. For the first compose.yaml, the default network is usually enough.

3. Compose DNS: service name as hostname and the localhost trap

With docker run, we got used to: “There is a container — it is isolated, but I can map a port to localhost.” In Compose, where there are several services, a new habit appears: if you want to reach a neighboring service, you use the service name as the host name. This will become the main engineering reflex in any multi-container environment, but we are training it now, while everything is simple and painless.

Service name as an address: http://app:8080

Suppose you have two services: our app (Catalog Service) and a hypothetical internal client that talks to it over HTTP. In Compose, it looks like this:

services:
  # Service that provides an HTTP API inside the Compose network
  app:
    image: docker-java-catalog-service

  # Internal client (also a container) that will call app
  internal-client:
    image: curlimages/curl:8.7.1
    environment:
      # Important: the service name app is used as the hostname in Compose DNS
      APP_BASE_URL: http://app:8080

Pay attention to http://app:8080. Here app is not the internet DNS and not “your machine’s hostname.” It is an entry in the built-in DNS of the Compose network: “the app service is reachable by the name app.”

The container IP address may change (the container was recreated — the IP is different), but the service name remains. That is the main victory over chaos: we stop hoping for stable IPs and start living with stable names. For the same reason, a database in Compose lives under a name like db, not some random IP and not localhost.

localhost inside a container means “myself”

One of the most common beginner mistakes is: “I am inside the container, so localhost is my machine.” No. localhost inside a container is that same container.

If the internal-client container tries to go to http://localhost:8080, it will look for an HTTP server inside itself. And inside itself it most likely has nothing but curl and quiet despair.

A mini illustration (do not run this as a “recipe,” just feel the difference in addresses):

# localhost inside the container = this same container (most likely there is no HTTP server there)
curl http://localhost:8080/actuator/health

# service name app = a neighboring container in the Compose network (built-in DNS)
curl http://app:8080/actuator/health

If you keep this rule in your head, you save hours of future “why is it not connecting?!”

A little about stability: name instead of IP

In theory, you can find the container IP and use it. In practice, this breaks immediately when the container is recreated. Compose does you a very human favor: “Do not think about IPs, think about names.” It is like a phone book: you do not remember that your colleague had number +1... yesterday and +1... today (well, hopefully), you look them up by name.

That is why in Compose, “clean” and meaningful service names matter: app, db, cache, broker, and so on. At our current stage, it is enough to understand the principle: the service name should be suitable for use in a URL.

4. volumes: data separate from the container

A container is a “disposable” thing, in a good sense. It is easy to recreate, and that is normal. But data that you want to keep should usually not disappear together with the container. Over the past days, we already talked about writable layers, bind mounts, and named volumes. Now we are adding an important layer: in Compose, volumes are part of the environment description, and they live next to services in the same YAML.

Volumes in Compose: two styles

In Compose, you can attach storage to a service through the volumes key. But the storage itself comes in two types: bind mount (a path on the host machine) and named volume (a Docker-managed volume). Their syntax is similar, and that regularly confuses people, so let us compare them in real life, not just in YAML.

Question Bind mount Named volume
What is on the left in the X:Y entry A path on the host machine The volume name (a Docker resource)
Are the data visible in the project file system Yes, it is just a normal directory Usually no, it is managed storage
Portability between machines Worse (paths, permissions) Better (Docker manages it itself)
When it is convenient Exporting files outward, working with code/configs Storing service state that does not need to be visible
Do you need to declare it at the top level volumes: No Yes (usually, to explicitly declare the resource)

Bind mount in Compose: our “export” scenario

For Container-Ready Catalog Service, we have a catalog export into files, and we want to see the result on the host machine in ./data/exports. That means a bind mount is the logical choice:

services:
  app:
    image: docker-java-catalog-service
    volumes:
      # Bind mount: left side is the host path, right side is the container path
      - ./data/exports:/data/exports

On the left, ./data/exports is a path in the repository. On the right, /data/exports is a path inside the container. It is the same principle as with docker run -v ..., just in YAML form.

Named volume in Compose: when the data matter, but “do not need to be exposed”

Sometimes you need the data to survive restarts, but you do not need to open the directory in a file manager and inspect the files by hand. Then a named volume becomes more convenient and less “fragile” (especially cross-platform):

services:
  app:
    image: docker-java-catalog-service
    volumes:
      # Named volume: left side is the Docker resource name, right side is the container path
      - exports-data:/data/exports

volumes:
  # Declare the named volume as a Compose environment resource
  exports-data:

Notice that a top-level volumes section has appeared. It declares the exports-data resource. The service then only “attaches” it.

Important rule: a volume does not connect services; it stores data

A very common logical mistake sounds like this: “If I attach a volume, then services will somehow see each other through it.” No. A volume is about files (state), a network is about connection (communication). These are two different axes. You could say the network is a “phone,” and the volume is a “cabinet with folders.” You cannot make a phone call through a cabinet.

If you need services to see each other, use the network and the service name. If you need to preserve files, use a volume or bind mount.

5. Three axes of the environment: processes, network, data

At this stage, beginners often get the feeling: “Compose is just YAML with a bunch of keys that you need to memorize.” In reality, it is more convenient to think like this: Compose describes an environment in three dimensions. There are processes (services), there is communication (networks), and there is state/files (volumes). If you learn to read any compose.yaml along these three axes, the file will stop looking like a shamanic scroll and start being an ordinary configuration.

A mini schema for reading a compose file

Let us break down “how to read it” at the level of thinking. First, you find services and understand who is participating in the environment. Then you look at how they communicate (default network or explicit networks). And only then do you figure out where the data live (volumes/bind mounts).

Here is a compact example where all three axes are visible, but without overload:

services:
  # Process/role in the environment
  app:
    image: docker-java-catalog-service

    # Access from the host machine to the container
    ports:
      - "8080:8080"

    # Data/files that should live outside the container
    volumes:
      - ./data/exports:/data/exports

Here, the service and storage (bind mount) are explicitly described, while the network is not described — that means there will be a default network. That is fine. Even if we currently have only one service, Compose still creates a “container environment,” and later it is convenient to add neighbors to it without rewriting the world.

The service name matters already now, even when there is only one service

Today, in the first version of compose.yaml, we will have one service only — our app. You might ask: “Why do I need DNS and a service name if there is nobody to talk to?” The answer is simple: the service name is part of the discipline. You name the service the way you want to refer to it in the environment. Even if there are no neighbors now, you do not want to rename everything tomorrow (when a second participant appears) and break the configuration.

If you choose the name app, it is short, neutral, and readable. If you choose the name very-important-catalog-service-that-i-love, the YAML will get longer, and life will not get better. Compose encourages simple names.

6. Typical mistakes when working with Compose

In Compose, mistakes often look “as if the application broke,” although in reality you just modeled the environment incorrectly in your head. The good news is that these mistakes are typical, and they are fixed not by heroics, but by the right words in your head. Let us reinforce the most common ones now so that in lecture 5 compose.yaml builds calmly, without “why is it not opening and why am I not sleeping again.”

Mistake #1: addressing a neighboring service through localhost.
This is a classic. Inside a container, localhost means “myself,” not another container and not your host machine. In Compose, to address a neighbor, you use the service name: http://app:8080, http://some-service:1234, and so on. If you fix in your head that “service name = hostname,” this mistake almost disappears.

Mistake #2: confusing the container’s internal port with the published host port.
The entry ports: "9090:8080" means “from the host machine, we go to 9090, and inside the container the application listens on 8080.” But inside the Compose network, another container should address its neighbor on the internal port, that is, http://app:8080, not http://app:9090. The internal port is the port of the process in the container; the host port is only the external “entry” from your machine.

Mistake #3: thinking that service is “one specific container forever.”
A service is a description. Containers are recreated, updated, and can be restarted after changes. If you start attaching yourself to a specific container as if it were a “character,” strange expectations appear, like “why did the IP change?” or “why was the container recreated?” Compose works with descriptions, and containers are replaceable executors.

Mistake #4: assuming that a volume is “a way to connect services.”
A volume is storage. It solves the problem of data and files, but not the problem of network communication. Communication is networks and DNS. If you want services to see each other, think about the network and service names. If you want to preserve files, think about bind mounts and named volumes. Mixing these models creates very strange configs.

Mistake #5: using container_name as the foundation of the whole environment.
At the start, it seems convenient to name a container manually so that you “definitely know how to reach it.” But in Compose, it is better to rely on the service name and built-in DNS. container_name often breaks scaling, creates name conflicts, and turns the environment into a “manual zoo,” which is exactly what Compose wanted to save you from.

1
Task
Docker for Spring, level 15, lesson 2
Locked
Accessing a sibling service by name on the default network
Accessing a sibling service by name on the default network
1
Task
Docker for Spring, level 15, lesson 2
Locked
Explicit network and bind mount for the result file
Explicit network and bind mount for the result file
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION