## Zadanie 1 ![Screenshot from 2025-10-18 21-17-14](https://hackmd.io/_uploads/SJEX_PWCxl.png) ### Docker-compose Służy do definiowania i odpalania wielu kontenerów dockerowych równocześnie Kluczowe punkty: * mamy plik analogiczny do `Dockerfile`, który nazywa się `docker-compose.yml`, który definiuje potrzebne własności (usługi, sieci, wolumeny itd.) * Uruchamia wszystsko przy pomocy jednej komendy * Domyślnie tworzy wewnętrzną sieć, do której są podłączone wszystkie serwisy, umożliwiając łatwą komunikację Docker Compose is a tool for running multi-container applications on Docker defined using the Compose file format. A Compose file is used to define how one or more containers that make up your application are configured. Once you have a Compose file, you can create and start your application with a single command: docker compose up. ### Live demo ```docker-compose services: web: ports: - "8081-8083:80" image: 'nginx:latest' deploy: replicas: 3 ``` Odpalenie (w trybie `detach`) `docker compose up -d` Sprawdzenie `docker ps` ``` CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0ad7a00557e0 nginx:latest "/docker-entrypoint.…" 2 seconds ago Up 2 seconds 0.0.0.0:8082->80/tcp, [::]:8082->80/tcp zad1-web2-1 d7349a239a87 nginx:latest "/docker-entrypoint.…" 2 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp, [::]:8081->80/tcp zad1-web1-1 bff07d29efe2 nginx:latest "/docker-entrypoint.…" 2 seconds ago Up 2 seconds 0.0.0.0:8083->80/tcp, [::]:8083->80/tcp zad1-web3-1 ``` Wstrzymanie (zobacz, że dalej widoczne w `docker ps -a`) `docker compose stop` Wyłączenie wszystkich i posprzątanie `docker compose down` ### Live demo wersja pro `docker-compose.yml` ```dockerfile= services: web: ports: - "8081-8083:80" image: 'nginx:latest' volumes: - ./entrypoint.sh:/docker-entrypoint.sh deploy: replicas: 3 command: /docker-entrypoint.sh app: image: alpine:latest command: sh -c "apk update && apk add curl && sleep infinity" container_name: app ``` `entrypoint.sh` ```bash= #!/bin/sh set -eu SERVICE_NAME=$(hostname) cat > /usr/share/nginx/html/index.html <<EOF <!DOCTYPE html> <html> <head><title>Docker Compose Test</title></head> <body> <h1>Hello from ${SERVICE_NAME}!</h1> <p>This is container running Nginx/Alpine.</p> </body> </html> EOF exec nginx -g "daemon off;" ``` `nginx.conf` ```nginx= worker_processes 1; daemon off; # Utrzymuje Nginx na pierwszym planie, wymagane w Dockerze events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; server { listen 80; server_name localhost; # Ustaw zmienną Nginx na wartość zmiennej środowiskowej # (Wymaga użycia "env" w pliku docker-compose) set $service_name "DEFAULT_WEB"; location / { # Zwracanie prostej odpowiedzi z wbudowaną zmienną ze skryptu entrypoint return 200 '<h1>Hello from $SERVICE_NAME!</h1>'; add_header Content-Type text/html; } } } ``` I teraz możemy odpalić ```bash= chmod +x entrypoint.sh docker compose up -d docker exec -it app sh curl http://web # I tak kilka razy żeby pokazać, że będą różne hostname'y odpowiadać ``` I ładnie widać, że mamy odpowiedzi z tych serwisów:D ## Zadanie 2 ![Screenshot from 2025-10-18 21-17-23](https://hackmd.io/_uploads/HJN7_PZAxx.png) Przygotowanie: 1. echo "Tajna flaga" >> /home/flag.txt 2. chmod 0400 /home/flag.txt 3. chown root:root /home/flag.txt 4. `docker run -it --rm -v <path_to_flag>:/mnt/host ubuntu bash` * `docker run -it --rm -v /home:/mnt/host ubuntu bash` Gdzie * `ubuntu` to obraz (możemy korzystać z ubuntu:22.04, latest albo alpine) * `-v` (volume), mapujemy `<path_to_flag>` na `/mnt/host` w naszym obrazie 5. Teraz to co możemy zrobić to po prostu `cat /mnt/host/flag.txt` i mamy flagę :D ## Zadanie 3 ![Screenshot from 2025-10-18 21-17-33](https://hackmd.io/_uploads/ByxVXOvbRgx.png) ### Komendy ```bash= docker build -f Dockerfile-x -t l3za:x . docker build -f Dockerfile-y -t l3za:y . docker run -v l3za:/root/katalog -it --rm l3za:x docker run -v l3za:/root/katalog -it --rm l3za:x docker run -v l3za:/root/katalog -it --rm l3za:x docker run -v l3za:/root/katalog -it --rm l3za:y docker run -v l3za:/root/katalog -it --rm l3za:y docker run -v l3za:/root/katalog -it --rm l3za:x ``` ### Co się stało? Volumeny są `persistant`, czyli są niezależne od dockera. To co się pewnie dzieje to `:x` nic nie robi, a ten `:y` psuje. Ale pamiętajmy, że volumen jest taki sam, więc ten, który tylko próbuje przeczytać dalej ma problem:D Jeśli nie mamy volumenu, to docker tworzy nowy, o tej nazwie, stąd w przypadku komend wyżej tworzy `l3za`. Z ciekawostek, volumeny można wyświetlić i usunąć ```bash= docker volume ls docker volume rm <img> ``` Jak naprawić? Można sobie hasze sprawdzać i jeśli inny (to znaczy, że zmieniony), trzeba ręcznie usunąć i wtedy jak odpalimy po raz kolejny, to docker stworzy nowy i będzie śmigać ## Zadanie 4 ![Screenshot from 2025-10-18 21-17-46](https://hackmd.io/_uploads/ByE7OPZ0eg.png) ### Sprawdzenie Domyślnie systemy cgroups są montowane w katalogu `/sys/fs/cgroup` Można sprawdzić np. ```bash= $ mount | grep cgroup cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot) ```` i wtedy w zależności od wersji mam cgroup1/2 ### Sprawdzenie Nie mam `procfs`, ale można też sprawdzić inaczej. Cała historia ```bash= $ ps PID TTY TIME CMD 5519 pts/0 00:00:00 bash $ cat /proc/5519/cgroup 0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-595ab7eb-d2ce-4ea2-bd73-f977199b9491.scope ``` I sprawdzenie ograniczeń to czytanie z plików /sys/fs/cgroups/<ścieżka z poprzedniego zadania> Gdzie <ścieżka z poprzedniego zadania> to wszystsko po `::` z poprzedniego ```bash= $ cd /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-595ab7eb-d2ce-4ea2-bd73-f977199b9491.scope $ cat memory.peak ``` ### Utworzenie własnego cgroup najpierw sprawdźmy czy w ogóle mamy kontrolery odpalone ```bash= cat /sys/fs/cgroup/cgroup.subtree_control ``` Chcemy mieć "cpu memory pids" jeśli nie mamy to ```bash= echo "+cpu +memory +pids" > cgroup.subtree_control ``` Tworzenie grupy: ```bash= mkdir moja_grupa ``` Ustawianie jej parametrów: ```bash= echo "209715200" > moja_grupa/memory.max echo "50000 100000" > moja_grupa/cpu.max echo "10" > moja_grupa/pids.max ``` możemy stworzyć proces ```bash sleep 3600 & PID=<pid sleepa> echo $PID > moja_grupa/cgroup.procs ``` Sanity check: ```bash cat /proc/$PID/cgroup ``` weźmy na obraz cpu.stat ```bash cat /sys/fs/cgroup/moja_grupa/cpu.stat ``` ``` usage_usec 15023 # Całkowity czas CPU (w mikrosekundach) zużyty przez procesy w tej grupie. user_usec 8010 # Czas CPU zużyty w trybie użytkownika. system_usec 7013 # Czas CPU zużyty w trybie jądra (systemu). nr_periods 0 # Liczba okresów (zgodnych z cpu.max), które minęły. nr_throttled 0 # Ile razy procesy w tej grupie zostały "zdławione" (throttled), bo przekroczyły limit. throttled_usec 0 # Całkowity czas (w mikrosekundach), przez który procesy były dławione. ``` ## Zadanie 5 ![Screenshot from 2025-10-18 21-17-55](https://hackmd.io/_uploads/BkNQdDWCle.png) Kontroler to specjalny ziomek który monitoruje ile dane procesy zżerają zasobów i potrafi kontrolować te procesy (spowalniać je, zabijać) Z ciekawszych ograniczeń to: - `cpu.weight`, pozwala ustalić z jaką wagą dane procesy mają zabierać CPU (dwa procesy jeden 100 (33%), drugi waga 200 (66%)) - `memory.high`, jakieś która nie daje ci hard limitu tylko soft. Procesy nie są zabijane ale ich wydajność spada https://man7.org/linux/man-pages/man7/cgroups.7.html **Kontrolery** - inaczej subsystemy, modyfikują zachowanie procesów w danej **cgroup**ie Ciekawe kontrolery: <!-- 1. cpu - gwarantuje, ile CPU będzie dana cgroupa mogła używać (jeśli więcej jest "wolne" to może używać więcej) 3. cpuset - przypisuje procesy z cgroupy do konkretnych CPU 5. memory - pozwala na limitowanie użycia pamięci procesu, pamięci jądra i swapu --> 1. **devices** - umożliwia kontrolowanie, kto może tworzyć i otwierać na czytanie/pisanie różne urządzenia * fajne bo można lepiej izolować procesy 2. **freezer** - pozwala na zatrzymywanie / wznawianie wszystkich procesów z cgroupy * można czasowo zatrzymać jakieś procesy, np jeśli są istotnie obciążające i w danym momencie to bardzo przeszkadza 3. **net_cls** - dodaje **classid** do pakietów tworzonych przed procesy z danej cgroupy, może być to potem używane przez firewall * np. wszystko z danej cgroupy musi zostać w podsieci i nie wychodzi na zewnątrz) * można pakiety wychodzące z tej sgroupy poddać dodatkowej analizie -> bezpieczeństwo czy coś * dotyczy tylko pakietów **wychodzących**, nie przychodzących <!-- 10. blkio 11. perf_event --> 4. **net_prio** - pozwala na dodanie priorytetów dla cgroup do konkretnego interfejsu sieciowego * dzięki temu, jeśli mamy cgroupę z procesami, które potrzebują jak najszybszy internet możemy je jako bardziej priorytetowe ustawić * generalnie Quality of Service <!-- 13. hugetlb 14. pids 15. rdma --> ### Różnice: <!-- ![Screenshot_20251022_234211](https://hackmd.io/_uploads/rJSa1AURex.png) --> Chodzi o to że w katalogu głównym mamy WSZYSTKIE kontrolery Natomiast w podkatalogach (np ten user-100.slice) mamy ograniczony dostęp i tylko do tych mamy dostęp... `cat /sys/fs/cgroup/cgroup.controllers`: ``` cpuset cpu io memory hugetlb pids rdma misc dmem ``` `cat /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers`: ``` cpu memory pids ``` Różnica wynika z tego, jakie kontrolery są dla kogo dostępne - zwykły użytkownik (user-1000) nie ma możliwości korzystania z kontrolerów cpuset, io, ... Dlaczego tak a nie inaczej? Generalnie korzystanie z pozostałych mogłoby istotnie zaburzyć: * pracę innych procesów (cpuset, io) * to, do jakiej pamięci jest dostęp (hugetlb, rdma) Generalnie nie chcemy użytkownikowi zwykłemu pozwalać na za duże kombinowanie. ## Zadanie 6 ![Screenshot from 2025-10-18 21-18-07](https://hackmd.io/_uploads/S1EX_PZAee.png) eee to inna wersja grupowania procesów tylko że przez systemd (? plox fact check me) Jak utworzyć własny slice? ```bash nano /etc/systemd/system/mojWaznySlice.slice ``` ```toml [Unit] Description=Moj wlasny slice do ograniczania zasobow # Ten slice nie musi byc uruchamiany recznie, # wystartuje automatycznie, gdy pierwszy proces zostanie w nim umieszczony. [Slice] # Tu moglibysmy zagniezdzic go w innym slice, ale zostawiamy go w glownym. [ResourceControl] # Ograniczenie pamieci RAM do 500 Megabajtow MemoryMax=500M # Ograniczenie CPU do 30% mocy JEDNEGO rdzenia. # (Jesli chcesz 30% CALKOWITEJ mocy np. 4 rdzeni, uzyj CPUWeight=... # lub ustaw CPUQuota=120% dla 1.2 rdzenia) CPUQuota=30% ``` Teraz tylko zostało odpalić dziada ```bash systemctl daemon-reload systemctl start mojWaznySlice.slice ``` Gemini ratuje i można tak odpalić ładnie procesy ```bash= systemd-run --unit=proces1 --slice=mojWaznySlice.slice sleep 3600 systemd-run --unit=proces2 --slice=mojWaznySlice.slice top ``` `systemd-cgls` pokazuje strukturę drzewiastą obecnych cgroup i sliceów ![Screenshot_20251022_235312](https://hackmd.io/_uploads/HJHUM08Rel.png) `systemd-cgtop` htop ale na cgroupach ![Screenshot_20251022_235136](https://hackmd.io/_uploads/SJOeGC8Rxg.png) ## Zadanie 7 ![Screenshot from 2025-10-18 21-18-14](https://hackmd.io/_uploads/rkE7uDZ0gx.png) pozdrawiam @Grzegorz dobry devops ### Całe demo ```bash # Przygotowanie docker run --rm -it --name test alpine /bin/sh # W nowej karcie PID=$(docker top test | awk 'NR>1 {print $2}') CGROUP_PATH=$(cat /proc/$PID/cgroup | cut -d: -f3) cat /sys/fs/cgroup/$CGROUP_PATH/cpu.weight cat /sys/fs/cgroup/$CGROUP_PATH/memory.max # Update docker update --cpu-shares 512 --memory 10M --memory-swap 10M test # Weryfikacja cat /sys/fs/cgroup/$CGROUP_PATH/cpu.weight cat /sys/fs/cgroup/$CGROUP_PATH/memory.max # W kontenerze dd if=/dev/zero of=/tmp/testfile bs=15M count=250 ``` ### Cgroup driver Cgroup driver oznacza jak docker się komunikuje z systemem by zarządzać zasobami (albo to jest systemd albo cgroupfs) ### Twardy limit demo Jeśli chodzi o twardy limit to można to zrobić tak. ``` docker run -it --rm --name dd-killer --memory=10m --memory-swap=10m alpine /bin/sh ``` i w środku odpalić ``` dd if=/dev/zero of=/tmp/testfile bs=15M count=250 ``` ### Twardy limit vs soft Jak przekroczymy twardy to jest oom i zabijamy proces. Jeśli przekroczymy soft to po prostu tracimy priorytet i dużo na wydajności tracimy. W jaki sposób są aplikowane? Na przykład hard w memory jest w `memory.max`, a soft jest w `memory.low` ### Limity pamięci * `--memory-reservation` - miękki limit, kernel stara się go trzymać, ale nie zabija procesu, jeśli zostaje przekroczony * `--memory` - twardy limit, jak przekroczymy to kernel zabija proces Fun fact: * kernel nie zawsze zabija proces, jeśli poprosimy o więcej niż przydzielona nam pamięć - potrafi obiecać ją nam i zabić proces dopiero, jak rzeczywiście tyle chcemy użyć? * co to zmienia? nie tak łatwo zauważyć to zabijanie bo musimy serio wykorzystać tę pamięć nie tylko ją chcieć mieć ### W jaki sposób aplikowane są te limity? systemd zapisuje odpowiednie rzeczy do cgroupów, kernel zajmuje się dbaniem o przestrzeganie tego ## Zadanie 8 ![Screenshot from 2025-10-18 21-18-23](https://hackmd.io/_uploads/BJ77OvZ0xg.png) https://docs.docker.com/build/bake/ Przykład: Zamiast: ```bash docker build -f Dockerfile -t myapp:latest . ``` Można: docker-bake.hcl ``` target "myapp" { context = "." dockerfile = "Dockerfile" tags = ["myapp:latest"] } ``` ```bash docker buildx bake myapp ``` Przydatne jeśli do buildowania używamy dużo flag, generalnie jest to bardziej skomplikowane niż tylko nazwa czy coś, np: ```bash docker build \ -f Dockerfile \ -t myapp:latest \ --build-arg foo=bar \ --no-cache \ --platform linux/amd64,linux/arm64 \ . ``` Bo bądźmy szczerzy - tego się nie zapamięta i łatwo się pomylić a tak to jest to gotowe i cyk wszystko pięknie kolorowo Podsumowanie: W skrócie docker compose jest to odpalania a docker bake jest do budowania UWAGA! Wczesniej trzeba, zeby dzialalo multi-platform build: ```bash docker buildx create --name multi --driver docker-container --use docker buildx inspect --bootstrap ``` ```bash docker buildx bake app-dev --set app-dev.output=type=local,dest=./out docker buildx bake app-prod --set app-prod.output=type=local,dest=./out2 ``` Dockerfile: ```Dockerfile FROM alpine:latest # Przekaż argument czasu budowania do obrazu ARG APP_VERSION="unknown" ENV VERSION=$APP_VERSION RUN echo "Budowanie obrazu w wersji: $VERSION" > /app-info.txt RUN echo "To jest prosty obraz testowy." >> /app-info.txt CMD ["cat", "/app-info.txt"] ``` docker-bake.hcl: ```hcl # Definiujemy grupę "default". # Jeśli wywołamy `docker buildx bake` bez podania celu, # zbudowane zostaną wszystkie cele z tej grupy. group "default" { targets = ["app-dev"] } # --- CEL 1: Wersja deweloperska --- # Prosty cel do budowania lokalnego target "app-dev" { # Kontekst budowania (katalog z Dockerfile) context = "." # Nazwa pliku Dockerfile dockerfile = "Dockerfile" # Tagi, jakie nadamy obrazowi tags = ["my-simple-app:dev"] # Argumenty budowania args = { APP_VERSION = "dev-0.1" } # Co zrobić z wynikiem: "local" ładuje obraz # do lokalnego daemona Dockera. output = ["type=local"] } # --- CEL 2: Wersja produkcyjna (Multi-platform) --- # Cel do budowania na wiele architektur i wypchnięcia do rejestru target "app-prod" { # Dziedziczymy wszystko z "app-dev" (kontekst, dockerfile) inherits = ["app-dev"] # Nadpisujemy tagi tags = ["my-registry/my-simple-app:1.0", "my-registry/my-simple-app:latest"] # Nadpisujemy argumenty args = { APP_VERSION = "1.0.0-prod" } # Definiujemy platformy platforms = ["linux/amd64", "linux/arm64"] # Co zrobić z wynikiem: "registry" automatycznie # wypycha obraz do rejestru (np. Docker Hub). # UWAGA: To wymaga wcześniejszego `docker login my-registry` output = ["type=registry"] } ``` <!-- GEMINI wysrało to --> <!-- ale to trzeba zcheckować -->