LW IT Solutions
« Przegląd bloga /Raspberry Pi/Tutorials / Tutorial: Utwardzanie pliku Docker Compose do pracy...
Ten artykuł w innych językach:

Tutorial: Utwardzanie pliku Docker Compose do pracy bez nadzoru na Raspberry Pi

Tutorial: Utwardzanie pliku Docker Compose do pracy bez nadzoru na Raspberry Pi
Spis treści
  1. Linia rozstrzygająca o powrocie
  2. Badanie kondycji, które coś bada
  3. Dzienniki, które nie zapełnią karty
  4. Limity pamięci i pułapka właściwa dla Pi
  5. Przypięcie obrazu, by restart nie był aktualizacją
  6. Sprawdzenie pliku przed uruchomieniem
  7. Pytania i odpowiedzi
  8. Źródła

Plik Compose uruchamiający stos pisze się w dziesięć minut. Plik Compose utrzymujący ten stos przy życiu przez rok potrzebuje około sześciu linii więcej na usługę – a każda z tych linii istnieje z powodu określonego sposobu, w jaki rzeczy się przewracają.

Poniżej przejście po nich w kolejności, w jakiej zwykle stają się potrzebne – co mniej więcej odpowiada kolejności, w jakiej natrafia na nie maszyna pozostawiona sama sobie.

Pięć awarii w ciągu dziewięćdziesięciu dni na szynie, każda z jedną linią Compose, która by jej zapobiegła, obok gotowy utwardzony opis usługi
Żaden z tych pięciu przypadków nie wymaga rzadkiej okoliczności. To wynik dziewięćdziesięciu dni pracy bez nadzoru.

Linia rozstrzygająca o powrocie

Kontener, który się zatrzymał, pozostaje zatrzymany. Po przerwie w zasilaniu, po awarii, po restarcie usługi – dopóki żadna reguła restartu nie mówi inaczej.

services:
  app:
    restart: unless-stopped

Trzy wartości warto rozróżnić. no jest domyślne i oznacza, że nic nie wraca. always uruchamia kontener ponownie w każdym przypadku, także po zatrzymaniu go ręcznie i późniejszym restarcie maszyny – i tak kontener celowo wyłączony z użycia pojawia się o trzeciej nad ranem. unless-stopped robi to samo z wyjątkiem właśnie tego przypadku i jest właściwym ustawieniem domyślnym dla maszyny, której nikt nie obserwuje.

Dwa szczegóły stojące za tym. Docker odczekuje między próbami restartu coraz dłużej, podwajając od 100 milisekund do minuty – kontener w pętli awarii nie kładzie więc maszyny, ale też nie przestaje próbować, a nic na to nie wskazuje. I reguła restartu działa wyłącznie przy kontenerze, który się kończy. Proces zawieszony przy działającym kontenerze to dokładnie przypadek następnego rozdziału.

Badanie kondycji, które coś bada

Zawieszona aplikacja jest gorsza od takiej, która padła, bo wszystko wokół uważa ją za działającą. Kontener działa, port jest otwarty, a żądania idą w próżnię.

    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s

Ostatniej linii brakuje najczęściej i to ona wywołuje najwięcej zamieszania. W czasie start_period nieudane badanie nie liczy się do retries. Bez niej usługa potrzebująca na wstanie więcej niż trzech interwałów badania, tu około 90 sekund, zostaje po drodze uznana za chorą, a usługi czekające na nią przez condition: service_healthy nie startują. Chory kontener nie zostaje przy tym zrestartowany: bez Swarm wynik badania zmienia tylko jego stan, a reguła restartu i tak zadziała dopiero wtedy, gdy kontener zakończy działanie.

To, co badanie bada, waży więcej niż samo jego istnienie. Żądanie na / dowodzi, że serwer WWW odpowiada, a robi to również wtedy, gdy stojące za nim połączenie z bazą zniknęło godzinę temu. Punkt końcowy dotykający części podatnych na awarię warto napisać właśnie w tym celu – i powinien odpowiadać szybko, bo biegnie co trzydzieści sekund przez cały czas pracy maszyny.

Badanie kondycji umożliwia ponadto wypowiedź o kolejności, której sam depends_on nie stawia. Bez warunku Compose uruchamia zależność i natychmiast idzie dalej; z warunkiem czeka.

  app:
    depends_on:
      db:
        condition: service_healthy

Skutek dla szczupłych obrazów: curl często nie jest zawarty, a badanie, którego nie da się wykonać, liczy się jako nieudane. Albo badanie używa czegoś, co w obrazie jest – wget -q --spider albo polecenia samej aplikacji – albo obraz dostaje pakiet, którego inaczej by nie potrzebował.

Dzienniki, które nie zapełnią karty

Domyślny sterownik dzienników pisze do pliku JSON rosnącego bez ograniczeń. Na serwerze z dużym dyskiem jest to nieporządek; na Pi z kartą pamięci jest to przyczyna awarii, bo pełny system plików kładzie całą maszynę, a nie tylko gadatliwy kontener.

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Trzydzieści megabajtów na usługę wystarcza z zapasem dla wszystkiego, co nie jest właśnie badane. Ustawienie należy do każdej usługi, a nie tylko do tej gadatliwej, bo gadatliwą bywa zwykle ta usługa, która kiedyś zaczyna wypisywać ślad błędu co sekundę.

Ten sam limit da się ustawić raz dla całej maszyny w /etc/docker/daemon.json, i to lepsze dla niego miejsce – obowiązuje jednak tylko dla kontenerów utworzonych później, istniejący stos trzeba więc utworzyć na nowo, aby zadziałał.

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Tam, gdzie problemem jest nie miejsce, lecz same zapisy, karta pamięci zyskuje na wyprowadzeniu dzienników poza system plików – driver: journald przekazuje je do systemd, któremu da się kazać trzymać je w pamięci operacyjnej.

Limity pamięci i pułapka właściwa dla Pi

Maszyna z czterema gigabajtami i bez pliku wymiany nie ma zapasu na wyciek. To, który proces ginie przy braku pamięci, rozstrzyga jądro – i regularnie nie jest to proces winny.

    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.5"

Limit zamienia awarię całej maszyny w awarię jednego kontenera: winny kontener zostaje zakończony i uruchomiony ponownie przez własną regułę restartu, a reszta działa dalej. To wynik znacznie lepszy, a limit opłaca się także wtedy, gdy liczba jest zgadywana – hojny limit wciąż ogranicza szkodę.

A teraz część dotycząca wyłącznie Raspberry Pi. W Raspberry Pi OS grupa kontrolna pamięci jest domyślnie wyłączona, a limit pamięci w pliku Compose zostaje wtedy przyjęty bez skargi i pozostaje bez skutku. Ostrzeżenie pojawia się raz przy starcie usługi i nigdy więcej.

# dopisac do jedynej linii w /boot/firmware/cmdline.txt, potem restart
cgroup_enable=memory cgroup_memory=1

# potem ta linia musi zniknac:
docker info 2>&1 | grep -i "no memory limit support"

Plik zawiera dokładnie jedną linię, a znak końca linii dopisany przez edytor czyni maszynę niezdolną do startu – dopisek należy więc na koniec istniejącej linii, oddzielony spacją.

Przypięcie obrazu, by restart nie był aktualizacją

Tag jest wędrującym wskaźnikiem. postgres:16 oznacza w tym miesiącu co innego niż w poprzednim, a chwili zmiany nikt nie wybiera – jest nią moment, w którym kontener przypadkiem powstaje na nowo.

    image: postgres:16.4@sha256:f8cd4e0b9b0d15b6bbbcbbdf8c5f8c8b6c6b3e9b7cbb4f2d1e3a5c7d9f0b2a46

Digest jest niezmienny, przez co z „ponownego utworzenia kontenera” staje się czynność o znanym wyniku. Tag zostaje przed nim jako dokumentacja – sam digest jest nieczytelny, a plik pełen sum kontrolnych nikomu nie mówi, co działa.

Digest używanego już obrazu jest o jedno polecenie stąd.

docker image inspect postgres:16.4 --format '{{index .RepoDigests 0}}'

Ostrzeżenie co do połączenia z automatycznymi aktualizacjami. Narzędzie planowo pobierające nowe obrazy i przypięty digest to przeciwieństwa – jedno i drugie naraz oznacza, że digest po cichu wygrywa, podczas gdy narzędzie aktualizujące melduje sukces. Albo wersje wędrują według planu, albo wędrują, gdy ktoś tak postanowi; oba rozwiązania da się obronić, posiadanie jednego z nich przez przypadek już nie.

Do tego samego bloku należą jeszcze dwie linie i nic nie kosztują. Kontener nigdy niepiszący do własnego systemu plików może to powiedzieć, a temu, który nie musi stawać się użytkownikiem uprzywilejowanym, da się oznajmić, że nigdy nim nie będzie.

    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    user: "1000:1000"

Sprawdzenie pliku przed uruchomieniem

Compose rozwija zmienne, scala pliki uzupełniające i wstawia wartości domyślne – a wynik często nie jest tym, co plik zdaje się mówić. Jedno polecenie wypisuje to, co faktycznie zostanie uruchomione.

docker compose config

W tym wyniku warto szukać trzech rzeczy. Pustej wartości tam, gdzie powinna stać zmienna – nieustawiona zmienna staje się bez komunikatu pustym ciągiem znaków, a brakujące hasło czyta się często jako brak hasła. Obrazu bez digestu. Oraz usługi bez restart, bez healthcheck albo bez logging, co oko w pliku z ośmioma usługami łatwo przeoczy.

To ostatnie da się sprawdzić maszynowo, i to się opłaca, bo jest to dokładnie ten rodzaj pominięcia, który powstaje przy szybkim dodaniu usługi.

docker compose config --format json | python3 -c '
import json, sys
stack = json.load(sys.stdin)
for name, svc in stack["services"].items():
    fehlt = [f for f in ("restart", "healthcheck", "logging") if f not in svc]
    if "@sha256:" not in svc.get("image", ""):
        fehlt.append("digest")
    print(f"{name:20s} {\", \".join(fehlt) if fehlt else \"ok\"}")
'

Po pierwszym przebiegu do rutyny należy ostatnie sprawdzenie i nie potrzebuje żadnego narzędzia: celowo zrestartować maszynę i zobaczyć, co wraca. Stos, który nigdy nie przeżył restartu, nie jest sprawdzony – a poranek po przerwie w zasilaniu nie jest porą, by się o tym dowiadywać.

Pytania i odpowiedzi

Po czym poznać pętlę awarii, skoro Docker sam niczego nie zgłasza?

Po danych, które Docker i tak prowadzi, tylko sam ich nie zgłasza. Wystarczą trzy miejsca:

  • Kolumna stanu w docker ps. Kontener w pętli figuruje tam jako „Restarting” albo z czasem działania wynoszącym kilka sekund, choć maszyna pracuje od kilku dni.
  • Licznik restartów. docker inspect --format '{{.RestartCount}}' <kontener> podaje, ile razy reguła restartu już zadziałała. Wartość rosnąca między dwoma odczytami jest jednoznacznym sygnałem.
  • Stan kondycji. Przy badaniu kondycji ta sama kolumna pokazuje dodatkowo „healthy” albo „unhealthy”. Kontener, który działa, a jest oznaczony jako unhealthy, to przypadek zawieszenia opisany w artykule.

Niewielki skrypt, który regularnie odczytuje te wartości i przy zmianie wysyła wiadomość, zamienia cichą pętlę w zgłoszoną.

Co przepada, gdy journald trzyma dzienniki wyłącznie w pamięci operacyjnej?

Wszystko, co zapisano przed ostatnim restartem, czyli akurat te komunikaty, które mogłyby wyjaśnić awarię albo przerwę w zasilaniu. Przy Storage=volatile dziennik leży w /run/log/journal i nie przetrwa restartu; kto chce oszczędzać kartę, a mimo to móc później zajrzeć do dzienników, powinien dodatkowo wysyłać je na drugą maszynę.

Który digest powinien trafić do pliku, gdy obraz jest zbudowany dla kilku architektur?

Digest indeksu, czyli listy wskazującej wersje dla poszczególnych architektur. Podaje go polecenie z artykułu, jeśli obraz pobrano wcześniej po tagu, a dzięki niemu każda maszyna nadal dostaje wersję, która do niej pasuje: Pi tę dla ARM, komputer testowy tę dla x86.

Pułapka tkwi w interfejsie webowym rejestru. Obok indeksu pokazuje on często także digesty poszczególnych wersji, a skopiowanie stamtąd wartości dla amd64 przypina Pi do obrazu dla obcej architektury. Docker i tak go pobiera i uruchamia, wyświetlając jedynie ostrzeżenie o niezgodnej platformie; bez zainstalowanej emulacji proces w kontenerze przerywa potem pracę z komunikatem „exec format error”.

To, czy digest jest właściwy, pokazuje docker buildx imagetools inspect z tagiem: na górze wyniku stoi digest indeksu, a pod nim poszczególne platformy z ich własnymi digestami.

Dlaczego usługa przestaje startować po ustawieniu read_only: true albo stałego user?

Dlatego, że wiele obrazów robi przy starcie więcej, niż ich usługa potrzebuje w pracy. Skrypt startowy często zapisuje plik konfiguracyjny ze zmiennych środowiskowych, odkłada plik PID w /run albo zakłada katalogi, zanim ruszy właściwy proces. Przy read_only: true każdy z tych zapisów poza /tmp i dołączonymi woluminami kończy się błędem. Komunikat o błędzie podaje zwykle ścieżkę, a ta powinna wtedy trafić jako kolejny wpis pod tmpfs albo do woluminu.

Przy stałym użytkowniku przyczyną są zwykle uprawnienia do plików. Danych w woluminie albo dołączonym katalogu, utworzonych wcześniej jako root, użytkownik o identyfikatorze 1000 nie może zapisywać. Do tego dochodzą obrazy, które celowo startują jako root, same dostosowują uprawnienia swoich katalogów danych i dopiero potem przełączają się na własnego użytkownika; na stałe ustawiona opcja user odbiera im ten pierwszy krok, a dostosowanie trzeba wtedy przeprowadzić wcześniej na hoście.

Obie linie nie kosztują więc wydajności, kosztują natomiast jeden przebieg próbny na usługę. Najbezpieczniej wprowadzać je usługa po usłudze i po każdej powtarzać test restartu z artykułu.

Lukas Wójcik

Lukas Wójcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Napisanie komentarza

Doświadczenia z innymi modelami i pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.

Artykuły i kategorie

CCTV

Śledź tę kategorię przez RSS

Cloud & AI

Wszystkie artykuły w tej kategorii (17) Śledź tę kategorię przez RSS

Data Privacy

Wszystkie artykuły w tej kategorii (19) Śledź tę kategorię przez RSS

Digital Analytics

Wszystkie artykuły w tej kategorii (58) Śledź tę kategorię przez RSS

Digital Marketing

Wszystkie artykuły w tej kategorii (37) Śledź tę kategorię przez RSS

IT & Networks

Wszystkie artykuły w tej kategorii (18) Śledź tę kategorię przez RSS

Music Production

Wszystkie artykuły w tej kategorii (17) Śledź tę kategorię przez RSS

Raspberry Pi

Śledź tę kategorię przez RSS

SaaS & Internet Earning

Śledź tę kategorię przez RSS

Smart Home

Wszystkie artykuły w tej kategorii (18) Śledź tę kategorię przez RSS

Tworzenie stron internetowych

Wszystkie artykuły w tej kategorii (11) Śledź tę kategorię przez RSS

Wtyczki i triki WordPress

Wszystkie artykuły w tej kategorii (14) Śledź tę kategorię przez RSS