SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor

Jak ustawić limity pamięci w Docker Compose

Dowiedz się, jak skonfigurować parametry deploy.resources, aby uniknąć błędu exit 137 i awarii serwera VPS. Ogranicz zużycie RAM przez kontenery za pomocą cgroups.

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 zastosowanie

Istnieją dwa sposoby zapisu tego samego parametru, co bywa mylące.

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 pochodzi ze schematu Swarm i obecnie stanowi część specyfikacji Compose, czyli formatu, który odczytuje 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 najnowszym stabilnym wydaniem w dniu 2026-10-06). Przeskok nastąpił z v2 bezpośrednio do v5, pomijając 3 i 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 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. Zatem powszechna porada, że "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. Stosowanie mem_limit: 512m i 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 zamknięty.

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 (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 wybiera proces wewnątrz kontenera i wysyła do niego sygnał SIGKILL. Zabicie procesu o PID 1 kończy działanie kontenera. Kod wyjścia 137 to suma 128 i 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 oznacza, że sygnał SIGKILL został wysłany z innego powodu; 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 rozpoczynają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 sama maszyna wyczerpała pamięć 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 posiadają żadnych ograniczeń.

Przy użyciu 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 swoim zamknięciu. Sprawdź kolumnę czasu pracy (uptime) oraz licznik 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 (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

Wartość MEM USAGE w docker stats uwzględnia 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, ponieważ czysta pamięć podręczna jest zwalniana, zanim mechanizm OOM killer zostanie uruchomiony. Usługa taka jak samodzielnie hostowany serwer multimediów Jellyfin z tego właśnie powodu będzie stale wykazywać zużycie 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 page cache, który można zwolnić. Limit należy ustalać w oparciu o anon z uwzględnieniem marginesu, a nie w oparciu o sumę całkowitą. Plik memory.events ostatecznie rozstrzyga tę kwestię: licznik oom_kill większy od zera oznacza, że jądro systemu zakończyło 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 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ł na maszynie z 8GB 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.
  • Procesy w tle (background worker): limit 512m.
  • Serwis mediów lub plików: limit 2g, z czego większość zostanie wykorzystana na page cache.

Nie należy kopiować tych wartości bezkrytycznie do własnego stosu. Uruchom usługi pod rzeczywistym obciążeniem na dobę, monitoruj docker stats, sprawdź szczytową wartość anon dla każdego kontenera i dodaj 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, dopóki nie 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 parametru -XX:MaxRAMPercentage=75, aby dostosować rozmiar sterty (heap) do limitu cgroup, a nie do pamięci RAM 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 zainterweniuje. Podobnie jest w przypadku Ollama, gdzie należy użyć odpowiedniego 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 zapytania. 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, a w przypadku braku limitu zwraca 0. 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 on, ż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 ten sam kod pojawia się przy przekroczeniu czasu wyłączania, gdy aplikacja ignoruje sygnał SIGTERM. Uruchom docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container>, aby rozróżnić te sytuacje. true 137 oznacza zakończenie 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 jest aktualnym formatem specyfikacji Compose i stanowi lepszy wybór dla nowego pliku. Pozostaw mem_limit, jeśli reszta 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 zostaje zamknięty?

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. 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) niemożliwy do odzyskania. Wysoka wartość file przy niskiej wartości anon oznacza, że kontener wykonuje operacje wejścia/wyjścia na dysku, a nie że jest bliski zamknięcia.

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 sztywnych wartości warto 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 pełnego wykorzystania.