SSD Nodes Learn 8GB RAM — $66/rok
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-01

Docker Compose: limity pamięci chroniące przed OOM

Ustaw limity pamięci i CPU w Docker Compose, aby uniknąć OOM na VPS. Sprawdź deploy.resources, mem_limit, kod 137, swap oraz dobór wartości.

Co oznacza limit pamięci Docker Compose

Limit pamięci Docker Compose to twardy limit nakładany przez jądro Linux na grupę cgroup (grupę kontrolną, funkcję jądra mierzącą zasoby dla zestawu procesów) jednego kontenera. Ustawienie deploy.resources.limits.memory dla usługi powoduje, że ten kontener nie może nigdy użyć większej ilości pamięci niż podana wartość. Gdy próbuje ją przekroczyć, jądro zabija proces działający wewnątrz kontenera, a kontener zwykle kończy działanie z kodem 137.

Ma to największe znaczenie na VPS, gdzie ilość pamięci RAM jest stała i nie ma dodatkowej pamięci hosta, którą można wykorzystać. Jeden kontener z wyciekiem pamięci lub nieprawidłowym zapytaniem zajmie każdą wolną stronę pamięci na serwerze z 8GB RAM. Następnie jądro zabije proces, który uzna za najbardziej obciążający system. Często będzie to baza danych lub sesja SSH, a nie kontener, który spowodował problem. Limity zmieniają awarię całego serwera w problem dotyczący jednej usługi, którą można ponownie uruchomić.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

Zastosuj ustawienie i potwierdź, że limit działa:

docker compose up -d
docker stats --no-stream

Kolumna MEM USAGE / LIMIT powinna zawierać wartość podobną do 142MiB / 1GiB. Jeśli kolumna limitu pokazuje pełną ilość pamięci RAM hosta, ustawienie nie zostało zastosowane. Pozostała część tego poradnika nie będzie działać, dopóki problem nie zostanie rozwiązany. Jeśli plik Compose jest nowym zagadnieniem, podstawy Docker Compose dla VPS opisują układ pliku, na którym opiera się ten poradnik.

deploy.resources.limits czy mem_limit: która wartość obowiązuje

Istnieją 2 zapisy tej samej koncepcji, dlatego może to być mylące.

mem_limit, mem_reservation, memswap_limit, cpus i cpu_shares to klucze usługi najwyższego poziomu odziedziczone po starszych formatach plików Compose. deploy.resources pochodzi ze schematu Swarm i obecnie jest częścią Compose Specification, czyli formatu odczytywanego obecnie przez docker compose.

Oba zapisy działają na pojedynczym hoście. Compose V2, czyli wtyczka docker compose, stosuje deploy.resources.limits i deploy.resources.reservations po uruchomieniu docker compose up, bez udziału klastra Swarm. Elementy bloku deploy dotyczące wyłącznie Swarm to pozostałe klucze: mode, placement, update_config i endpoint_mode mają znaczenie dla docker stack deploy i są ignorowane przez docker compose up. Zatem często powtarzana informacja, że „deploy wymaga Swarm”, jest nieprawdziwa w odniesieniu do podsekcji resources. Zastosowanie się do niej pozostawia usługi bez żadnego limitu.

W jednym projekcie należy stosować tylko jeden zapis. Umieszczenie mem_limit: 512m i deploy.resources.limits.memory: 1g w tej samej usłudze tworzy plik, którego zawartości nie można od razu odczytać. Zamiast zgadywać, która wartość została zastosowana, należy sprawdzić ją w daemonie:

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

Wartości pamięci są wyrażane w bajtach, dlatego 1g wyświetla się jako 1073741824. CPU jest wyrażany w nanoCPU, dlatego 1.5 wyświetla się jako 1500000000. Wartość 0 w dowolnym polu oznacza brak ustawionego limitu. Najmniejszy limit pamięci akceptowany przez Docker to 6m. Poniżej tej wartości kontener nie zostanie uruchomiony.

Co się dzieje, gdy kontener osiągnie limit

Kontener nie zwalnia. Kończy działanie.

Gdy proces żąda strony pamięci, a cgroup jest już na poziomie memory.max, jądro najpierw odzyskuje to, co może w ramach tego cgroup: czyste strony cache, a następnie strony, które można przenieść do swap. Jeśli odzyskanie pamięci nie zwolni wystarczającej ilości miejsca, mechanizm OOM (out of memory) w cgroup wybiera proces wewnątrz kontenera i wysyła do niego SIGKILL. Zabicie PID 1 kontenera kończy działanie kontenera. Kod wyjścia 137 to po prostu 128 plus sygnał 9, dlatego 137 jest oznaką dowolnego SIGKILL, a nie samodzielnym dowodem wystąpienia OOM.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 oznacza zabicie przez OOM. false 137 oznacza, że SIGKILL wysłał inny proces, a typową przyczyną jest osiągnięcie przez docker compose stop dziesięciosekundowego okresu oczekiwania, ponieważ aplikacja zignorowała SIGTERM. To rozróżnienie oszczędza wiele godzin, ponieważ te dwa problemy nie mają ze sobą nic wspólnego.

Zdarzenie jest rejestrowane jeszcze w dwóch miejscach. Należy monitorować działanie demona:

docker events --filter event=oom

Następnie należy odczytać dziennik jądra, który zachowuje się po restarcie:

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

Zabicie przez cgroup zapisuje wiersz rozpoczynający się od Memory cgroup out of memory: Killed process 24713 (node). Wiersz bez prefiksu Memory cgroup oznacza OOM hosta, czyli sytuację, w której na samej maszynie zabrakło pamięci RAM. Przed tym ma chronić ustawienie limitów, dlatego jego wystąpienie oznacza, że suma limitów jest zbyt wysoka albo niektóre usługi w ogóle nie mają limitu.

W przypadku restart: unless-stopped pętla OOM jest trudna do wykrycia, ponieważ sekundę po zakończeniu działania usługi w docker compose ps wygląda ona prawidłowo. Należy sprawdzić kolumnę czasu działania i licznik restartów oraz połączyć limit z healthcheckiem zgłaszającym niezdrowy stan aplikacji, aby kontener, który stale kończy działanie, był widoczny bez konieczności jego ciągłego monitorowania.

Rezerwacja jest wskazówką, limit jest regułą

reservations.memory (starszy mem_reservation) to miękki poziom minimalny. Docker opisuje go jako miękki limit aktywowany, gdy demon wykryje rywalizację o zasoby lub małą ilość pamięci na hoście. Nie uniemożliwia on kontenerowi przekroczenia tej wartości i nie gwarantuje, że pamięć będzie dostępna, gdy kontener zgłosi takie zapotrzebowanie. Jedynie skłania jądro do wcześniejszego odzyskiwania pamięci z kontenerów, które przekraczają swoją rezerwację.

Sama rezerwacja nie zapewnia więc żadnej ochrony. Należy jej używać do oznaczania usługi, która powinna być traktowana priorytetowo w warunkach przeciążenia, a dla bezpieczeństwa polegać na limicie. Rezerwację należy ustawić poniżej limitu. W przeciwnym razie kontener nie zostanie uruchomiony: Docker odrzuci konfigurację z komunikatem Minimum memory limit can not be less than memory reservation limit.

Rzetelne rozliczanie swap

Większość obrazów VPS nie zawiera w ogóle pliku swap. Uruchomić swapon --show i free -h. Jeśli całkowita ilość swap wynosi zero, żadne z poniższych ustawień dotyczących swap nie działa, a limit pamięci jest wyłącznie limitem pamięci RAM.

memswap_limit nie oznacza ilości swap. Jest to suma pamięci i swap. Przy użyciu mem_limit: 1g i memswap_limit: 2g kontener otrzymuje 1GB pamięci RAM i 1GB swap. Ustawienie obu wartości na taką samą wartość całkowicie wyłącza swap dla kontenera. Ustawienie mem_limit i pozostawienie memswap_limit bez wartości umożliwia kontenerowi ponowne wykorzystanie swap do rozmiaru limitu pamięci.

Ubuntu 24.04 i Debian 13 domyślnie używają cgroup v2, w którym swap jest osobnym licznikiem (memory.swap.max), dlatego działa bez dodatkowej konfiguracji. Stary komunikat Your kernel does not support swap limit capabilities pochodzi z hostów cgroup v1 uruchomionych bez swapaccount=1. Na tych hostach limit pamięci nadal obowiązuje, natomiast część dotycząca swap jest ignorowana.

Należy realistycznie ocenić korzyści z używania swap. Sprawia on, że zakończenie procesu przez OOM trwa dłużej, ale nie zmniejsza prawdopodobieństwa takiego zakończenia, ponieważ proces z wyciekiem pamięci zapełnia swap równie łatwo jak pamięć RAM. Tymczasem kontener intensywnie korzystający ze swap na współdzielonej pamięci masowej VPS spowalnia wszystkie pozostałe usługi na hoście. W przypadku usług wrażliwych na opóźnienia prawidłowy limit bez swap powoduje szybszą i bardziej przewidywalną awarię.

Dlaczego użycie pamięci wygląda gorzej, niż jest w rzeczywistości

Wartość MEM USAGE w docker stats obejmuje pamięć podręczną stron, dlatego kontener odczytujący duże pliki zbliża się do swojego limitu i pozostaje na tym poziomie. Jest to normalne i nie oznacza wycieku, ponieważ czysta pamięć podręczna jest odzyskiwana, zanim zostanie wywołany mechanizm OOM killer. Usługa taka jak samodzielnie hostowany serwer multimediów Jellyfin będzie z tego powodu stale wyglądać na działającą blisko limitu.

Podział tej wartości na pamięć podręczną i rzeczywisty working set należy wykonać wewnątrz kontenera:

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon to pamięć anonimowa, czyli working set, którego nie można zwolnić przez odrzucenie. file to pamięć podręczna stron, którą można zwolnić. Limit należy ustalać na podstawie anon powiększonego o margines, a nie na podstawie wartości całkowitej. Plik memory.events rozstrzyga tę kwestię jednoznacznie: licznik oom_kill większy od zera oznacza, że kernel zabił coś w tym kontenerze od chwili jego uruchomienia, a rosnący licznik max oznacza, że kontener jest obecnie utrzymywany na swoim limicie. Oba polecenia wymagają powłoki i coreutils dostępnych w obrazie, dlatego nie działają w obrazie distroless ani scratch.

Limity alokacji na VPS z 8GB pamięci

Należy rozpocząć od hosta, a nie od aplikacji. Na VPS z 8GB pamięci należy pozostawić około 1GB dla jądra, demona Docker, sshd, journald i własnej powłoki logowania. Do rozdysponowania pozostaje około 7GB, a suma limitów wszystkich kontenerów powinna pozostać poniżej tej wartości. Nadmierna alokacja działa do dnia, w którym dwa usługi osiągną jednocześnie szczytowe zużycie zasobów.

Przykładowy podział dla hosta z 8GB pamięci:

  • Reverse proxy: limit 128m. Jest to niewielki proces, a tak niski limit natychmiast ujawnia niekontrolowane przeładowanie konfiguracji.
  • PostgreSQL: limit 2g, z wartością shared_buffers ustawioną na około 512MB w konfiguracji bazy danych.
  • Kontener aplikacji: limit 1g.
  • Worker działający w tle: limit 512m.
  • Usługa multimediów lub plików: limit 2g, z czego większość będzie stanowić pamięć podręczna stron.

Nie należy kopiować tych wartości do własnego stosu. Należy uruchomić usługi przy rzeczywistym obciążeniu przez jeden dzień, monitorować docker stats, odczytać szczytową wartość anon dla każdego kontenera i dodać około połowy tej wartości jako zapas. Ustawienie zbyt niskiego limitu jest gorsze niż brak limitu, ponieważ powoduje zakończenie działania sprawnej usługi podczas normalnego skoku ruchu sieciowego.

Jeden problem wymaga osobnego omówienia. Większość środowisk uruchomieniowych nie uwzględnia limitu, dopóki nie zostanie im on przekazany. PostgreSQL bez problemu ustawi shared_buffers i work_mem powyżej limitu kontenera, a następnie zostanie zakończony. JVM (Java virtual machine) potrzebuje -XX:MaxRAMPercentage=75, aby ustalać rozmiar sterty na podstawie limitu cgroup, a nie pamięci RAM hosta. Node.js potrzebuje --max-old-space-size w megabajtach, ustawionego poniżej limitu kontenera, w przeciwnym razie jego garbage collector pozwoli na zwiększanie sterty do czasu interwencji jądra. cgroup nie negocjuje. Kończy działanie procesu.

Limity CPU działają zupełnie inaczej

cpus: "1.5" oznacza 150% jednego rdzenia, egzekwowane jako limit przydziału CFS (completely fair scheduler). Kontener otrzymuje 150ms czasu CPU w każdym okresie trwającym 100ms. Limit jest współdzielony przez wszystkie wątki kontenera. Po jego wykorzystaniu jądro wstrzymuje kontener do rozpoczęcia następnego okresu.

To jest najważniejsza różnica. Kontener, który przekroczy limit pamięci, zostaje zakończony. Kontener, który przekroczy limit CPU, jest ograniczany i nadal działa, ale wolniej. Limit CPU można więc ustawiać z niewielkim zapasem. Limit pamięci wymaga natomiast pozostawienia odpowiedniego zapasu.

cpu_shares to inne narzędzie: względna waga, która ma znaczenie tylko wtedy, gdy procesory są rzeczywiście w pełni obciążone. Dwa kontenery z udziałami 1024 i 512 dzielą obciążony rdzeń mniej więcej w proporcji dwa do jednego. Na nieobciążonym hoście żaden z nich nie jest ograniczany. Udziałów należy używać do ustalania ważności usług, a cpus wtedy, gdy potrzebny jest rzeczywisty limit maksymalny, na przykład aby nocne transkodowanie nie pozbawiło serwera WWW zasobów.

FAQ

Czy deploy.resources.limits działa bez Docker Swarm?

Tak. Compose V2 stosuje deploy.resources.limits i deploy.resources.reservations po uruchomieniu docker compose up na jednym hoście. Należy to potwierdzić za pomocą docker inspect --format '{{.HostConfig.Memory}}' <container>. Polecenie wyświetla limit w bajtach oraz 0, gdy nie zastosowano żadnego limitu. Klucze w obrębie deploy, które rzeczywiście wymagają Swarm, to mode, placement, update_config i endpoint_mode.

Co oznacza kod zakończenia 137 w Docker Compose?

Oznacza, że główny proces otrzymał SIGKILL, ponieważ 137 to 128 plus sygnał 9. Najczęstszą przyczyną jest mechanizm OOM killera jądra, ale przekroczenie limitu czasu zamykania powoduje ten sam kod, gdy aplikacja ignoruje SIGTERM. Należy uruchomić docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container>, aby rozróżnić te przypadki. true 137 oznacza zakończenie procesu z powodu braku pamięci, a false 137 nie.

Czy należy ustawić mem_limit czy deploy.resources.limits.memory?

Oba ustawienia działają z docker compose. deploy.resources.limits.memory jest obecnie zalecaną formą Compose Specification i stanowi lepszy wybór dla nowego pliku. Należy zachować mem_limit, jeśli pozostała część pliku już używa starszych kluczy najwyższego poziomu. Ustawienie obu wartości dla jednej usługi tylko utrudnia odczyt pliku, dlatego należy wybrać jedną z nich i zweryfikować wynik za pomocą docker inspect.

Dlaczego kontener utrzymuje zużycie pamięci na poziomie pełnego limitu, ale nie zostaje zakończony?

Wartość zużycia w docker stats obejmuje pamięć podręczną stron, którą jądro zwalnia pod presją zamiast wywoływać zakończenie procesu przez OOM killera. Należy uruchomić docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat i odczytać wartość anon. Jest to zestaw roboczy pamięci, którego nie można odzyskać. Wysoka wartość file przy niskiej wartości anon oznacza, że kontener wykonuje operacje wejścia i wyjścia dysku, a nie że wkrótce zostanie zakończony.

Ile pamięci RAM należy pozostawić nieprzydzielonej na VPS z 8GB?

Należy pozostawić około 1GB na jądro, demona Docker, sshd, journald i własną powłokę. Następnie suma limitów wszystkich kontenerów powinna pozostać niższa niż pozostałe 7GB. Przed przyjęciem konkretnych wartości należy przez jeden dzień monitorować szczytową wartość anon dla każdego kontenera podczas rzeczywistego obciążenia. Łączną wartość należy traktować jako budżet, a nie jako cel do całkowitego wykorzystania.