SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-10-24

Limity pamięci Docker Compose bez błędu OOM

Ustaw limity CPU i pamięci w Docker Compose. Poznaj deploy.resources, mem_limit, kod 137, swap oraz dobór wartości, aby awaria jednej usługi nie wyłączyła VPS.

Działanie limitu pamięci w Docker Compose

Limit pamięci w Docker Compose to twardy limit nakładany przez jądro Linux na cgroup (grupę kontrolną, czyli funkcję jądra mierzącą zasoby dla zestawu procesów) danego kontenera. Po ustawieniu deploy.resources.limits.memory dla usługi, kontener nie może przekroczyć wskazanej wartości. W przypadku próby przekroczenia limitu, jądro przerywa proces wewnątrz kontenera, co zazwyczaj skutkuje zakończeniem kontenera z kodem 137.

Jest to szczególnie istotne na serwerach VPS, gdzie ilość pamięci RAM jest stała i nie ma zapasowych zasobów hosta do wykorzystania. Kontener z wyciekiem pamięci lub błędnym zapytaniem może zająć każdą wolną stronę pamięci na maszynie z 8GB RAM. Jądro systemu przerywa wówczas proces, który uzna za najbardziej obciążający – często jest to baza danych lub sesja SSH, a nie kontener będący źródłem problemu. Limity pozwalają ograniczyć awarię całego serwera do restartu jednej usługi.

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

Zastosuj ustawienie i sprawdź, czy limit jest aktywny:

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

Kolumna MEM USAGE / LIMIT powinna wskazywać wartość zbliżoną do 142MiB / 1GiB. Jeśli kolumna limitu pokazuje pełną pamięć RAM hosta, ustawienie nie zostało zastosowane; dalsza część tego przewodnika nie będzie skuteczna, dopóki ten stan nie ulegnie zmianie. Jeśli plik compose jest nowym zagadnieniem, podstawy Docker Compose dla VPS opisują strukturę pliku, na której opiera się to rozwiązanie.

deploy.resources.limits czy mem_limit: co ma pierwszeństwo

Istnieją dwa sposoby zapisu tej samej funkcjonalności, co prowadzi do niejasności.

mem_limit, mem_reservation, memswap_limit, cpus oraz cpu_shares to klucze usług najwyższego poziomu, odziedziczone ze starszych formatów plików Compose. deploy.resources wywodzi się ze schematu Swarm i obecnie stanowi część specyfikacji Compose, czyli formatu, który odczytuje dzisiaj docker compose.

Oba rozwiązania działają na pojedynczym hoście. Docker Compose, czyli wtyczka docker compose, stosuje deploy.resources.limits oraz deploy.resources.reservations podczas uruchamiania docker compose up, nawet bez klastra Swarm. Wtyczka znajduje się obecnie w linii v5 (wersja v5.6.0 była najnowszą stabilną wersją w dniu 2026-10-10). Numeracja przeskoczyła z v2 bezpośrednio na v5, pomijając 3 oraz 4, aby uniknąć pomyłek ze starymi wersjami formatu plików Compose. Żadne wydanie v5 nie zmieniło sposobu obsługi limitów zasobów. Elementy bloku deploy zorientowane na Swarm to pozostałe klucze, takie jak placement, update_config oraz endpoint_mode, które opisują harmonogramowanie i wdrażanie w klastrze węzłów dla docker stack deploy. Zatem powszechna opinia, że "deploy wymaga Swarm", jest błędna w odniesieniu do podsekcji resources, a jej przestrzeganie pozostawia usługi bez żadnych limitów.

Należy wybrać jeden sposób zapisu w ramach projektu. Używanie mem_limit: 512m oraz deploy.resources.limits.memory: 1g w tej samej usłudze utrudnia czytelność: specyfikacja Compose stanowi, że jeśli mem_limit jest ustawione, musi być spójne z limits.memory, nie określając jednak, która wartość ma pierwszeństwo w przypadku konfliktu. Niezależnie od wybranego zapisu, należy zweryfikować, jakie wartości faktycznie zostały zastosowane przez daemon:

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

Wartości pamięci są wyrażone w bajtach, dlatego 1g wyświetla się jako 1073741824. Procesor (CPU) jest mierzony w nano CPU, więc 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 odmawia uruchomienia.

Co się dzieje po osiągnięciu limitu przez kontener

Kontener nie zwalnia. Zostaje zatrzymany.

Gdy proces żąda strony pamięci, a cgroup osiągnął już swój memory.max, jądro najpierw odzyskuje zasoby wewnątrz tej grupy: czystą pamięć podręczną stron, a następnie strony, które można przenieść do pliku wymiany. Jeśli odzyskiwanie nie zwolni wystarczającej ilości pamięci, mechanizm OOM (out of memory) killer wybiera proces wewnątrz kontenera i wysyła do niego sygnał SIGKILL. Zabicie procesu PID 1 kończy działanie kontenera. Kod wyjścia 137 to suma 128 oraz sygnału 9, więc 137 jest sygnaturą każdego sygnału SIGKILL, a nie samodzielnym dowodem na wystąpienie błędu OOM.

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

true 137 oznacza zabicie procesu przez OOM killer. false 137 zazwyczaj oznacza, że sygnał SIGKILL został wysłany z innego powodu (program może również celowo zakończyć działanie z kodem 137, choć jest to rzadkie). Typową przyczyną jest przekroczenie przez docker compose stop dziesięciosekundowego okresu karencji, gdy aplikacja zignorowała sygnał SIGTERM. To rozróżnienie pozwala zaoszczędzić wiele godzin pracy, ponieważ oba problemy nie mają ze sobą nic wspólnego.

Zdarzenie to jest rejestrowane w dwóch dodatkowych miejscach. Monitoruj demona w czasie rzeczywistym:

docker events --filter event=oom

Następnie sprawdź dziennik jądra, który jest zapisem przetrwającym restart:

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

Zabicie procesu przez cgroup powoduje wyświetlenie linii zaczynającej się od Memory cgroup out of memory: Killed process 24713 (node). Linia ta oznacza, że jeden z kontenerów osiągnął własny limit; może się ona pojawić, nawet gdy host dysponuje dużą ilością wolnej pamięci RAM. Linia bez prefiksu Memory cgroup oznacza błąd OOM na poziomie hosta, co oznacza, że maszynie zabrakło pamięci RAM. Jest to awaria, której limity mają zapobiegać, więc jej wystąpienie zazwyczaj oznacza, że suma limitów jest zbyt wysoka, niektóre usługi nie mają żadnych limitów lub coś spoza Docker zużywa więcej zasobów, niż przewidziano w marginesie bezpieczeństwa.

W przypadku restart: unless-stopped pętla OOM jest trudna do wykrycia, ponieważ usługa w docker compose ps wygląda na działającą sekundę po awarii. Sprawdź kolumnę uptime oraz licznik restartów i powiąż limit z healthcheckiem zgłaszającym stan niezdrowy aplikacji, aby kontener, który ciągle się restartuje, był widoczny bez konieczności ciągłego monitorowania.

Rezerwacja to wskazówka, limit to zasada

reservations.memory (starsze mem_reservation) stanowi miękki próg. Docker definiuje to jako miękki limit, który aktywuje się, gdy demon wykryje rywalizację o zasoby lub niski poziom pamięci na hoście. Mechanizm ten nigdy nie zatrzymuje kontenera przekraczającego tę wartość i nie gwarantuje dostępności pamięci w momencie zapotrzebowania. Powoduje jedynie, że jądro systemu w pierwszej kolejności odzyskuje pamięć z kontenerów, które przekroczyły swój poziom rezerwacji.

Zatem sama rezerwacja nie zapewnia ochrony. Należy jej używać do oznaczania usług, które mają być traktowane priorytetowo w warunkach obciążenia, natomiast dla bezpieczeństwa należy polegać na limicie. Wartość rezerwacji musi być niższa od limitu, w przeciwnym razie kontener nie zostanie uruchomiony: Docker odrzuci konfigurację z błędem Minimum memory limit can not be less than memory reservation limit.

Rozliczanie swapu, szczerze

Większość obrazów VPS nie posiada skonfigurowanego pliku wymiany (swap). Uruchom swapon --show oraz free -h. Jeśli całkowita wartość swap wynosi zero, każde poniższe ustawienie związane ze swapem nie przyniesie efektu, a limit pamięci będzie ograniczeniem wyłącznie dla pamięci RAM.

memswap_limit nie określa ilości swapu. Jest to suma pamięci RAM oraz swapu. Przy ustawieniu mem_limit: 1g oraz memswap_limit: 2g, kontener otrzymuje 1GB pamięci RAM oraz 1GB swapu. Ustawienie obu wartości na ten sam poziom sprawia, że kontener nie korzysta ze swapu wcale. Ustawienie mem_limit przy pozostawieniu memswap_limit jako nieustawionego pozwala kontenerowi na użycie swapu o rozmiarze równym jego limitowi pamięci.

Systemy Ubuntu 24.04 oraz Debian 13 domyślnie korzystają z cgroup v2, gdzie swap jest oddzielnym licznikiem (memory.swap.max) i działa bez dodatkowej konfiguracji. Stary komunikat Your kernel does not support swap limit capabilities pochodzi z hostów z cgroup v1 uruchomionych bez swapaccount=1. W takich przypadkach limit pamięci nadal obowiązuje, podczas gdy część dotycząca swapu jest ignorowana.

Należy uczciwie ocenić, co daje użycie swapu. Powoduje ono, że proces OOM kill działa wolniej, a nie rzadziej, ponieważ proces z wyciekiem pamięci wypełnia swap równie chętnie co RAM. Jednocześnie kontener intensywnie korzystający ze swapu na współdzielonym nośniku VPS spowalnia każdą inną usługę na serwerze. W przypadku zadań wrażliwych na opóźnienia, poprawny limit bez swapu pozwala na szybsze i bardziej przewidywalne wykrycie awarii.

Dlaczego zużycie pamięci wygląda na wyższe niż w rzeczywistości

W systemie Linux wartość MEM USAGE w docker stats odejmuje jedynie nieaktywną pamięć podręczną stron (inactive_file w cgroup v2), więc ostatnio używana pamięć podręczna nadal jest wliczana. W efekcie kontener, który odczytuje duże pliki, zbliża się do swojego limitu i utrzymuje to zużycie. Jest to zachowanie normalne i nie oznacza wycieku, ponieważ czysta pamięć podręczna jest zwalniana, zanim zostanie wywołany mechanizm OOM killer. Usługa taka jak samodzielnie hostowany serwer multimediów Jellyfin będzie z tego właśnie powodu stale wykazywać zużycie bliskie limitu.

Należy rozdzielić wartość na pamięć podręczną oraz rzeczywisty zestaw roboczy z wnętrza 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 zestaw roboczy, którego nie można usunąć. file to pamięć podręczna stron, którą można usunąć. Limit należy ustalać w oparciu o anon z uwzględnieniem marginesu, a nie w oparciu o wartość całkowitą. Plik memory.events ostatecznie rozstrzyga kwestię: licznik oom_kill większy od zera oznacza, że jądro systemu zabiło jakiś proces w tym kontenerze od momentu jego uruchomienia, a rosnący licznik max oznacza, że kontener jest obecnie utrzymywany na poziomie swojego limitu. Oba polecenia wymagają powłoki oraz pakietu coreutils wewnątrz obrazu, dlatego nie zadziałają w obrazach typu distroless lub scratch.

Limity rozmiaru na VPS z 8GB RAM

Należy zacząć od hosta, a nie od aplikacji. Na VPS z 8GB RAM należy zarezerwować około 1GB dla jądra systemu, demona Docker, sshd, journald oraz własnej powłoki użytkownika. Pozostaje około 7GB do rozdysponowania, a suma limitów wszystkich kontenerów powinna mieścić się w tej wartości. Nadmierne przydzielanie zasobów (overcommitting) działa do momentu, w którym dwie usługi osiągną szczytowe obciążenie jednocześnie.

Przykładowy, funkcjonalny podział dla maszyny z 8GB RAM:

  • Reverse proxy: limit 128m. Jest to lekki proces, a tak restrykcyjny limit pozwala natychmiast wykryć błędy w przeładowaniu konfiguracji.
  • PostgreSQL: limit 2g, z parametrem shared_buffers ustawionym na około 512MB w konfiguracji bazy danych.
  • Kontener aplikacji: limit 1g.
  • Background worker: limit 512m.
  • Usługa plików lub mediów: limit 2g, z czego większość zostanie wykorzystana na page cache.

Nie należy kopiować tych wartości bezkrytycznie do własnego stosu. Należy uruchomić usługi pod rzeczywistym obciążeniem przez jeden dzień, monitorować docker stats, sprawdzić szczytową wartość anon dla każdego kontenera i dodać około połowę tej wartości jako margines bezpieczeństwa. Zbyt restrykcyjny limit jest gorszy niż jego brak, ponieważ powoduje ubicie poprawnej usługi podczas typowego skoku ruchu.

Jedna pułapka wymaga szczególnej uwagi. Niektóre środowiska uruchomieniowe ignorują limity, dopóki nie zostaną o nich poinformowane, a te, które je odczytują, nie zawsze dostosowują swój rozmiar w oczekiwany sposób. PostgreSQL bez problemu przydzieli shared_buffers oraz work_mem powyżej limitu kontenera, co doprowadzi do jego ubicia. Nowoczesne środowisko JVM (Java virtual machine) domyślnie wykrywa limit cgroup, jednak kompilacje HotSpot, takie jak OpenJDK, przydzielają stercie (heap) tylko 25 procent tej wartości. Dlatego -XX:MaxRAMPercentage=75 jest standardowym sposobem na umożliwienie stercie wykorzystania większej części limitu, przy jednoczesnym pozostawieniu miejsca na pamięć poza stertą. Node.js od wersji 12 automatycznie dostosowuje domyślny rozmiar sterty na podstawie limitu cgroup, ustawiając go na około połowę limitu kontenera i ograniczając do około 2GB. Wartość --max-old-space-size w megabajtach należy ustawiać tylko wtedy, gdy wymagany jest inny limit, pamiętając o utrzymaniu go poniżej limitu kontenera, aby mechanizm garbage collector mógł zadziałać przed interwencją jądra systemu. Podobnie sytuacja wygląda w przypadku Ollama, choć używa innego parametru, ponieważ zwiększenie num_ctx powoduje wzrost pamięci podręcznej KV o setki megabajtów, co prowadzi do ubicia kontenera w trakcie przetwarzania długiego promptu. Cgroup nie negocjuje. Cgroup ubija proces.

Limity CPU działają w zupełnie inny sposób

cpus: "1.5" oznacza 150% jednego rdzenia, egzekwowane jako limit CFS (Completely Fair Scheduler). Kontener otrzymuje 150ms czasu procesora w każdym okresie 100ms, dzielone pomiędzy wszystkie jego wątki. Po wyczerpaniu tego czasu jądro systemu wstrzymuje proces do rozpoczęcia kolejnego okresu.

To kluczowa różnica. Kontener, który przekroczy limit pamięci, zostaje zabity. Kontener, który przekroczy limit CPU, jest dławiony (throttled) i kontynuuje pracę, ale wolniej. Dlatego limity CPU można ustawiać agresywnie, podczas gdy limity pamięci wymagają zapasu.

cpu_shares to inne narzędzie: relatywna waga, która ma znaczenie tylko wtedy, gdy procesory są w pełni obciążone. Dwa kontenery z udziałami 1024 i 512 dzielą zajęty rdzeń w proporcji mniej więcej dwa do jednego, a na nieobciążonym serwerze żaden z nich nie jest ograniczany. Używaj udziałów, aby szeregować usługi według ważności, a używaj cpus, gdy potrzebujesz rzeczywistego górnego limitu, na przykład aby zapobiec sytuacji, w której nocne zadanie transkodowania pozbawi zasobów serwer WWW.

FAQ

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

Tak. Docker Compose, wtyczka docker compose (obecnie w linii v5), stosuje deploy.resources.limits oraz deploy.resources.reservations podczas uruchamiania docker compose up na pojedynczym hoście. Można to potwierdzić za pomocą docker inspect --format '{{.HostConfig.Memory}}' <container>, które wyświetla limit w bajtach lub 0, jeśli limit nie został nałożony. Klucze wewnątrz deploy zaprojektowane z myślą o Swarm to te klastrowe, takie jak placement, update_config oraz endpoint_mode.

Co oznacza kod wyjścia 137 w Docker Compose?

Prawie zawsze oznacza to, że główny proces otrzymał sygnał SIGKILL, ponieważ 137 to 128 plus sygnał 9 (program może również wyjść z kodem 137 celowo, ale jest to rzadkie). Częstą przyczyną jest mechanizm OOM killer jądra systemu, jednak przekroczenie czasu wyłączenia (shutdown timeout) generuje ten sam kod, gdy aplikacja ignoruje sygnał SIGTERM. Uruchom docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container>, aby rozróżnić te sytuacje. true 137 oznacza zabicie procesu z powodu braku pamięci, a false 137 nie.

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

Oba rozwiązania działają z docker compose. deploy.resources.limits.memory to aktualny format specyfikacji Compose i jest lepszym wyborem dla nowych plików. Pozostaw mem_limit, jeśli reszta pliku korzysta ze starszych kluczy najwyższego poziomu. Specyfikacja Compose wymaga spójności obu wartości, jeśli zostały ustawione jednocześnie, dlatego należy wybrać jedną i zweryfikować wynik za pomocą docker inspect.

Dlaczego kontener osiąga pełny limit pamięci i nie jest zabijany?

Wartość użycia w docker stats wlicza niedawno używaną pamięć podręczną stron (page cache), którą jądro usuwa pod obciążeniem zamiast wyzwalać OOM killer. Uruchom docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat i odczytaj wartość anon, która reprezentuje zestaw roboczy (working set), którego nie można odzyskać. Wysoka wartość file przy niskiej wartości anon oznacza kontener wykonujący operacje wejścia/wyjścia na dysku, a nie kontener bliski zakończenia działania.

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

Należy zarezerwować około 1GB dla jądra systemu, demona Docker, sshd, journald oraz powłoki użytkownika, a następnie utrzymać sumę limitów wszystkich kontenerów poniżej pozostałych 7GB. Przez jeden dzień monitoruj szczytową wartość anon dla każdego kontenera pod rzeczywistym obciążeniem, zanim ustalisz limity, i traktuj sumę jako budżet, a nie cel do wypełnienia.

Czy ten artykuł był przydatny?