Composing services¶
Practical 6.5 composing-services-annex.pdf · PDF
Try to write, from memory, the command that started the database on the Images, volumes and ports page. A name, a password variable, a volume with an exact path, the image with its tag... Did you get every option right?
Now imagine a real application: the program, its database, maybe a cache. Two or three of those long commands, typed in the right order, with no typo, on every server, and still remembered in six months. Nobody can do that reliably. What you need is a file that describes what must exist, and a tool that makes it so.
What you will learn
- Explain what a compose file is and what problem it solves
- Read and write basic YAML: keys, lists, indentation
- Describe an application as services, ports, volumes and environment in
compose.yaml - Start, inspect and stop the whole application with
podman compose up,ps,logsanddown - Explain how one service finds another by name on the project's private network
- Deploy a real application with its database, and prove the data survives
One file for the whole application¶
Ships do not leave port with the crew remembering where each container goes. They carry a loading plan: a document that lists every box, where it sits and what is connected to what. Any crane operator in any port can read it.
A compose file (
compose.yaml) describes an application as a set of services. Each service is one container: its image, ports, volumes and environment.
One command reads the plan and starts everything:
Docker's command is docker compose up -d, with the same file.
Careful: YAML runs nothing
A compose file is not a program. It only describes. The tool (podman compose) reads the description and runs, for you, the same podman commands you typed by hand.
YAML in four rules¶
The file is written in YAML, a text format for structured data. You met it already in the Netplan file of Clients, servers and shared resources. Here is the nginx command of the intranet, translated:
services:
web:
image: docker.io/library/nginx:1.30
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
key: value, with a space after the colon.- Indentation is the structure: two spaces per level, never tabs.
webis insideservices;imageis insideweb. - A line starting with
-is an item of a list. - Quote the port pairs:
"8080:80".
| In the command | In the compose file |
|---|---|
--name web |
The service name web: |
-p 8080:80 |
ports: |
-v ./site:... |
volumes: |
-e NAME=value |
environment: |
-d |
The -d of podman compose up -d |
The intranet as a compose file¶
On ubu-srv, install the tool and clear the old containers:
Ubuntu 26.04 installs podman-compose 1.5.0. podman compose (with a space) is Podman's own subcommand: it finds the compose tool and hands it the work.
Save the YAML above as ~/intranet/compose.yaml (the site folder is already there) and check it has no tabs: cat -A compose.yaml shows a tab as ^I. Then, from ~/intranet:
podman compose up -d
podman compose ps
curl -s http://localhost:8080 | grep "<p>"
podman compose logs
psshows one container calledintranet_web_1: project name (the folder) + service name + a number.upalso created a network,intranet_default, for the project.- Each log line is prefixed with the service name:
[web] | ... "GET / HTTP/1.1" 200 ....
podman compose down removes the container and the network. Change the host port to 8090 in the file, up -d again, and it answers on 8090. The file is now the single source of truth: nobody needs to remember the command. Finish with down, so port 8080 is free.
Where people get stuck
An error at up naming a line and a column (in "compose.yaml", line 3, column 1) is almost always indentation: a tab, or image at the same level as web. And no compose.yaml ... file found means you are not in the project folder.
A real application: Gitea and PostgreSQL¶
Gitea is a Git server with a web interface: a private GitHub inside the office, where Tramuntana's programmers keep their code. (Git itself comes in Unit 7.) Today it is simply a real application that needs a real database. The file below is the PostgreSQL example from Gitea's documentation, with named volumes and a password taken from a separate file.
The project folder is ~/app. First the secret, in a file called .env:
Then compose.yaml:
services:
server:
image: docker.gitea.com/gitea:28.0.0
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${DB_PASSWORD}
restart: always
volumes:
- gitea-data:/data
ports:
- "8080:3000"
- "2222:22"
depends_on:
- db
db:
image: docker.io/library/postgres:18
restart: always
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=gitea
volumes:
- db-data:/var/lib/postgresql
volumes:
gitea-data:
db-data:
Read it like a loading plan:
- Two services:
server(Gitea) anddb(PostgreSQL), so two containers. - The database is configured with the variables you found on its Docker Hub page:
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB. GITEA__database__HOST=db:5432:dbis the service name. Inside the project's private network every service is reachable by its name, through a small DNS that Podman runs for the project.ports: the office can reach 8080 (the web; inside its box Gitea listens on 3000) and 2222 (Git over SSH). The database has noports: only Gitea can talk to it.${DB_PASSWORD}is replaced by the value in.env. The compose file can be shared; the.envfile must not be.depends_onstartsdbbeforeserver;restart: alwaysstarts a container again if it stops.- The last
volumes:block declares the two named volumes, the warehouses for the data.
flowchart LR
OFFICE["<b>Office PCs</b><br>browser, git"] -->|"8080 web<br>2222 Git over SSH"| SERVER
subgraph NET ["app_default: the project's private network"]
SERVER["<b>server</b><br>Gitea, listens on 3000"] -->|"db:5432<br>found by name"| DB["<b>db</b><br>PostgreSQL<br>no published port"]
end
SERVER --- V1[("gitea-data")]
DB --- V2[("db-data")]
Careful: localhost inside a container
Writing HOST=localhost:5432 is a classic mistake. Inside a container, localhost is that container itself, not the server. Other services are reached by their service name.
Deploy Tramuntana Git¶
This is the Practical 6.5 sheet. From ~/app:
Two containers, app_db_1 and app_server_1, both Up, with ports 0.0.0.0:2222->22/tcp, 0.0.0.0:8080->3000/tcp on the server only. The database log ends with database system is ready to accept connections, and podman volume ls lists app_db-data and app_gitea-data.
First-run setup¶
Open http://192.168.122.10:8080 in your host's browser. Gitea shows Initial Configuration:
- Database Settings are already filled in, from the environment variables. Change nothing.
- General Settings: Site Title
Tramuntana Git· SSH Server Port2222· Gitea Website URLhttp://192.168.122.10:8080/. Leave HTTP Server Port at3000: that is the port inside the container. - Optional Settings → Administrator Account Settings: your first name, an e-mail and a password (not the database one).
- Press Install Gitea, sign in, and create a repository called
intranet.
Where is the data?¶
Your repository is a row in PostgreSQL:
One row: intranet. Now the test:
After down: no containers, no network, but both volumes are still listed. After up: the site, your user and the repository are back, with no setup page.
| Command | Containers | Network | Named volumes |
|---|---|---|---|
podman compose down |
Removed | Removed | Kept |
podman compose down -v |
Removed | Removed | Deleted: users and repositories lost |
Survive your logout¶
The containers of a normal user stop when that user's last session ends. Tell systemd to keep them running:
Close your SSH session and reload the browser: Tramuntana Git is still there.
Two more traps
- Changed the password in
.envand nowpassword authentication failed for user "gitea"? PostgreSQL only reads the variable when its volume is empty: it keeps the first password. In a practical,podman compose down -vand start again. depends_ononly starts the containers in order; it does not wait until the database is ready. Gitea itself retries the connection.
Key takeaways¶
- A compose file describes all the services of an application in one place;
podman compose up -dstarts them,downremoves them. Docker uses the same file. - YAML:
key: value, lists with-, and indentation with spaces is the structure. Tabs are an error. - Each project gets a private network; services find each other by service name (
db:5432), never bylocalhost. - Publish only what the office needs: the database has no
ports. - Secrets go in
.env(mode 600), not in the shared compose file. downkeeps the named volumes;down -vdeletes them, and the data with them.
Quick check¶
1. What is a compose file for?
- Describing all the services of an application in one file
- Building a new image from a recipe
- Making backups of the named volumes
- Installing Podman on a new server
A compose file lists each service with its image, ports, volumes and environment. One command starts them all; building images is the job of a Containerfile.
2. How does one service find another one in the same compose project?
- By the service name, on the project's private network
- By localhost, since they run on the same server
- By the server's fixed IP address
- It cannot: services are fully isolated
Compose puts the services on a private network with a small DNS, so db resolves to the database container. Inside a container, localhost is the container itself.
3. After podman compose down, what is still there?
- The named volumes
- The containers
- The project network
- Nothing at all
down removes containers and network but keeps the named volumes, so up brings the data back. Only down -v deletes the volumes.
4. In a YAML file, what decides that image belongs to the service web?
- The indentation
- The capital letters
- The order of the lines
- A semicolon at the end
In YAML, indentation with spaces is the structure: image is two spaces deeper than web, so it is inside it. A tab makes the file invalid.
5. Why can a PC in the office not connect to the PostgreSQL of Tramuntana Git?
- The db service publishes no port
- The database traffic is encrypted
- PostgreSQL is stopped by default
- Its password is too long to type
Without a ports section, the database is reachable only inside the project's private network, so only Gitea can talk to it. A port that is not published cannot be attacked from the office.
6. Gitea cannot reach its database, and the file says GITEA__database__HOST=localhost:5432. What is wrong?
- Inside the Gitea container, localhost is Gitea itself
- Port 5432 must be published with a ports section
- PostgreSQL only answers on port 3000
- The database must start after Gitea, not before
localhost always means the container you are in. The database is another container, reached by its service name: db:5432.
7. Pau wants to share the Tramuntana Git setup with a colleague. Select the two that are true.
- He can share compose.yaml, because it holds no password
- He must keep .env private, because it holds the password
- He should paste the password into compose.yaml to keep one file
- He must also send his named volumes for the file to work
${DB_PASSWORD} is read from .env, so the compose file can be shared safely. The .env file, with mode 600, is the secret.
The office server now has files, printers and an application. Next: who is allowed to do what on it, in User rights and security policies.