SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-28

Jak ustawić limity pamięci RAM w Docker Compose

Dowiedz się jak skonfigurować deploy.resources w Docker Compose, aby uniknąć błędu exit 137 i awarii serwera VPS. Zabezpiecz kontenery przed zajęciem całej pamięci RAM hosta.

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 pracy kontenera z kodem 137.

Jest to kluczowe 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 serwerze z 8GB RAM. W takiej sytuacji jądro przerywa proces, który uzna za najbardziej obciążający – często jest to baza danych lub sesja SSH, a nie kontener, który spowodował problem. Limity pozwalają ograniczyć awarię całego serwera do restartu pojedynczej 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 pomocna, dopóki ten stan nie zostanie naprawiony. Jeśli plik compose jest nowością, podstawy Docker Compose dla VPS opisują strukturę pliku, na której opiera się to rozwiązanie.

deploy.resources.limits czy mem_limit: co ma zastosowanie

Istnieją dwa sposoby zapisu tego samego parametru, co jest źródłem nieporozumień.

mem_limit, mem_reservation, memswap_limit, cpus oraz cpu_shares to klucze usług najwyższego poziomu, odziedziczone po starszych formatach 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. Compose V2, czyli wtyczka docker compose, stosuje deploy.resources.limits oraz deploy.resources.reservations podczas uruchamiania docker compose up, nawet bez obecności klastra Swarm. Elementy bloku deploy specyficzne wyłącznie dla Swarm to pozostałe klucze: mode, placement, update_config oraz endpoint_mode mają znaczenie dla docker stack deploy i są ignorowane przez docker compose up. Powszechna opinia, że "sekcja deploy wymaga Swarm", jest błędna w odniesieniu do podsekcji resources, a jej przestrzeganie sprawia, że usługi pozostają bez żadnych limitów.

Należy wybrać jeden sposób zapisu w ramach projektu. Użycie mem_limit: 512m oraz deploy.resources.limits.memory: 1g w tej samej usłudze sprawia, że plik staje się nieczytelny. Zamiast zgadywać, która wartość została przyjęta, należy zapytać demona:

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

Wartości pamięci są wyrażone w bajtach, więc 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: czyści pamięć podręczną stron (page cache), a następnie strony, które można przenieść do pliku wymiany (swap). Jeśli odzyskiwanie nie zwolni wystarczającej ilości pamięci, mechanizm OOM (out of memory) killer dla cgroup wybiera proces wewnątrz kontenera i wysyła do niego sygnał SIGKILL. Zabicie PID 1 kontenera kończy jego działanie. Kod wyjścia 137 to suma 128 i sygnału 9, zatem 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 oznacza, że sygnał SIGKILL został wysłany przez inny podmiot, a najczęstszą przyczyną jest docker compose stop, który przekroczył dziesięciosekundowy okres karencji, ponieważ 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 jest rejestrowane w dwóch dodatkowych miejscach. Monitoruj działanie 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 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 świadczy o tym, że suma limitów jest zbyt wysoka lub niektóre usługi nie mają przypisanych żadnych ograniczeń.

Przy 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 liczbę restartów i powiąż limit z testem kondycji (healthcheck), który zgłasza aplikację jako niezdrową, aby kontener, który ciągle się restartuje, był widoczny bez konieczności ciągłego nadzoru.

Rezerwacja to wskazówka, limit to zasada

reservations.memory (starszy mem_reservation) stanowi miękki próg. Docker opisuje go jako miękki limit, który aktywuje się, gdy demon wykryje rywalizację o zasoby lub niski poziom pamięci na hoście. Nigdy nie zatrzymuje on kontenera po przekroczeniu tej wartości i nie gwarantuje, że pamięć będzie dostępna, gdy kontener o nią poprosi. Powoduje jedynie, że jądro systemu w pierwszej kolejności odzyskuje pamięć z kontenerów, które przekroczyły swoją rezerwację.

Zatem sama rezerwacja nie zapewnia ochrony. Należy jej używać do oznaczania usług, które mają być traktowane priorytetowo w warunkach dużego obciążenia, natomiast w kwestii bezpieczeństwa należy polegać na limicie. Rezerwacja 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.

Obsługa swap, szczerze mówiąc

Większość obrazów VPS jest dostarczana bez żadnego pliku 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 stanowić wyłącznie ograniczenie pamięci RAM.

memswap_limit nie określa ilości swapu. Jest to suma pamięci oraz swapu. Przy mem_limit: 1g oraz memswap_limit: 2g kontener otrzymuje 1GB pamięci RAM oraz 1GB swapu. Ustawienie obu wartości na tym samym poziomie powoduje brak dostępu do swapu. Ustawienie mem_limit przy pozostawieniu memswap_limit jako nieustawionego pozwala kontenerowi na użycie swapu o rozmiarze równym jego limitowi pamięci.

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

Należy uczciwie ocenić, co daje swap. Sprawia on, że proces OOM kill następuje wolniej, a nie rzadziej, ponieważ proces z wyciekiem pamięci zapełnia swap równie chętnie co RAM. Jednocześnie kontener intensywnie korzystający ze swapu na współdzielonej pamięci masowej VPS spowalnia każdą inną usługę na serwerze. W przypadku zastosowań wrażliwych na opóźnienia, poprawny limit bez swapu pozwala na szybszą i bardziej przewidywalną reakcję w razie awarii.

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

Wartość MEM USAGE w docker stats uwzględnia pamięć podręczną stron (page cache), więc kontener odczytujący duże pliki zbliża się do swojego limitu i utrzymuje ten stan. Jest to zachowanie normalne i nie oznacza wycieku pamięci, ponieważ czysta pamięć podręczna jest zwalniana, zanim mechanizm OOM killer zostanie uruchomiony. Usługa taka jak własny serwer multimediów Jellyfin z tego właśnie powodu będzie stale wykazywać użycie pamięci bliskie limitowi.

Należy rozdzielić tę wartość na pamięć podręczną oraz rzeczywisty zestaw roboczy z poziomu 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 tę 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 ograniczany przez swój limit. 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 pamięci RAM

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

Proponowany podział dla serwera z 8GB pamięci RAM:

  • Reverse proxy: limit 128m. Jest to lekki proces, a tak restrykcyjny limit pozwala natychmiast wykryć błędną konfigurację powodującą zapętlenie przeładowań.
  • PostgreSQL: limit 2g, z parametrem shared_buffers ustawionym na około 512MB w konfiguracji bazy danych.
  • Kontener aplikacji: limit 1g.
  • Background worker: limit 512m.
  • Serwis 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 środowiska. Należy uruchomić usługi pod rzeczywistym obciążeniem przez dobę, 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. Limit jest niewidoczny dla większości środowisk uruchomieniowych, chyba że zostaną one o nim poinformowane. PostgreSQL bez odpowiedniej konfiguracji spróbuje ustawić shared_buffers oraz work_mem powyżej limitu kontenera, co doprowadzi do jego ubicia. Maszyna wirtualna Java (JVM) wymaga flagi -XX:MaxRAMPercentage=75, aby dostosować rozmiar sterty (heap) do limitu cgroup, a nie do pamięci fizycznej hosta. Node.js wymaga ustawienia --max-old-space-size w megabajtach na wartość poniżej limitu kontenera, w przeciwnym razie mechanizm odśmiecania pamięci (garbage collector) pozwoli stercie rosnąć, aż jądro systemu przerwie proces. Podobnie zachowuje się Ollama, wymagająca odpowiednich parametrów, 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 zapytania. Cgroup nie negocjuje. Cgroup zabija 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, dzielonym mię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 (shares) odpowiednio 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. Udziały służą do szeregowania usług według ważności, natomiast cpus należy stosować, gdy wymagany jest twardy limit, na przykład aby zapobiec sytuacji, w której nocne zadanie transkodowania obciąża serwer WWW.

FAQ

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

Tak. Compose V2 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, które faktycznie wymagają Swarm, to mode, placement, update_config oraz endpoint_mode.

Co oznacza kod wyjścia 137 w Docker Compose?

Oznacza to, że główny proces otrzymał sygnał SIGKILL, ponieważ 137 to suma 128 i sygnału 9. Najczęstszą przyczyną jest mechanizm OOM killer jądra systemu, jednak przekroczenie czasu wyłączenia (shutdown timeout) generuje ten sam kod, gdy aplikacja ignoruje SIGTERM. Należy wykonać docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container>, aby rozróżnić te sytuacje. true 137 oznacza przerwanie z powodu braku pamięci, natomiast 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 jest aktualnym formatem specyfikacji Compose i stanowi lepszy wybór dla nowych plików. Należy zachować mem_limit, jeśli pozostała część pliku korzysta już ze starszych kluczy najwyższego poziomu. Ustawienie obu parametrów dla jednej usługi utrudnia czytelność pliku, dlatego należy wybrać jeden 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 uwzględnia pamięć podręczną stron (page cache), którą jądro zwalnia pod obciążeniem zamiast wywoływać OOM kill. Należy uruchomić docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat i odczytać wartość anon, która reprezentuje rzeczywisty zestaw roboczy (working set), którego nie można odzyskać. Wysoka wartość file przy niskiej wartości anon oznacza, że kontener wykonuje operacje wejścia-wyjścia na dysku, a nie że jest bliski zakończenia działania.

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

Należy zarezerwować około 1GB dla jądra systemu, demona Docker, sshd, journald oraz powłoki użytkownika, a sumę limitów wszystkich kontenerów utrzymać poniżej pozostałych 7GB. Przed ustaleniem ostatecznych wartości należy przez dobę monitorować szczytowe zużycie anon dla każdego kontenera pod rzeczywistym obciążeniem i traktować sumę jako budżet, a nie cel do osiągnięcia.