Jak wdrażać aplikacje Docker Compose na VPS
Praktyczny, produkcyjny poradnik Docker Compose v2 na Ubuntu: instalacja silnika, plik Compose z sieciami i wolumenami, sekrety, reverse proxy z przodu, aktualizacje bez paniki i kopie zapasowe nazwanych wolumenów.

Docker Compose to zwykły sposób na mały stos na jednym VPS: aplikacja WWW, baza, cache Redis i reverse proxy w jednym pliku YAML. To nie Kubernetes. Nie dostaniesz failoveru na wiele węzłów. Dostaniesz powtarzalną, recenzowalną konfigurację, którą odtworzysz na nowym VPS Hiddence w minuty — a właśnie tego potrzebuje większość projektów pobocznych i małych produktów.
Ten artykuł zakłada, że chcesz coś bliższego produkcji niż docker run w piątkowy wieczór. Zainstalujemy Docker Engine i wtyczkę Compose, utworzymy użytkownika wdrożeniowego bez root na co dzień, napiszemy plik Compose z healthcheckami i polityką restartu, trzymamy sekrety poza Git, wystawimy do internetu tylko porty reverse proxy i opiszemy rutynę aktualizacji oraz kopii. Przykładowy stos to typowa aplikacja WWW plus PostgreSQL plus Caddy, ale ten sam wzorzec działa dla Node, PHP, Python albo zestawu workerów.
Dlaczego Compose na jednym VPS nadal ma sens
Ludzie skaczą do Kubernetesa, bo tak wypada, a potem spędzają weekend na YAML dla jednej witryny. Compose zostaje czytelny. Wersjonujesz plik, dokumentujesz zmienne środowiskowe i możesz zrobić diff zmiany przed zastosowaniem. Izolacja wystarczy: skompromitowany kontener aplikacji nie powinien trzymać gniazda UNIX bazy, jeśli użyłeś sieci i użytkowników z minimalnymi uprawnieniami. Limity zasobów zatrzymują jeden wyciek pamięci przed zamrożeniem całego VPS.
- Jeden plik opisuje cały stos
- Nazwane wolumeny przeżywają odtworzenie kontenerów
- Wewnętrzne sieci Docker trzymają Postgres z dala od publicznego internetu
- restart: unless-stopped pokrywa większość rebootów
- Łatwo skopiować na drugi VPS, gdy pierwszy przestaje wystarczać
Wymagania
Użyj VPS z wystarczającą ilością RAM na aplikację plus Postgres. Plan 2 GB to realistyczne minimum dla aplikacji + bazy + proxy. Zakładamy Ubuntu 24.04. Nie instaluj Dockera z przypadkowego Snapa bez czytania, co robi z cgroups; kroki poniżej używają oficjalnego repozytorium apt Dockera.
- Ubuntu 22.04/24.04, root lub sudo
- Zalecane 2 GB RAM (1 GB tylko dla malutkich stosów bez Postgres)
- Domena wskazująca na VPS, jeśli chcesz automatyczny HTTPS
- Git albo inny sposób skopiowania projektu na serwer
Krok 1. Zainstaluj Docker Engine i Compose v2
Compose v2 to wtyczka wywoływana jako docker compose (ze spacją), nie stary binarny docker-compose w Pythonie. Instaluj oba z repozytorium Dockera, żeby dostawać bieżące łatki bezpieczeństwa.
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
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" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version
docker compose versionKrok 2. Użytkownik wdrożeniowy i układ katalogów
Compose jako root działa, ale dedykowany użytkownik w grupie docker jest czyściejszy. Projekt kładź w /opt albo /srv, nie w /root, żeby kopie i uprawnienia były oczywiste. Nigdy nie dawaj światu zapisu do katalogu z .env.
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app
# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/appKrok 3. Napisz plik Compose z myślą o produkcji
Plik poniżej to szablon. Zamień obraz aplikacji na swój. Postgres nie jest publikowany na 0.0.0.0:5432 — na zewnątrz tylko Caddy na 80 i 443. Healthchecki nie pozwalają reverse proxy słać ruchu do aplikacji, która jeszcze migruje bazę. Przypinaj tagi obrazów; latest to przepis na niespodziewane awarie.
# /srv/app/compose.yaml
services:
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
app:
condition: service_healthy
app:
image: ghcr.io/example/webapp:1.4.2
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
interval: 10s
timeout: 3s
retries: 10
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- backend
networks:
frontend: {}
backend: {}
volumes:
pgdata:
caddy_data:
caddy_config:Krok 4. Sekrety, .env i Caddyfile
Hasła trzymaj w .env na serwerze, chmod 600, i nigdy nie commituj tego pliku. Hasła generuj openssl rand -base64 32. Caddyfile musi tylko zrobić reverse proxy na nazwę usługi aplikacji w sieci Docker.
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs
# /srv/app/Caddyfile
app.example.com {
encode gzip
reverse_proxy app:8000
}
chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100Krok 5. Firewall, logi i limity zasobów
UFW powinien zezwalać tylko na 22, 80 i 443. Docker czasem omija UFW dla opublikowanych portów; jeśli potrzebujesz twardego firewalla na hoście, sprawdź aktualną interakcję Docker + UFW dla Twojej Ubuntu albo publikuj porty tylko na 127.0.0.1 i stawiaj Caddy na hoście. Ustaw limity pamięci w Compose, żeby Postgres nie zjadł VPS.
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
# Optional in compose.yaml under app or db:
# deploy:
# resources:
# limits:
# memory: 512M
# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart dockerKrok 6. Aktualizacje i rollbacki
Nudna aktualizacja, która działa: ściągnij nowe obrazy, up -d, obserwuj healthchecki, trzymaj poprzedni tag w git, żeby przypiąć wstecz. Nie rób docker compose down na produkcyjnej bazie, chyba że chcesz przestać przyjmować ruch. down -v kasuje wolumeny — to wycieranie, nie restart.
cd /srv/app
git pull # if the Compose file lives in git
# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps
# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v # this deletes named volumesKopie zapasowe nazwanych wolumenów
Migawka VPS jest dobra. Logiczny dump Postgresa jest lepszy, jeśli odtwarzasz na inną maszynę. Zaplanuj to cronem. Przetestuj odtworzenie raz, albo nie masz kopii — masz pliki, co do których masz nadzieję, że są kopiami.
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz
# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~
# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app appRozwiązywanie problemów
Czytaj docker compose ps i docker compose logs usługa. Niepowodzenia pull obrazu to zwykle limity ghcr/Docker Hub albo prywatny obraz bez logowania. 502 z Caddy znaczy, że aplikacja nie jest healthy albo hostname proxy jest zły (użyj nazwy usługi Compose, nie localhost, z wnętrza kontenera Caddy). Błędy uprawnień na wolumenach często znaczą, że obraz działa jako uid 1000, a katalog na hoście należy do root.
- compose: command not found — zainstalowałeś starą nazwę binarki; użyj docker compose
- port is already allocated — coś innego trzyma 80/443 (Apache, Nginx, inny Caddy)
- database connection refused — aplikacja wystartowała przed Postgresem; healthcheck + depends_on z condition
- dysk pełny — docker system df, potem ostrożny prune nieużywanych obrazów
- permission denied na docker.sock — użytkownik nie w grupie docker albo potrzebujesz nowej sesji logowania
Bezpieczeństwo
Nie publikuj portów bazy. Nie włączaj privileged: true. Nie montuj /var/run/docker.sock do kontenera aplikacji, chyba że celowo piszesz menedżer kontenerów. Aktualizuj silnik. Skanuj własne obrazy, jeśli je budujesz. .env na dysku nadal jest plikiem sekretu — ogranicz, kto może ssh na skrzynkę.
- Brak publicznych 5432 / 3306 / 6379
- Przypinaj digesty obrazów albo przynajmniej niemutowalne tagi
- chmod 600 .env
- Unattended-upgrades systemu hosta nadal mają znaczenie
- Jeden projekt Compose na aplikację zmniejsza promień rażenia
Wskazówki
- compose.yaml w git; .env.example bez prawdziwych sekretów
- Profile dla opcjonalnych workerów (docker compose --profile workers up -d)
- Obserwuj docker stats, gdy dobierasz rozmiar VPS
- Bind-mount kodu źródłowego to nie wzorzec produkcyjny — zapiecz obraz
- Gdy stos urośnie do kilku VPS, wtedy patrz na orkiestrację — nie wcześniej
Prawie produkcyjny Compose na jednym VPS to: oficjalne pakiety Dockera, zamknięty .env, YAML, który nie publikuje bazy, healthchecki, reverse proxy na 80/443, UFW, tagi obrazów, do których możesz wrócić, i dumpy Postgresa, które choć raz odtworzyłeś. Zacznij od szablonu z tego artykułu, podmień obraz aplikacji i traktuj docker compose down -v jak polecenie niszczące z takim samym szacunkiem jak rm -rf.