Images, volumes and ports¶
Exercise images-volumes-and-ports-annex.pdf · PDF
You change the web page inside your nginx container. It works: the browser shows your new text. Then you remove the container and start a new one from the same image. Which page comes back: yours, or the original?
Make your guess now. The answer is the most important fact about containers, and it decides where a company keeps its web pages, its passwords and its databases. On this page you build the first page of Tramuntana's intranet, a database that survives being destroyed, and your own image.
What you will learn
- Show that a container's own files disappear with it, and why that is a feature
- Keep data outside the box with a bind mount and with a named volume, and choose between them
- Publish a port for everybody or only for the server itself (
127.0.0.1) - Configure a container with environment variables, read from the image's documentation
- Read a container's logs to find out why it stopped
- Write a Containerfile, build an image and run it
The box forgets¶
Start web as on the Containers page and change its page from inside:
podman exec web sh -c 'echo "<h1>Edited inside</h1>" > /usr/share/nginx/html/index.html'
curl http://localhost:8080
The answer is <h1>Edited inside</h1>. Now remove the box and start a fresh one:
podman rm -f web
podman run -d --name web -p 8080:80 docker.io/library/nginx:1.30
curl -s http://localhost:8080 | grep title
<title>Welcome to nginx!</title>. Your edit is gone.
Why? The image is read-only. Each container gets a thin writable layer on top of it, and rm throws that layer away. This is a feature, not a bug: any container can be replaced by a fresh one in a second, and updating a service is simply "remove the old box, start the new one".
So anything worth keeping must live outside the box. Picture the shipping container with a hatch cut in its side, and a shelf of the warehouse showing through. That hatch is a mount.
flowchart LR
subgraph BOX ["The container (thrown away and replaced)"]
direction TB
W["<b>writable layer</b><br>deleted with rm"]
I["<b>image</b><br>read-only"]
end
subgraph WH ["ubu-srv: the warehouse (stays)"]
direction TB
BM["<b>bind mount</b><br>~/intranet/site"]
NV["<b>named volume</b><br>dbdata"]
end
BM -->|"-v ./site:/usr/share/nginx/html"| BOX
NV -->|"-v dbdata:/var/lib/postgresql"| BOX
| Bind mount | Named volume | |
|---|---|---|
| What it is | A folder of the server that you choose | A storage area that Podman creates and manages |
| Written as | -v ./site:/usr/share/nginx/html |
-v dbdata:/var/lib/postgresql |
| Good for | Files you edit yourself: a web page, a config file | Data the program manages: a database |
| You find it at | Where you put it | podman volume inspect dbdata |
Careful: a volume is not a backup
It is the only copy, stored on the same disk as everything else. If the disk dies, it dies too. Backups (Unit 4) are still needed.
The intranet page: a bind mount¶
Work on ubu-srv as your normal user. Start clean with podman rm -f web web2. Create the folder and the page:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Tramuntana Apps intranet</title></head>
<body>
<h1>Tramuntana Apps</h1>
<p>Intranet, version 1. Served from a container.</p>
</body>
</html>
Serve that folder from nginx:
podman run -d --name web -p 8080:80 -v ./site:/usr/share/nginx/html:ro docker.io/library/nginx:1.30
curl -s http://localhost:8080 | grep "<p>"
curl prints <p>Intranet, version 1. Served from a container.</p>. Read the -v option like -p: left of the colon is the server, right is where it appears inside the box. The final :ro makes it read-only for the container.
Change "version 1" to "version 2" with nano on the server and reload the browser: the page changes at once, with no restart. Now try to write from inside:
$ podman exec web touch /usr/share/nginx/html/x
touch: cannot touch '/usr/share/nginx/html/x': Read-only file system
Why would a company want a web server that cannot change its own pages? If the service is ever attacked, the attacker cannot deface the site.
Two usual errors
the container name "web" is already in use → an old container is still there: podman rm -f web. lstat site: no such file or directory → you are in the wrong folder: pwd must say /home/<you>/intranet.
A port only for the server¶
-p 8080:80 listens on every network interface of the server (0.0.0.0). Sometimes only the server itself should reach a service. Remove web and start it again with an address in front:
podman run -d --name web -p 127.0.0.1:8080:80 -v ./site:/usr/share/nginx/html:ro docker.io/library/nginx:1.30
podman port web
podman port prints 80/tcp -> 127.0.0.1:8080 instead of 0.0.0.0:8080. curl on ubu-srv still works; the browser on the host (http://192.168.122.10:8080) does not. As in Unit 3: 0.0.0.0 = every interface, 127.0.0.1 = the loopback, the machine itself. A database should almost never be published on 0.0.0.0.
Configuring a box: environment variables¶
Everybody downloads the same postgres image. How does your database get your password? It cannot be inside the image: the image is public. Instead, the image reads environment variables (Unit 5) when it starts, and you pass them with -e NAME=value.
Which variables? You do not guess: you read the image's page. On Docker Hub, the postgres image has a section "Environment Variables": POSTGRES_PASSWORD is required, POSTGRES_USER and POSTGRES_DB are optional. Every serious image has such a list. An image is somebody else's program, and reading its documentation is the skill.
What happens if you forget it:
$ podman run --name db docker.io/library/postgres:18
Error: Database is uninitialized and superuser password is not specified.
You must specify POSTGRES_PASSWORD to a non-empty value for the
superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".
podman ps -a shows Exited (1). With -d you would not see the message on the screen, but the same text is in podman logs db. Remove it with podman rm db.
Careful: Podman is not broken
A container lives exactly as long as its main program. If the program refuses to start, the container exits. The reason is almost always in podman logs.
A database that survives¶
PostgreSQL is a free database server: a program that stores data in tables and answers questions about it. Give it a named volume and a password:
podman volume create dbdata
podman run -d --name db -e POSTGRES_PASSWORD=Tramuntana-2027 \
-v dbdata:/var/lib/postgresql docker.io/library/postgres:18
podman logs db
The last log line must be database system is ready to accept connections. There is no -p: nobody outside the box needs to reach it today.
Put some data in with psql, the database's own client, inside the box:
CREATE TABLE staff (name text, dept text);
INSERT INTO staff VALUES ('marta','dev'), ('joan','design'), ('aina','admin');
SELECT * FROM staff;
\q
The answers are CREATE TABLE, INSERT 0 3 and a table with three rows. (SQL is a subject of its own next year; today you only type it. If the prompt changes to postgres-#, you forgot the ;.)
Now the destruction test:
Run the same podman run of the database again, wait a few seconds, then:
The three rows are back. The box is new; the cargo was safe in the warehouse.
Careful: the path must be exact
A typo in the volume path gives a database that works and then forgets everything at the next rm. For postgres:18 the path is /var/lib/postgresql; older versions used /var/lib/postgresql/data. The image's page explains it.
Who can read the password?¶
It prints POSTGRES_PASSWORD=Tramuntana-2027. A variable is not hidden: anyone who can run podman exec or podman inspect on the server reads it. That is one more reason why who may log in to the server matters (key-only SSH, from SSH remote access).
Your own image: the Containerfile¶
The intranet works, but it is a folder on one server plus a long command. Could you hand Marta one thing that contains nginx and your page? Yes: your own image.
A Containerfile is a text file with the recipe of an image, one instruction per line. Docker calls it
Dockerfile; the syntax is the same.
In ~/intranet, create a file called Containerfile:
# Tramuntana Apps intranet: nginx plus our page
FROM docker.io/library/nginx:1.30
COPY site/ /usr/share/nginx/html/
EXPOSE 80
| Instruction | Meaning |
|---|---|
FROM |
Start from this existing image; nobody starts from nothing |
COPY |
Copy files from the build folder into the image |
RUN |
Run a command while building, for example apt-get install |
EXPOSE |
A note: this image listens on that port. It does not publish it |
CMD |
The program a container starts by default; here nginx's own is kept |
Each instruction adds a layer on top of the previous ones. Build it:
$ podman build -t tramuntana-intranet:1.0 .
STEP 1/3: FROM docker.io/library/nginx:1.30
STEP 2/3: COPY site/ /usr/share/nginx/html/
STEP 3/3: EXPOSE 80
COMMIT tramuntana-intranet:1.0
Successfully tagged localhost/tramuntana-intranet:1.0
-t gives the image a name and a tag. The final dot is the build folder: where COPY looks for site/. podman images lists localhost/tramuntana-intranet 1.0 165 MB: nginx's 165 MB plus a tiny layer with your page (podman history tramuntana-intranet:1.0 shows it).
Run it with no volume at all:
podman run -d --name intranet -p 8081:80 localhost/tramuntana-intranet:1.0
curl -s http://localhost:8081 | grep "<p>"
The page comes from inside the image. Now edit the page to "version 3" and look at port 8081: it still shows the old text, because the image is a frozen copy. Build tramuntana-intranet:2.0, remove intranet, start it from the new tag. Two tags, two versions, and going back is one command.
| Bind mount | Own image | |
|---|---|---|
| Use it for | A page you are still editing | A finished version you want to ship and number |
| Changes appear | At once | Only after a new build |
Key takeaways¶
- A container's writable layer is deleted with
podman rm. That makes containers replaceable, so data must live outside the box. - A bind mount (
-v ./folder:/path) shares a server folder you choose; a named volume (-v name:/path) is storage Podman manages, right for databases. Neither is a backup. -p 127.0.0.1:8080:80publishes a port only for the server itself;-p 8080:80publishes it on every interface.- Images are configured with environment variables (
-e), listed in the image's documentation. They are readable withpodman exec ... env. - A container lives as long as its main program: when it exits, read
podman logs. - A Containerfile (
FROM,COPY,RUN,EXPOSE,CMD) is the recipe;podman build -t name:tag .makes your own image.
Quick check¶
1. You edit a file inside a container that has no volume, then run podman rm. What happens to the edit?
- It is deleted together with the container
- It is kept inside the image for next time
- It is kept in a hidden volume
- It is copied to your home folder
The edit was in the container's writable layer, which belongs to that container and disappears with rm. The image stays read-only.
2. Where should the files of a database container live?
- In a named volume
- In the image itself
- In the container's own layer
- In the /tmp folder of the container
A named volume is storage that Podman keeps outside any container, so the data survives when the box is removed and replaced.
3. What does -e POSTGRES_PASSWORD=... do?
- It sets an environment variable that the image reads at start
- It encrypts the whole database on the disk
- It publishes the database port to the network
- It creates a volume for the database files
-e passes an environment variable. The postgres image reads it at start to set the superuser password; without it, it refuses to start.
4. Who can connect to a container published with -p 127.0.0.1:8080:80?
- Only programs on the server itself
- Any PC on the network
- Only other containers
- Nobody at all
127.0.0.1 is the loopback: the port is open only for the server. curl on ubu-srv works; the host's browser does not.
5. Which Containerfile instruction names the image you build on?
- FROM
- COPY
- EXPOSE
- CMD
Every image starts FROM an existing one. COPY adds files, EXPOSE only documents a port, and CMD sets the default program.
6. You start postgres:18 without any -e, and podman ps -a shows Exited (1). What is the best next step?
- Read podman logs to see why the program stopped
- Reinstall Podman, because it has crashed
- Add EXPOSE 5432 to make it stay up
- Restart the server so the box can start
A container lives as long as its main program. The logs say exactly why it stopped: here, POSTGRES_PASSWORD was not specified.
7. You built tramuntana-intranet:1.0, then edited the page on the server. Select the two that are true.
- A container from tag 1.0 still shows the old page
- Building a new tag puts the edited page inside a new image
- The running container picks up the edit at once
- The edit changes the 1.0 image on the disk
An image is a frozen copy: COPY took the page at build time. Only a bind mount shows edits at once. Build 2.0 and replace the container, and you can still go back to 1.0.
Today you typed two long commands, one for the web and one for the database, and you had to remember both. Next, you write them once in one file and start a whole application with three words: Composing services.