Instalacja Docker Compose na Ubuntu 24.04
Dowiedz się jak zainstalować Docker Engine i Compose v2 na Ubuntu 24.04. Poznaj konfigurację compose.yml oraz uniknij błędów z ufw przy publikacji portów.
Cel instrukcji
Docker Compose stanowi fundament dla niemal wszystkich usług opisanych na tej stronie. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — każda z tych instrukcji rozpoczyna się od polecenia utworzenia pliku compose, a niniejsza strona wyjaśnia znaczenie tego pliku. Na systemie Ubuntu 24.04 zostanie zainstalowany Docker Engine oraz plugin Compose v2 z oficjalnego repozytorium apt firmy Docker. Następnie zostanie uruchomiony stos dwóch usług: Miniflux (lekki czytnik RSS) oraz PostgreSQL. Ta konfiguracja obejmuje wszystkie wzorce stosowane w większych aplikacjach: przypięte obrazy, bazę danych z mechanizmem healthcheck, wolumen nazwany, sekrety w pliku .env oraz port udostępniony wyłącznie dla localhost.
Instalacja zajmuje pięć minut. Pozostała część instrukcji dotyczy problemów, które pojawiają się w późniejszym etapie: uprawnień grupy docker (równych uprawnieniom root), omijania reguł ufw przez udostępnione porty oraz flagi w docker compose down, która usuwa bazę danych bez dodatkowego potwierdzenia.
Wymagania wstępne: czysty system Ubuntu 24.04 na maszynie KVM VPS, użytkownik z uprawnieniami sudo oraz minimum 1 GB pamięci RAM. Istniejąca instalacja Docker jest dopuszczalna — pierwsza sekcja opisuje proces usuwania poprzednich wersji.
Instalacja z repozytorium Docker, a nie z repozytorium Ubuntu
Przed wykonaniem pierwszej komendy należy uniknąć dwóch błędów. Pakiet docker.io z repozytorium Ubuntu działa, lecz posiada opóźnione wersje względem wydawnictw Docker i posiada nieprawidłową strukturę wtyczek. Samodzielny plik binarny docker-compose (z myślnikiem) to Compose v1: oparty na Pythonie, wycofany z użytku w 2023 roku; jest to powód błędów w starszych poradnikach. Współczesny Compose to docker compose (ze spacją), będący wtyczką CLI, instalowaną z tego samego repozytorium co silnik.
Jeśli system zawiera już powyższe komponenty, należy je najpierw usunąć — w tym docker-compose-v2 (własne pakowanie wtyczki przez Ubuntu), aby wszystkie elementy pochodziły z jednego repozytorium:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runcPackage 'docker.io' is not installed, so not removed to standardowy wynik na nowym VPS. Następnie należy dodać repozytorium Docker i dokonać instalacji:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginNależy zweryfikować wszystkie trzy warstwy:
docker --version
docker compose version
sudo docker run --rm hello-worldDwie pierwsze komendy wyświetlają ciągi znaków wersji — Docker Compose version v2.x.x potwierdza obecność wtyczki zamiast wycofanego pliku binarnego v1. Komenda hello-world powinna zakończyć się wynikiem Hello from Docker!. Pakiet umożliwia uruchomienie usługi przy starcie systemu; systemctl is-enabled docker wyświetla enabled.
Grupa docker należy do root — podejmij świadomą decyzję
Obecnie każda komenda docker wymaga uprawnień sudo, ponieważ gniazdo (socket) demona w /var/run/docker.sock należy do użytkownika root oraz grupy docker. Brak członkostwa w grupie powoduje najczęściej wyszukiwany błąd Docker:
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sockStandardowe rozwiązanie:
sudo usermod -aG docker $USERCzłonkostwo w grupie jest aktywowane podczas logowania, więc błąd występuje w bieżącej sesji powłoki. Aby zastosować zmiany w bieżącej sesji, należy uruchomić newgrp docker, lub wylogować się i zalogować ponownie; polecenie id powinno wówczas wyświetlić docker na liście grup.
Kluczowa informacja: członkostwo w grupie docker daje uprawnienia root na hoście. Nie są to uprawnienia "zbliżone do root" ani "podwyższone" — jest to pełny root. Każdy użytkownik w tej grupie może uruchomić docker run --rm -it -v /:/host alpine chroot /host i przejąć kontrolę nad całym systemem plików bez podawania hasła. Grupa ta służy wygodzie, a nie izolacji.
Prawdziwą alternatywą jest tryb rootless w Dockerze — daemon działa wtedy jako użytkownik nieposiadający uprawnień administratora. Wiąże się to z następującymi kosztami: porty poniżej 1024 wymagają dodatkowej konfiguracji, sieć działa przez shim w przestrzeni użytkownika (userspace), co generuje mierzalny narzut, a niektóre obrazy mogą działać nieprawidłowo bez uprawnień root. Na serwerze VPS z jednym administratorem, który posiada uprawnienia sudo, zmiana ta nie wpływa na praktykę i jest przyjmowana przez wszystkie niniejsze poradniki — należy jednak pamiętać, aby nie nadawać tych uprawnień, jakby były one mniej ryzykowne niż sudo.
Budowa pliku compose
Dla każdego stosu należy utworzyć osobny katalog — nazwa katalogu staje się nazwą projektu, która jest prefiksem dla kontenerów, sieci i wolumenów:
sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/minifluxNależy utworzyć compose.yml (nowoczesna nazwa; docker-compose.yml nadal działa). Należy pominąć stary klucz version: — jest przestarzały, a Compose wyświetla ostrzeżenie po jego wykryciu.
services:
miniflux:
image: miniflux/miniflux:2.2.9
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=miniflux
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:Każda powyższa linia to decyzja. Należy wdrażać je pojedynczo.
Pinowanie wersji obrazów — :latest plus pull to niekontrolowana aktualizacja
postgres:16-alpine, a nie postgres:latest. Tag nie jest zamrożony: :latest przy każdym pobieraniu (pull) odwołuje się do najnowszego obrazu przesłanego przez maintainera. W połączeniu z rutynową praktyką aktualizacji — docker compose pull && docker compose up -d — oznacza to, że :latest powoduje skoki wersji głównych w momencie ich wydania przez dostawcę, a nie w wybranym przez użytkownika terminie. W przypadku PostgreSQL nie jest to teoria: niespodziewana aktualizacja z wersji 16 na 17 spowoduje pętlę restartów kontenera z powodu niezgodności katalogu danych, ponieważ aktualizacje wersji głównych Postgres wymagają procedury dump i restore, a nie samego restartu.
Należy pinować co najmniej wersję główną (postgres:16-alpine odpowiada wydaniom patchowym 16.x), a aplikacje pinować do konkretnego wydania, np. miniflux/miniflux:2.2.9 — należy sprawdzić stronę wydania projektu i użyć aktualnej wersji podczas tworzenia pliku. Wówczas aktualizacja staje się celową, jednolinijkową edycją widoczną w git diff.
Publikacja na 127.0.0.1, ponieważ Docker omija ufw
"127.0.0.1:8080:8080" — adres hosta, port hosta, port kontenera. Większość tutoriali używa "8080:8080", co jest skrótem od 0.0.0.0:8080:8080: nasłuchiwania na wszystkich interfejsach, w tym publicznym.
Występuje tutaj pułapka, która dotyka niemal każdego. Docker publikuje port poprzez zapisanie reguły DNAT, która nadpisuje cel pakietu na wewnętrzny adres IP kontenera przed filtrowaniem. Pakiet wybiera ścieżkę FORWARD i nigdy nie trafia do INPUT, gdzie znajdują się reguły ufw. sudo ufw deny 8080 zgłasza sukces, ufw status pokazuje odrzucenie portu, a usługa nadal odpowiada na żądania z całego internetu. Firewall nie jest uszkodzony; jest omijany przez projekt systemu. Dlaczego Docker omija ufw i jak skutecznie filtrować ruch kontenerów wyjaśnia ten mechanizm oraz rozwiązanie DOCKER-USER dla portów, które muszą pozostać publiczne.
Praktyka eliminująca ten problem: należy wiązać (bind) publikowane porty z 127.0.0.1 (chyba że istnieje konkretny powód, aby tego nie robić) i umieścić reverse proxy przed wszystkim, co ma być dostępne publicznie. Jest to dokładnie to, co przewodnik po reverse proxy Traefik buduje jako kolejny krok — jeden kontener obsługujący porty 80 i 443, który przekierowuje ruch do pozostałych usług za pomocą nazw hostów i TLS. (Jeśli korzystasz ze starszej wersji Traefik v2? Przewodnik migracji z Traefik v2 do v3 opisuje zmiany nazw i reguł.)
Zweryfikuj wiązanie po uruchomieniu stosu: sudo ss -tlnp | grep 8080 powinno pokazywać 127.0.0.1:8080, a nie 0.0.0.0:8080 lub *:8080.
Named volumes vs bind mounts
db-data:/var/lib/postgresql/data to nazwany wolumen (named volume): Docker tworzy i zarządza katalogiem w /var/lib/docker/volumes/ i montuje go w kontenerze. Alternatywą jest bind mount, ./data:/var/lib/postgresql/data, który mapuje wybrany przez użytkownika ścieżkę na hoście.
Podział stosowany w praktyce: nazwane wolumeny wyłącznie dla danych dotykanych przez kontenery — przede wszystkim bazy danych, ponieważ Docker inicjalizuje wolumen z uprawnieniami oczekiwanymi przez obraz, co zapewnia poprawne działanie uprawnień do plików. Bind mounts dla plików edytowanych z poziomu hosta — pliki konfiguracyjne edytowane edytorem tekstu, biblioteki mediów przesyłane przez rsync, wszystko, czego ścieżka ma być jawna. Klasycznym błędem bind mount jest własność plików: kontener działa na UID 999, a katalog na hoście należy do UID 1000, co powoduje błąd permission denied w logach aplikacji podczas startu. Nazwane wolumeny eliminują tę klasę błędów, kosztem przechowywania danych w ścieżce zarządzanej przez Docker — szczegóły poniżej.
environment i .env — trzymaj sekrety poza git
${POSTGRES_PASSWORD} nie jest odczytywane z powłoki (shell); Compose interpoluje je z pliku o nazwie .env znajdującego się obok compose.yml. Należy go utworzyć:
cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignoreWartości należy generować za pomocą openssl rand -hex 24. Celowo używa się formatu hex, a nie base64: hasło to trafia do ciągu połączenia DATABASE_URL, natomiast znaki /, + i = generowane przez base64 powodują błędy parsowania URL — błąd ten objawia się jako błąd uwierzytelniania, a nie błąd składni, co skutkuje utratą czasu. Linia .gitignore musi znaleźć się przed pierwszym commitem: plik compose jest bezpieczny do publikacji i wersjonowania, plik .env nigdy nie powinien być, a sekret, który trafił do historii git, należy traktować jako wymagający rotacji. Jeśli uruchomisz stos z brakującą zmienną, Compose wyświetli ostrzeżenie i będzie kontynuował pracę z pustym ciągiem znaków — co w przypadku hasła do Postgres oznacza nieudaną instalację:
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.docker compose config drukuje w pełni zinterpolowany plik — jest to najszybszy sposób na sprawdzenie, jakie dane faktycznie otrzymają kontenery; należy pamiętać, że wynik zawiera sekrety.
depends_on nie czeka na nic — chyba że dodasz healthcheck
Samo użycie depends_on: [db] kontroluje jedynie kolejność uruchamiania: Compose uruchamia najpierw Postgres, a chwilę później aplikację, podczas gdy Postgres potrzebuje jeszcze kilku sekund na rozpoczęcie przyjmowania połączeń. Aplikacja próbuje połączyć się z bazą, napotyka błąd i kończy działanie lub podejmuje próby ponowne połączenia, zależnie od implementacji.
Niezawodna metoda to ta użyta w powyższym pliku: usługa db definiuje healthcheck (Postgres dostarcza pg_isready właśnie do tego celu), a aplikacja deklaruje depends_on z parametrem condition: service_healthy. Compose uruchamia bazę danych, sprawdza status co 10 sekund i uruchamia Miniflux dopiero po pomyślnym przejściu testu. Jeśli baza danych nie osiągnie stanu healthy (np. błędne hasło, uszkodzony wolumen), aplikacja nie zostanie uruchomiona, a Compose poinformuje, która zależność zawiodła:
dependency failed to start: container miniflux-db-1 is unhealthyKomunikat ten wskazuje na docker compose logs db, gdzie znajduje się rzeczywisty błąd.
restart: unless-stopped
restart: unless-stopped dla obu usług oznacza, że kontenery zostaną uruchomione po awarii oraz po restarcie VPS, ale pozostaną wyłączone, jeśli użytkownik celowo wykonał docker compose stop. Alternatywa always przywraca kontenery nawet po ręcznym zatrzymaniu — co rzadko jest pożądane. Bez polityki restartu, restart systemu po aktualizacji jądra o 4 rano po cichu wyłączy usługi, dopóki nie zostaną zauważone.
Codzienne operacje
Wszystkie codzienne czynności obejmują pięć komend, wykonywanych z katalogu projektu.
docker compose up -d # create and start; idempotent, recreates only what changed
docker compose ps # status, ports, and health of this project's containers
docker compose logs -f miniflux # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d # upgrade to the pinned tags
docker compose down # stop and remove containers and the networkKomenda up -d jest bezpieczna do wielokrotnego uruchamiania — porównuje ona stan pliku ze stanem faktycznym i modyfikuje tylko te usługi, których konfiguracja lub obraz uległy zmianie. Para komend upgrade pobiera aktualne wersje wskazane przez przypięte tagi: wydania patch pod wersją postgres:16-alpine; w przypadku dokładnego przypięcia (exact pin) nic się nie pobierze, dopóki tag nie zostanie edytowany — to jest cel tego mechanizmu. Po aktualizacjach gromadzą się stare obrazy; zwolnij miejsce na dysku za pomocą docker image prune -f.
Komenda niszcząca: docker compose down jest bezpieczna — kontenery i sieć są tymczasowe, a dane znajdują się w wolumenie. Komenda docker compose down -v usuwa również wolumeny nazwane. Oznacza to natychmiastowe i nieodwracalne usunięcie bazy danych, bez prośby o potwierdzenie. Flaga -v służy do usuwania eksperymentów; w przypadku stosu zawierającego rzeczywiste dane, należy traktować ją tak samo jak rm -rf. W przypadku /var/lib/docker/volumes/ nie istnieje kosz.
Aby uzyskać powłokę wewnątrz działającego kontenera: docker compose exec db psql -U miniflux otwiera dostęp do bazy danych, a docker compose exec miniflux sh otwiera powłokę w aplikacji.
Lokalizacja danych
Wolumeny nazwane otrzymują prefiks projektu. Przykładowo db-data w katalogu miniflux staje się miniflux_db-data:
docker volume ls
docker volume inspect miniflux_db-dataWyjście polecenia inspect zawiera istotną linię:
"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"Ten katalog stanowi bazę danych — jest własnością użytkownika root, znajduje się w systemie plików hosta i zachowuje trwałość po down, aktualizacjach oraz przebudowaniu kontenerów. Dane te muszą zostać uwzględnione w procesie tworzenia kopii zapasowych.
Kopia zapasowa nazwanego wolumenu
Standardowym schematem jest użycie tymczasowego kontenera, który montuje wolumen w trybie read-only obok katalogu hosta, a następnie wykonuje operację tar:
docker run --rm \
-v miniflux_db-data:/data:ro \
-v "$PWD":/backup \
alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .Nie wymaga to instalacji ani uruchomionych procesów w tle. Przywracanie polega na wykonaniu operacji odwrotnej — tar xzf do nowego, pustego wolumenu z tymi samymi punktami montowania.
W przypadku baz danych należy uwzględnić jedną kwestię: wykonanie operacji tar na uruchomionym katalogu danych Postgres może zapisać stan w trakcie zapisu, co uniemożliwi poprawne uruchomienie bazy. Należy albo użyć docker compose stop na czas trwania operacji tar, albo — co jest lepszym rozwiązaniem — wykonać zrzut logiczny, który jest spójny z założenia:
docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gzFlaga -T wyłącza pseudo-terminal przydzielany domyślnie przez Compose — przekazywanie wyjścia zrzutu przez TTY może spowodować uszkodzenie danych. Należy umieścić jedną z tych komend w cron i skopiować wynik poza VPS; kopia zapasowa znajdująca się na tej samej dysku co chronione dane jest jedynie kopią, a nie backupem. Przewodnik Nextcloud opisuje pełną procedurę harmonogramowania opartą dokładnie na tych dwóch schematach.
Tryby awarii oraz wyświetlane komunikaty
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — użytkownik nie należy jeszcze do grupy docker lub sesja została rozpoczęta przed dodaniem do grupy. Komenda id wyświetla grupy użytkownika; newgrp docker naprawia bieżącą powłokę, natomiast wylogowanie i ponowne zalogowanie rozwiązuje problem dla wszystkich grup.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — inny problem: usługa daemon nie działa. Komendy sudo systemctl status docker oraz sudo journalctl -u docker -n 50 podają przyczynę. Na serwerach VPS najczęstszą przyczyną jest brak miejsca na dysku — należy sprawdzić df -h /var/lib/docker w pierwszej kolejności.
Bind for 127.0.0.1:8080 failed: port is already allocated — inny kontener zajmuje już ten sam port hosta. Komenda docker ps wskazuje konkretny kontener; przyczyną jest zazwyczaj pozostałość po eksperymentalnym docker run sprzed kilku tygodni. Jeśli docker ps nie wykazuje konfliktów, port jest zajęty przez proces spoza Docker — sudo ss -tlnp | grep 8080 wskazuje jego nazwę.
yaml: line 14: did not find expected key — błąd wcięć w linii wskazanej lub bezpośrednio nad nią. Pliki Compose to format YAML: wymagane jest wcięcie o wielkości dwóch spacji, używane są wyłącznie spacje, a obecność znaku tabulatora powoduje błąd krytyczny. Komenda docker compose config służy do walidacji pliku bez uruchamiania usług; zaleca się jej używanie po każdej edycji.
Problem z ufw nie wyświetla żadnego błędu, co czyni go niebezpiecznym: wdrożenie przebiega pomyślnie, ufw status wygląda poprawnie, a skanowanie portów z zewnątrz nadal ujawnia bazę danych. Należy ponownie przeanalizować sekcję dotyczącą portów, sprawdzić każdą wpis ports: pod kątem braku prefiksu 127.0.0.1: oraz potwierdzić konfigurację z innej maszyny za pomocą curl http://your-vps-ip:8080 — oczekiwaną odpowiedzią jest connection refused.
W dalszej części przewodnik Traefik wyjaśnia, jak przekształcić ten pojedynczy stos w wiele aplikacji działających za jednym punktem wejścia HTTPS, a lista usług do self-hostingu w 2026 zawiera zestaw aplikacji do uruchomienia w tej konfiguracji.
Serwer gier, taki jak serwer Minecraft na VPS, stanowi odpowiedni projekt Compose do nauki podstaw.
FAQ
Dlaczego otrzymuję komunikat "permission denied while trying to connect to the Docker daemon socket"?
Użytkownik nie należy do grupy docker lub został do niej dodany po rozpoczęciu bieżącej sesji — członkostwo jest aktywne dopiero po ponownym zalogowaniu. Należy uruchomić sudo usermod -aG docker $USER, a następnie newgrp docker lub wylogować się i zalogować ponownie, a następnie potwierdzić za pomocą id. Grupa ta nadaje uprawnienia równe użytkownikowi root do hosta, dlatego należy dodawać tylko tych użytkowników, którym przyznałoby się uprawnienia sudo.
Czy polecenie docker compose down usuwa moje dane?
Standardowe polecenie docker compose down nie usuwa danych — usuwa kontenery oraz sieć projektu; nazwane wolumeny pozostają nienaruszone, a kolejne wywołanie up -d ponownie je podłączy. Polecenie docker compose down -v jest destrukcyjne: usuwa nazwane wolumeny, co oznacza usunięcie bazy danych bez potwierdzenia i możliwości cofnięcia operacji. Nigdy nie należy uruchamiać -v na stosie zawierającym rzeczywiste dane, chyba że posiada się zweryfikowaną kopię zapasową.
Jaka jest różnica między docker-compose a docker compose?
docker-compose (z myślnikiem) to Compose v1, samodzielny plik binarny Python, który osiągnął koniec cyklu życia (end of life) w 2023 roku i nie powinien być instalowany na nowych serwerach. docker compose (ze spacją) to Compose v2, wtyczka Go dla Docker CLI, instalowana jako docker-compose-plugin z repozytorium apt firmy Docker. Polecenia oraz pliki YAML są niemal w pełni kompatybilne, więc gdy stary poradnik podaje docker-compose up, należy wpisać docker compose up.
Dlaczego mogę uzyskać dostęp do kontenera Docker z internetu, mimo że ufw blokuje dany port?
Dzieje się tak, ponieważ Docker publikuje porty za pomocą reguł DNAT w łańcuchu PREROUTING w iptables, a przekierowane pakiety wędrują ścieżką FORWARD przez własne łańcuchy Dockera — nigdy nie trafiają do łańcucha INPUT, w którym obowiązują reguły ufw. W związku z tym ufw deny 8080 nie ma wpływu na opublikowany port kontenera. Należy naprawić to u źródła: publikować porty na 127.0.0.1: i zamiast tego udostępniać usługi poprzez reverse proxy.
Czy powinienem używać nazwanego wolumenu (named volume), czy bind mountu?
Nazwane wolumeny należy stosować do danych dostępnych wyłącznie dla kontenera — szczególnie dla baz danych, ponieważ Docker ustawia własność plików zgodnie z oczekiwaniami obrazu, co zapewnia poprawne uprawnienia. Bind mounty należy stosować do plików zarządzanych również z poziomu hosta: plików konfiguracyjnych podlegających edycji, przesyłanych mediów lub wszystkiego, czego ścieżka ma być jawna. Jeśli kontener nie uruchamia się, zwracając permission denied przy użyciu bind mountu, należy w pierwszej kolejności sprawdzić niezgodność identyfikatorów UID między hostem a kontenerem.