Kontenery, część 2
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.htmlKonfiguracja 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 offmó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/
--> 0c8ed6efaee0Using 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:8080REPOSITORY 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 .envZanim coś postawisz, sprawdź, jak narzędzie rozumie Twój plik:
docker compose configZobaczysz 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 dbCONTAINER 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.podmanCompose 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 lsdown 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 argsuid=0(root) gid=0(root) groups=0(root)
USER HUSER PID COMMAND
root 1001 1 sleep 300W 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_mapania:165536:65536
0 1001 1
1 165536 65536Czytamy: 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.targetSekcja [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 stronaNazwa 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.1Cał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 aniaOstatnia 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 recentRozwią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/logiPo 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.1ID 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 508283a1dcc4Instalacja 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
| Komenda | Flaga | Co robi |
|---|---|---|
podman build | -t NAZWA:TAG | nazwa i wersja obrazu |
podman build | -f PLIK | wskazuje plik z przepisem |
podman build | --no-cache | buduje od zera, bez pamięci kroków |
podman history | warstwy obrazu | |
podman compose | up -d | stawia wszystkie usługi w tle |
podman compose | down, down -v | rozbiera, z -v także woluminy |
podman compose | ps, logs | stan i logi usług |
docker compose | config | pokazuje plik po podstawieniu zmiennych |
podman run | -v SKĄD:DOKĄD:Z | prywatna etykieta SELinux |
podman run | -v SKĄD:DOKĄD:z | wspólna etykieta SELinux |
podman unshare | komenda w przestrzeni nazw użytkownika | |
systemctl | --user | usługi zwykłego użytkownika |
loginctl | enable-linger UŻYTKOWNIK | usł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.1Dostaje 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 deniedPorty 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
- Napisz
Containerfilez własną stroną i zbuduj obrazmoja-strona:1.0. Zmień stronę, zbuduj1.1i znajdź w wyniku linijkiUsing cache. - Jako zwykły użytkownik uruchom ten obraz na porcie 8080 i sprawdź
podman topz kolumnamiuserihuser. - 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 d848dc584599c22f7993e4f48909eba935e056d5cb64a85dfa793e64c76e3826Trzy 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 argsUSER HUSER COMMAND
root 1001 nginx: master process nginx -g daemon off;
www-data 165568 nginx: worker processRoot 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.targetpodman 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 aniaExecStart=/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.targetNajpierw 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.