Przejdź do treści
Lekcja 95 z 117

Kurs › Moduł 10 › Lekcja 95

Kontenery, część 2

⏱ ok. 12 min czytania· moduł 10: Usługi i serwery

Po tej lekcji: kursant buduje własny obraz z pliku Containerfile, stawia aplikację z kilku kontenerów jednym plikiem compose, uruchamia kontenery bez roota, robi z kontenera usługę systemd przez Quadlet i wie, kiedy przy SELinux dopisać :Z.

Wstęp

kartka z odręcznym przepisem na pierogi, obok plik Containerfile

Do tej pory jedliśmy w restauracji: gotowe obrazy z hurtowni. Ale co, jeśli żaden obraz nie ma dokładnie tego, czego potrzebujesz? Twoja strona, Twoja konfiguracja, Twoje pliki. Możesz wejść do kontenera, doinstalować ręcznie i… stracić wszystko przy pierwszym rm.

Admin robi inaczej. Pisze przepis. Przepis, z którego każdy, na każdym serwerze, ugotuje dokładnie to samo danie. Dziś napiszemy taki przepis, zbudujemy z niego obraz, postawimy całą aplikację jednym plikiem, uruchomimy kontenery bez roota, a na koniec sprawimy, że kontener wstaje sam po restarcie serwera, jak porządna usługa.

Containerfile, czyli przepis na obraz

Pamiętasz: skrypt to przepis kulinarny. Containerfile też jest przepisem, tylko jego wynikiem nie jest zupa, ale obraz. W Dockerze ten sam plik nazywa się Dockerfile. Format jest identyczny.

Zróbmy własny obraz ze stroną sklepu. Folder projektu:

mkdir -p ~/projekt/strona && cd ~/projekt
echo '<h1>Sklep z kontenera</h1>' > strona/index.html

Konfiguracja nginx dla naszej strony, plik strona.conf:

server {
    listen 80 default_server;
    root /var/www/html;
    index index.html;
}

I sam przepis, plik Containerfile:

FROM docker.io/library/ubuntu:24.04
LABEL opis="Strona firmowa"
RUN apt-get update \
 && apt-get install -y --no-install-recommends nginx \
 && rm -rf /var/lib/apt/lists/*
COPY strona.conf /etc/nginx/sites-available/default
COPY strona/ /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Czytamy jak przepis:

  • FROM — weź gotowe ciasto. Zaczynamy od czystego Ubuntu, nie od zera.
  • LABEL — karteczka na słoiku, opis dla ludzi.
  • RUN — zrób to w kuchni. Tu: zainstaluj nginx i wyrzuć listę paczek, żeby obraz był lżejszy.
  • COPY — włóż do środka moje pliki z folderu projektu.
  • EXPOSE — informacja, że program słucha na porcie 80. Tylko informacja. Portu nie otwiera, to dalej robi -p.
  • CMD — co uruchomić, kiedy ktoś postawi kontener. daemon off mówi nginxowi, żeby nie uciekał w tło, bo pamiętasz: jak główny proces zniknie, kontener się kończy.

Budujemy:

podman build -t strona:1.0 .
STEP 1/7: FROM docker.io/library/ubuntu:24.04
STEP 2/7: LABEL opis="Strona firmowa"
--> 508283a1dcc4
STEP 3/7: RUN apt-get update  && apt-get install -y --no-install-recommends nginx  && rm -rf /var/lib/apt/lists/*
--> 54a1acc54be2
STEP 4/7: COPY strona.conf /etc/nginx/sites-available/default
--> d848dc584599
STEP 5/7: COPY strona/ /var/www/html/
--> 4a9eb917d81d
STEP 6/7: EXPOSE 80
--> 85ac03960b13
STEP 7/7: CMD ["nginx", "-g", "daemon off;"]
COMMIT strona:1.0
--> 8bc0c4265cc1
Successfully tagged localhost/strona:1.0

-t strona:1.0 to nazwa i wersja, czyli tag. Kropka na końcu to folder z przepisem i plikami, tak zwany kontekst budowania. Bez kropki Podman nie wie, skąd wziąć składniki.

Teraz zmień stronę i zbuduj wersję 1.1:

echo '<h1>Sklep z kontenera, wersja 2</h1>' > strona/index.html
podman build -t strona:1.1 .
STEP 3/7: RUN apt-get update  && apt-get install -y --no-install-recommends nginx  && rm -rf /var/lib/apt/lists/*
--> Using cache 54a1acc54be2a40d2922ff51e457db14bedbd153dded3619c9a42ad5d9e154b6
STEP 4/7: COPY strona.conf /etc/nginx/sites-available/default
--> Using cache d848dc584599c22f7993e4f48909eba935e056d5cb64a85dfa793e64c76e3826
STEP 5/7: COPY strona/ /var/www/html/
--> 0c8ed6efaee0

Using cache. Pierwsze budowanie trwało u mnie prawie 30 sekund, drugie mniej niż 10. Podman pamięta wyniki kroków i robi od nowa tylko to, co się zmieniło, oraz wszystko po tym. Jak przepis na lasagne: zmieniłeś ser na wierzchu, więc nie gotujesz od nowa sosu. Dlatego w przepisie rzeczy, które zmieniają się rzadko (instalacja paczek), daje się na górę, a te, które zmieniają się ciągle (Twoje pliki), na dół.

podman images localhost/strona
podman run -d --name sklep -p 8080:80 strona:1.1
curl -s localhost:8080
REPOSITORY        TAG         IMAGE ID      CREATED         SIZE
localhost/strona  1.1         d23abad2373d  3 seconds ago   124 MB
localhost/strona  1.0         8bc0c4265cc1  17 seconds ago  124 MB
<h1>Sklep z kontenera, wersja 2</h1>

Obraz zbudowany lokalnie dostaje przedrostek localhost/. Wersja 1.0 dalej leży obok, więc jak 1.1 się nie uda, wracasz w sekundę.

[Ubuntu] [Docker] Docker domyślnie szuka pliku Dockerfile. Podman najpierw Containerfile, a jak go nie ma, to Dockerfile. W Dockerze wskaż plik wprost: docker build -f Containerfile -t strona:1.0 .

Compose, czyli cała aplikacja w jednym pliku

Sklep potrzebuje bazy. Możesz pisać dwie długie komendy podman run, pamiętać o sieci, woluminie, zmiennych i kolejności. Albo spisać to raz w pliku. Compose to plan osiedla: które kontenery, z jakich obrazów, z jakimi portami i woluminami. Jedna komenda stawia całe osiedle, druga je rozbiera.

Plik compose.yaml w folderze projektu:

services:
  web:
    build:
      context: .
      dockerfile: Containerfile
    image: localhost/strona:1.1
    ports:
      - "8080:80"
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: docker.io/library/mariadb:11
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASS}
      MARIADB_DATABASE: sklep
      MARIADB_USER: sklep
      MARIADB_PASSWORD: ${DB_PASS}
    volumes:
      - dbdane:/var/lib/mysql
    restart: unless-stopped

volumes:
  dbdane:

Wcięcia są tu święte. YAML to format, w którym dwie spacje w złym miejscu zmieniają sens całego pliku. Hasła nie są wpisane na sztywno, tylko jako ${DB_PASS}. Ich wartości leżą obok, w pliku .env, który nie trafia do repozytorium:

printf 'DB_ROOT_PASS=zmien-mnie-1\nDB_PASS=zmien-mnie-2\n' > .env
chmod 600 .env

Zanim coś postawisz, sprawdź, jak narzędzie rozumie Twój plik:

docker compose config

Zobaczysz plik z podstawionymi hasłami i dopisanymi domyślnymi wartościami. Jak zapomnisz o .env, dostaniesz ostrzeżenie:

level=warning msg="The \"DB_PASS\" variable is not set. Defaulting to a blank string."

Stawiamy osiedle:

podman compose up -d
podman compose ps
podman compose logs web
podman exec projekt_web_1 getent hosts db
CONTAINER ID  IMAGE                         COMMAND               CREATED        STATUS        PORTS                 NAMES
59f02e0e5b60  docker.io/library/mariadb:11  mariadbd              5 seconds ago  Up 4 seconds                        projekt_db_1
04e97cf15e67  localhost/strona:1.1          nginx -g daemon o...  3 seconds ago  Up 3 seconds  0.0.0.0:8080->80/tcp  projekt_web_1
10.89.1.2       db.dns.podman

Compose sam założył sieć dla projektu i kontener web znajduje bazę pod nazwą db. W konfiguracji PHP wpisujesz więc jako serwer bazy po prostu db. Rozbiórka:

podman compose down
podman volume ls

down usuwa kontenery i sieć, ale wolumin z bazą zostaje. To celowe: rozbierasz osiedle, ale nie wyrzucasz mebli z magazynu. down -v wyrzuca też woluminy. Tego na produkcji nie wpisuje się przed kawą.

Skąd wziąć compose? podman compose to tylko pośrednik, który woła zewnętrzne narzędzie: podman-compose albo docker-compose.

[Ubuntu] sudo apt install podman-compose albo dla Dockera sudo apt install docker-compose-v2, wtedy piszesz docker compose.

[RHEL] podman-compose jest zwykle w repozytorium EPEL. Red Hat sam zamiast compose promuje Quadlet, o którym za chwilę.

Podman bez roota

Do tej pory wszystko robiliśmy jako root. A kontener uruchomiony przez roota, z którego ktoś się wydostanie, to ktoś z kluczem do całego budynku. Podman potrafi uruchamiać kontenery jako zwykły użytkownik. Bez sudo, bez grupy docker.

Jak to możliwe, że w środku jest root? Wyobraź sobie hotel. W pokoju 312 gość może się ogłosić królem. Ma klucz do minibaru, przestawia meble, rządzi pilotem. Ale królem jest tylko w swoim pokoju. Na korytarzu jest zwykłym gościem z pokoju 312.

su - ania
podman run -d --name spioch docker.io/library/ubuntu sleep 300
podman exec spioch id
podman top spioch user huser pid args
uid=0(root) gid=0(root) groups=0(root)
USER        HUSER       PID         COMMAND
root        1001        1           sleep 300

W kontenerze root. Na serwerze (HUSER) to zwykły numer użytkownika ania. A jeśli proces w kontenerze działa jako inny użytkownik, na serwerze dostaje numer z puli wydzielonej dla Ani:

grep ania /etc/subuid
podman unshare cat /proc/self/uid_map
ania:165536:65536
         0       1001          1
         1     165536      65536

Czytamy: numer 0 w kontenerze to 1001 na serwerze, czyli sama Ania. Numery od 1 w górę to pula 65536 numerów zaczynająca się od 165536. Taką pulę useradd przydziela automatycznie, każdemu nowemu użytkownikowi inną.

Obrazy i kontenery Ani leżą w jej domu, w ~/.local/share/containers, nie w /var/lib/containers. Każdy użytkownik ma własny plac budowy.

Quadlet, czyli kontener jako usługa

Kontener uruchomiony ręcznie nie wstanie sam po restarcie serwera. A przecież mamy kierownika zmiany, systemd, który od tego jest. Quadlet to tłumacz: piszesz krótki plik w stylu systemd, a Quadlet przy przeładowaniu systemd zamienia go na pełną usługę. Red Hat poleca go do nowych konfiguracji zamiast starszego podman generate systemd. Quadlet pojawił się w Podmanie 4.4.

Plik /etc/containers/systemd/strona.container:

[Unit]
Description=Strona firmowa w kontenerze

[Container]
Image=localhost/strona:1.1
ContainerName=strona
PublishPort=8080:80
Volume=/srv/strona/logi:/var/log/nginx:Z

[Service]
Restart=always

[Install]
WantedBy=multi-user.target default.target

Sekcja [Container] to po prostu flagi podman run zapisane w słupku: PublishPort to -p, Volume to -v. Uruchomienie:

sudo systemctl daemon-reload
sudo systemctl start strona
systemctl status strona

Nazwa usługi to nazwa pliku bez .container. Uwaga, pułapka: systemctl enable strona nie zadziała. Usługa jest generowana, a za start przy uruchomieniu systemu odpowiada sekcja [Install] w Twoim pliku.

Co Quadlet z tego robi? Można podejrzeć bez uruchamiania:

/usr/libexec/podman/quadlet -dryrun
---strona.service---
...
ExecStart=/usr/bin/podman run --name=strona --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d -v /srv/strona/logi:/var/log/nginx:Z --publish 8080:80 localhost/strona:1.1

Cała długa komenda, której nie musisz pamiętać.

Wersja bez roota: plik kładziesz w ~/.config/containers/systemd/, a komendy dostają --user:

systemctl --user daemon-reload
systemctl --user start strona
sudo loginctl enable-linger ania

Ostatnia linijka jest ważna. Bez niej usługi Ani startują dopiero, gdy Ania się zaloguje, i giną, gdy się wyloguje. enable-linger mówi: niech jej usługi żyją od startu serwera, nawet gdy jej nie ma. Jak stałe zlecenie w banku.

:Z, czyli etykieta dla SELinux

[RHEL] Na Red Hacie pilnuje SELinux. Pamiętasz: każdy plik ma etykietę, a procesy wchodzą tylko tam, gdzie etykieta się zgadza. Kontener ma etykietę „kontener”. Folder /srv/strona/logi ma etykietę „zwykły folder na serwerze”. Ochroniarz nie wpuści, nawet jeśli klasyczne uprawnienia rwx są w porządku. W środku kontenera widzisz wtedy Permission denied, a w dzienniku audytu odmowę:

sudo ausearch -m avc -ts recent

Rozwiązanie to literka na końcu -v:

  • :z (małe) — etykieta wspólna: z tego folderu może korzystać kilka kontenerów,
  • :Z (duże) — etykieta prywatna: tylko ten jeden kontener.
sudo mkdir -p /srv/strona/logi
ls -Zd /srv/strona/logi
podman run -d --name test -v /srv/strona/logi:/var/log/nginx:Z localhost/strona:1.1
ls -Zd /srv/strona/logi

Po drugim ls -Z typ etykiety zmieni się na container_file_t, z dopisanymi kategoriami, które odróżniają ten kontener od innych. Podman przekleja etykietę za Ciebie.

I teraz najważniejsze ostrzeżenie lekcji. Nigdy nie dawaj :Z na foldery systemowe: /home, /etc, /var, /usr. Podman przeklei etykiety wszystkich plików w środku i inne usługi, na przykład SSH czy logowanie, mogą przestać działać, bo nagle ich pliki mają etykietę kontenera. Dokumentacja Podmana mówi wprost: nie przeklejaj etykiet treści systemowej. Podpinaj własne, dedykowane foldery, na przykład w /srv. Ćwicz na maszynie wirtualnej.

[Ubuntu] Ubuntu używa AppArmor zamiast SELinux, więc :Z nic tam nie psuje, ale też nic nie zmienia. Warto go pisać i tak, żeby ten sam plik działał na obu rodzinach.

Jak to działa pod spodem

podman build to nic magicznego. Dla każdej linijki RUN Podman stawia tymczasowy kontener z dotychczasowego obrazu, wykonuje w nim komendę i zapisuje różnicę jako nową warstwę. Widzisz to w historii obrazu:

podman history localhost/strona:1.1
ID            CREATED         CREATED BY                                     SIZE        COMMENT
0c8ed6efaee0  2 seconds ago   /bin/sh -c #(nop) CMD ["nginx", "-g", "dae...  0B          FROM a753a53c537c
<missing>     3 seconds ago   /bin/sh -c #(nop) EXPOSE 80                    0B          FROM 0c8ed6efaee0
<missing>     5 seconds ago   /bin/sh -c #(nop) COPY dir:23ddc88e7c18830...  3.58kB      FROM d848dc584599
d848dc584599  23 seconds ago  /bin/sh -c #(nop) COPY file:e3bc967d99aedc...  3.58kB      FROM 54a1acc54be2
54a1acc54be2  27 seconds ago  /bin/sh -c apt-get update  && apt-get inst...  24.9MB      FROM 508283a1dcc4

Instalacja nginx to 24.9 MB, nasze pliki to kilka kilobajtów. Dwie wersje obrazu dzielą wszystkie warstwy poza ostatnimi, więc na dysku nie zajmują podwójnie.

Rootless działa dzięki przestrzeni nazw użytkowników (user namespace). Jądro trzyma tabelkę przeliczania numerów, którą widziałeś w uid_map. Programy newuidmap i newgidmap z paczki uidmap [Ubuntu] albo shadow-utils [RHEL] wpisują do niej pule z /etc/subuid i /etc/subgid.

A Quadlet to tak zwany generator systemd. Przy każdym daemon-reload i przy starcie systemu systemd uruchamia generatory, a one w locie produkują pliki usług. Dlatego nie ma czego „włączać”: usługa istnieje tylko dlatego, że istnieje Twój plik .container.

Ściąga flag

KomendaFlagaCo robi
podman build-t NAZWA:TAGnazwa i wersja obrazu
podman build-f PLIKwskazuje plik z przepisem
podman build--no-cachebuduje od zera, bez pamięci kroków
podman historywarstwy obrazu
podman composeup -dstawia wszystkie usługi w tle
podman composedown, down -vrozbiera, z -v także woluminy
podman composeps, logsstan i logi usług
docker composeconfigpokazuje plik po podstawieniu zmiennych
podman run-v SKĄD:DOKĄD:Zprywatna etykieta SELinux
podman run-v SKĄD:DOKĄD:zwspólna etykieta SELinux
podman unsharekomenda w przestrzeni nazw użytkownika
systemctl--userusługi zwykłego użytkownika
loginctlenable-linger UŻYTKOWNIKusługi użytkownika żyją bez logowania

⚠ Typowy błąd

Ania uruchamia kontener bez roota i chce porządnie, na porcie 80:

podman run -d -p 80:80 localhost/strona:1.1

Dostaje mniej więcej taki komunikat:

rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

Porty poniżej 1024 to w Linuksie okienka zarezerwowane dla kierownictwa. Zwykły użytkownik ich nie otworzy, nawet przez kontener. Masz trzy wyjścia. Najprostsze: wysoki port, -p 8080:80, a przed tym reverse proxy z lekcji o Nginx. Drugie: obniżyć granicę, tak jak radzi sam komunikat, wpisem w /etc/sysctl.d/ i sysctl --system. Trzecie: na RHEL-u przekierowanie portu w firewalld. Czego nie robimy? Nie uruchamiamy wszystkiego z powrotem jako root, bo „tak szybciej”.

✎ Zadanie

  1. Napisz Containerfile z własną stroną i zbuduj obraz moja-strona:1.0. Zmień stronę, zbuduj 1.1 i znajdź w wyniku linijki Using cache.
  2. Jako zwykły użytkownik uruchom ten obraz na porcie 8080 i sprawdź podman top z kolumnami user i huser.
  3. Napisz plik Quadlet w ~/.config/containers/systemd/, uruchom kontener jako usługę użytkownika, włącz linger i sprawdź po restarcie maszyny, że strona odpowiada.
Pokaż rozwiązanie

Przepis, ten sam co w lekcji, tylko z Twoją stroną:

mkdir -p ~/moja && cd ~/moja
mkdir strona && echo '<h1>Moja strona 1.0</h1>' > strona/index.html
# Containerfile i strona.conf jak w odcinku
podman build -t moja-strona:1.0 .
echo '<h1>Moja strona 1.1</h1>' > strona/index.html
podman build -t moja-strona:1.1 . | grep 'Using cache'
--> Using cache 508283a1dcc4191b2c732d52e9f8f1923e31ba95b523e4b43f71a53defa015f5
--> Using cache 54a1acc54be2a40d2922ff51e457db14bedbd153dded3619c9a42ad5d9e154b6
--> Using cache d848dc584599c22f7993e4f48909eba935e056d5cb64a85dfa793e64c76e3826

Trzy kroki z pamięci, czyli wszystko przed kopiowaniem strony. Zadanie zrobione tak, jak było trzeba: jako zwykły użytkownik, bez sudo. Obraz zbudowany przez Anię leży u Ani. Root go nie widzi, i odwrotnie.

podman run -d --name moja -p 8080:80 localhost/moja-strona:1.1
podman top moja user huser args
USER        HUSER       COMMAND
root        1001        nginx: master process nginx -g daemon off;
www-data    165568      nginx: worker process

Root w kontenerze to Ania na serwerze. www-data w kontenerze to numer z puli Ani. Teraz Quadlet, plik ~/.config/containers/systemd/moja.container:

[Unit]
Description=Moja strona w kontenerze

[Container]
Image=localhost/moja-strona:1.1
ContainerName=moja
PublishPort=8080:80

[Service]
Restart=always

[Install]
WantedBy=default.target
podman rm -f moja
/usr/libexec/podman/quadlet -dryrun -user | grep -E 'ExecStart|WantedBy'
systemctl --user daemon-reload
systemctl --user start moja
sudo loginctl enable-linger ania
ExecStart=/usr/bin/podman run --name=moja --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --publish 8080:80 localhost/moja-strona:1.1
WantedBy=default.target

Najpierw usuwamy ręcznie postawiony kontener, bo dwa kontenery nie zmieszczą się na jednym porcie. Po restarcie maszyny curl localhost:8080 odpowiada, a Ania nawet się nie zalogowała. Kontener pracuje, kiedy właścicielka śpi. Marzenie każdego pracodawcy.

Co dalej

Kontener korzysta z jądra serwera. A co, jeśli potrzebujesz zupełnie innego systemu, własnego jądra albo Windowsa obok Linuksa? W następnej lekcji stawiamy prawdziwe maszyny wirtualne na KVM: virt-install, virsh, dyski qemu-img i automatyczna konfiguracja przez cloud-init.