Docker Compose dla początkujących: od jednej komendy do całego środowiska
Docker uczy uruchamiania pojedynczych kontenerów. Docker Compose rozwiązuje następny problem: realny projekt prawie nigdy nie składa się z jednego procesu. Masz aplikację, bazę danych, Redis, proces roboczy, kolejkę, narzędzia programistyczne i czasem observability. Compose pozwala opisać to w jednym pliku i uruchomić jedną komendą.
Ten poradnik jest praktyczny. Zbudujemy lokalne środowisko: aplikacja Node.js, PostgreSQL i Redis. Po drodze przejdziemy przez compose.yaml, porty, sieci, wolumeny, .env, kontrole gotowości, logi, debugowanie i sprzątanie.
Jeżeli Docker jako taki jest jeszcze niejasny, najpierw przeczytaj Docker dla początkujących. Ten artykuł zakłada, że wiesz już czym jest obraz, kontener, port i wolumen.
Wymagania i instalacja Docker Compose
W nowych instalacjach Compose jest wtyczką Docker CLI, więc komenda ma formę docker compose, a nie stare docker-compose. Stary binarny docker-compose v1 jest przestarzałe.
macOS i Windows
Zainstaluj Docker Desktop.
Docker Compose jest dołączony do Docker Desktop.
Po uruchomieniu Docker Desktop sprawdź wersję w terminalu.
docker version
docker compose versiondocker version
docker compose versionLinux / Ubuntu
Jeśli instalowałeś Docker Engine według aktualnej instrukcji Dockera, doinstaluj wtyczkę Compose:
sudo apt-get update
sudo apt-get install -y docker-compose-plugin
docker compose versionsudo apt-get update
sudo apt-get install -y docker-compose-plugin
docker compose versionJeśli działa tylko docker-compose, masz prawdopodobnie starą wersję i warto ją wymienić na plugin.
Co zbudujemy
app- mała aplikacja HTTP w Node.js.db- PostgreSQL z trwałym wolumenem.redis- Redis jako przykład pamięci podręcznej albo kolejki / zależności środowiska uruchomieniowego.Jedna prywatna sieć Compose, w której usługi widzą się po nazwach.
Kontrola gotowości dla Postgresa i warunek startu aplikacji po gotowości bazy.
Krok 1: przygotuj katalog projektu
mkdir compose-demo
cd compose-demo
mkdir appmkdir compose-demo
cd compose-demo
mkdir appW tym poradniku trzymamy aplikację w katalogu app, a plik Compose w głównym katalogu projektu.
Krok 2: napisz minimalną aplikację Node.js
Utwórz app/package.json:
{
"name": "compose-demo-app",
"version": "1.0.0",
"private": true,
"scripts": { "start": "node server.js" },
"dependencies": {
"pg": "^8.13.0",
"redis": "^4.7.0"
}
}{
"name": "compose-demo-app",
"version": "1.0.0",
"private": true,
"scripts": { "start": "node server.js" },
"dependencies": {
"pg": "^8.13.0",
"redis": "^4.7.0"
}
}Utwórz app/server.js:
const http = require("node:http");
const { Client } = require("pg");
const { createClient } = require("redis");
const port = Number(process.env.PORT || 3000);
const databaseUrl = process.env.DATABASE_URL;
const redisUrl = process.env.REDIS_URL;
async function checkPostgres() {
const client = new Client({ connectionString: databaseUrl });
await client.connect();
const result = await client.query("select now() as now");
await client.end();
return result.rows[0].now;
}
async function checkRedis() {
const client = createClient({ url: redisUrl });
await client.connect();
await client.set("compose-demo:last-check", new Date().toISOString());
const value = await client.get("compose-demo:last-check");
await client.disconnect();
return value;
}
const server = http.createServer(async (_req, res) => {
try {
const [postgresTime, redisValue] = await Promise.all([checkPostgres(), checkRedis()]);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, postgresTime, redisValue }));
} catch (error) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: error.message }));
}
});
server.listen(port, "0.0.0.0", () => {
console.log("App listening on 0.0.0.0:" + port);
});const http = require("node:http");
const { Client } = require("pg");
const { createClient } = require("redis");
const port = Number(process.env.PORT || 3000);
const databaseUrl = process.env.DATABASE_URL;
const redisUrl = process.env.REDIS_URL;
async function checkPostgres() {
const client = new Client({ connectionString: databaseUrl });
await client.connect();
const result = await client.query("select now() as now");
await client.end();
return result.rows[0].now;
}
async function checkRedis() {
const client = createClient({ url: redisUrl });
await client.connect();
await client.set("compose-demo:last-check", new Date().toISOString());
const value = await client.get("compose-demo:last-check");
await client.disconnect();
return value;
}
const server = http.createServer(async (_req, res) => {
try {
const [postgresTime, redisValue] = await Promise.all([checkPostgres(), checkRedis()]);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, postgresTime, redisValue }));
} catch (error) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: error.message }));
}
});
server.listen(port, "0.0.0.0", () => {
console.log("App listening on 0.0.0.0:" + port);
});Krok 3: dodaj Dockerfile aplikacji
Compose może budować obraz aplikacji z lokalnego Dockerfile. Utwórz app/Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY server.js ./
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]Dodaj też app/.dockerignore:
node_modules
npm-debug.log
.env
.git
coverage
dist
.nextnode_modules
npm-debug.log
.env
.git
coverage
dist
.nextKrok 4: pierwszy compose.yaml
Utwórz plik compose.yaml w głównym katalogu projektu:
services:
app:
build:
context: ./app
ports:
- "3000:3000"
environment:
PORT: "3000"
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres-data:services:
app:
build:
context: ./app
ports:
- "3000:3000"
environment:
PORT: "3000"
DATABASE_URL: postgres://app:app@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres-data:To jest serce Compose: trzy usługi, jedna sieć tworzona automatycznie i jeden nazwany wolumen dla danych Postgresa.
Krok 5: uruchom całe środowisko
docker compose up --builddocker compose up --buildPierwszy start zbuduje obraz aplikacji i pobierze obrazy Postgresa oraz Redisa. Po starcie sprawdź aplikację:
curl http://localhost:3000curl http://localhost:3000Odpowiedź powinna zawierać ok: true, czas z Postgresa i wartość zapisaną w Redisie.
docker compose up --build -d
docker compose psdocker compose up --build -d
docker compose psKrok 6: zrozum nazwy hostów w Compose
W Compose usługi w tej samej sieci widzą się po nazwie serwisu. Dlatego aplikacja używa hostów db i redis, a nie localhost.
Z komputera hosta Postgres jest dostępny przez
localhost:5432, bo wystawiliśmy port.Z kontenera
appPostgres jest dostępny przezdb:5432.Z kontenera
appRedis jest dostępny przezredis:6379.
To jeden z najczęstszych błędów początkujących: wpisują localhost w connection stringu aplikacji działającej w kontenerze i dziwią się, że baza nie odpowiada.
Krok 7: logi, shell i debugowanie
docker compose ps
docker compose logs app
docker compose logs -f db
docker compose exec app sh
docker compose exec db psql -U app -d app
docker compose exec redis redis-cli pingdocker compose ps
docker compose logs app
docker compose logs -f db
docker compose exec app sh
docker compose exec db psql -U app -d app
docker compose exec redis redis-cli pingJeśli aplikacja nie startuje, zacznij od docker compose ps i docker compose logs app. Nie zgaduj.
Krok 8: zmienne środowiskowe i plik .env
W demo wpisaliśmy hasło Postgresa w compose.yaml, bo to lokalny przykład. W projekcie lepiej przenieść konfigurację do .env, a prawdziwych sekretów nie commitować.
POSTGRES_USER=app
POSTGRES_PASSWORD=app
POSTGRES_DB=app
APP_PORT=3000POSTGRES_USER=app
POSTGRES_PASSWORD=app
POSTGRES_DB=app
APP_PORT=3000Następnie w Compose możesz użyć interpolacji:
services:
app:
ports:
- "${APP_PORT}:3000"
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
db:
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}services:
app:
ports:
- "${APP_PORT}:3000"
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
db:
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}Do repo dodaj raczej .env.example, a nie prawdziwe .env z sekretami.
Krok 9: wolumeny i reset bazy
Dane Postgresa są w named volume postgres-data. Dzięki temu restart kontenera nie kasuje bazy.
docker compose down
docker compose up -d
# dane nadal istniejądocker compose down
docker compose up -d
# dane nadal istniejąJeśli chcesz świadomie wyczyścić bazę, usuń zestaw usług razem z wolumenami:
docker compose down -vdocker compose down -vUwaga: down -v usuwa dane.
Krok 10: rebuild, restart i praca nad kodem
docker compose build app
docker compose up -d appdocker compose build app
docker compose up -d appPo zmianie samej konfiguracji Compose zwykle wystarczy:
docker compose up -ddocker compose up -dDo pracy programistycznej można dodać bind mount kodu i osobny target dev, ale najpierw warto mieć działający prosty wariant.
Krok 11: profiles, czyli usługi opcjonalne
Compose profiles przydają się, gdy część usług nie ma startować zawsze.
services:
adminer:
image: adminer:latest
profiles: ["tools"]
ports:
- "8080:8080"services:
adminer:
image: adminer:latest
profiles: ["tools"]
ports:
- "8080:8080"docker compose --profile tools up -ddocker compose --profile tools up -dKrok 12: Compose w pracy z AI i agentami
Compose jest świetny dla zespołów pracujących z AI, bo agent kodujący nie musi zgadywać, jak uruchomić bazę, pamięć podręczną, kolejkę czy lokalny emulator. Ma jeden plik i jedną komendę.
Nowa osoba w zespole odpala
docker compose up -dzamiast instalować Postgresa i Redisa globalnie.Agent AI może uruchamiać testy integracyjne w takim samym środowisku jak programista.
CI może użyć tego samego modelu zależności, tylko z innymi sekretami i bez wystawiania zbędnych portów.
Krok 13: kiedy Compose przestaje wystarczać
Docker Compose nie jest Kubernetesem. Jest świetny do lokalnej pracy programistycznej, prostych środowisk testowych i małych wdrożeń, ale nie rozwiązuje sam z siebie skalowania, wdrożeń kroczących, automatycznego skalowania, zarządzania sekretami czy zaawansowanej orkiestracji.
Do lokalnego zestawu usług i CI - Compose jest bardzo dobry.
Do prostego VPS-a - Compose bywa wystarczający, jeśli świadomie obsłużysz backupy, update’y i monitoring.
Do większego systemu produkcyjnego - zwykle wchodzą platformy managed, Kubernetes, Nomad albo PaaS.
Najczęstsze błędy początkujących
Używanie
localhostmiędzy kontenerami zamiast nazw usług, np.dbalboredis.Trzymanie danych bazy bez wolumenu i utrata danych po re-create kontenera.
Commitowanie
.envz prawdziwymi sekretami.Brak kontroli gotowości dla bazy i start aplikacji zanim baza jest gotowa.
Wystawianie na hosta portów, które nie muszą być dostępne poza siecią Compose.
Mylenie
docker compose downzdocker compose down -v. Druga komenda usuwa wolumeny.Traktowanie Compose jako produkcyjnego orchestratora do wszystkiego.
Minimalna checklista dobrego compose.yaml
Każda usługa ma czytelną nazwę.
Aplikacja używa nazw usług jako hostów, nie
localhost.Bazy danych mają named volumes.
Sekrety nie są zapisane na stałe w obrazie ani w repo.
Usługi zależne od bazy mają kontrolę gotowości albo logikę ponawiania prób.
Wystawiasz tylko te porty, które faktycznie muszą być dostępne z hosta.
README opisuje
up,down,logs,exec, reset danych i typowe błędy.
Docker Compose jest najlepszym następnym krokiem po podstawach Dockera. Zamiast pamiętać kilka długich komend docker run, opisujesz środowisko w pliku, który rozumie programista, agent AI i CI.
Najważniejsze rzeczy do zapamiętania: usługi komunikują się po nazwach, dane trwałe trzymaj w wolumenach, konfigurację podawaj przez zmienne środowiskowe, logi sprawdzaj przez docker compose logs, a down -v traktuj jak świadomy reset danych.