Przejdź do treści
DevOps

Docker Compose - Lokalne środowisko deweloperskie od A do Z

Opublikowano:
·6 min czytania·Autor: MDS Software Solutions Group

Docker Compose Lokalne

devops

Docker Compose - Lokalne środowisko deweloperskie od A do Z

Konfiguracja lokalnego środowiska deweloperskiego to jedno z pierwszych wyzwań, z jakimi mierzy się każdy zespół programistów. Nowy członek zespołu spędza często cały pierwszy dzień na instalowaniu baz danych, konfiguracji zmiennych środowiskowych i rozwiązywaniu konfliktów wersji. Docker Compose eliminuje te problemy, pozwalając uruchomić kompletne środowisko deweloperskie jednym poleceniem: docker compose up.

W tym artykule pokażemy krok po kroku, jak zbudować profesjonalne lokalne środowisko deweloperskie obejmujące frontend (Next.js), backend (.NET API), bazę danych (PostgreSQL), cache (Redis) oraz serwer e-mail (Mailhog). Omówimy składnię pliku docker-compose.yml, wolumeny, zmienne środowiskowe, sieci, health checki, hot reload, migracje bazodanowe oraz debugowanie w kontenerach.

Czym jest Docker Compose?#

Docker Compose to narzędzie do definiowania i uruchamiania wielokontenerowych aplikacji Docker. Zamiast ręcznie uruchamiać każdy kontener osobnym poleceniem docker run, definiujesz wszystkie usługi w jednym pliku YAML i zarządzasz nimi za pomocą prostych komend.

Kluczowe zalety Docker Compose:

  • Jeden plik konfiguracyjny - cała infrastruktura opisana w docker-compose.yml
  • Powtarzalność - identyczne środowisko na każdej maszynie deweloperskiej
  • Izolacja - brak konfliktów między projektami, każdy ma własne kontenery
  • Łatwy onboarding - nowy deweloper uruchamia docker compose up i pracuje
  • Wersjonowanie - plik compose jest częścią repozytorium, zmiany są śledzone w Git

Składnia pliku docker-compose.yml#

Plik docker-compose.yml to serce Docker Compose. Oto podstawowa struktura:

# Wersja specyfikacji (opcjonalna od Compose v2)
version: "3.9"

services:
  # Definicje usług (kontenerów)
  nazwa-uslugi:
    image: obraz:tag           # Gotowy obraz z Docker Hub
    build: ./sciezka           # Lub budowanie z Dockerfile
    ports:
      - "host:kontener"        # Mapowanie portów
    volumes:
      - ./local:/container     # Mapowanie wolumenów
    environment:               # Zmienne środowiskowe
      - KLUCZ=wartosc
    env_file:
      - .env                   # Plik ze zmiennymi
    depends_on:                # Zależności między usługami
      - inna-usluga
    networks:                  # Sieci
      - moja-siec
    restart: unless-stopped    # Polityka restartu

volumes:
  # Nazwane wolumeny (trwałe dane)
  dane-postgres:

networks:
  # Własne sieci
  moja-siec:
    driver: bridge

Każda sekcja services definiuje jeden kontener. Sekcja volumes deklaruje nazwane wolumeny do trwałego przechowywania danych, a networks definiuje sieci pozwalające kontenerom się komunikować.

Kompletne środowisko: Next.js + .NET API + PostgreSQL + Redis + Mailhog#

Przejdźmy do praktyki. Poniżej znajduje się kompletna konfiguracja środowiska deweloperskiego dla typowej aplikacji enterprise. Struktura projektu wygląda następująco:

projekt/
├── docker-compose.yml
├── docker-compose.override.yml
├── docker-compose.prod.yml
├── .env
├── .env.example
├── frontend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── package.json
│   └── src/
├── backend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   ├── MyApi.sln
│   └── src/
└── database/
    ├── init.sql
    └── migrations/

Główny plik docker-compose.yml#

version: "3.9"

services:
  # ─── Frontend (Next.js) ────────────────────────────────
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - NEXT_PUBLIC_API_URL=http://localhost:5000/api
      - WATCHPACK_POLLING=true
    depends_on:
      backend:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped

  # ─── Backend (.NET API) ────────────────────────────────
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    ports:
      - "5000:5000"
      - "5001:5001"
    volumes:
      - ./backend/src:/app/src
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:5000
      - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=myapp_dev;Username=devuser;Password=devpass123
      - Redis__ConnectionString=redis:6379
      - Email__SmtpHost=mailhog
      - Email__SmtpPort=1025
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    networks:
      - app-network
    restart: unless-stopped

  # ─── PostgreSQL ────────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
    environment:
      - POSTGRES_DB=myapp_dev
      - POSTGRES_USER=devuser
      - POSTGRES_PASSWORD=devpass123
      - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U devuser -d myapp_dev"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ─── Redis ─────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ─── Mailhog (serwer e-mail do testów) ────────────────
  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"
      - "8025:8025"
    networks:
      - app-network
    restart: unless-stopped

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

networks:
  app-network:
    driver: bridge

Ta konfiguracja uruchamia pięć usług, które razem tworzą kompletne środowisko deweloperskie. Frontend Next.js nasłuchuje na porcie 3000, backend .NET na porcie 5000, PostgreSQL na standardowym porcie 5432, Redis na 6379, a panel Mailhog jest dostępny pod adresem http://localhost:8025.

Wolumeny - trwałość danych#

Wolumeny w Docker Compose pełnią dwie kluczowe role: zapewniają trwałość danych między restartami kontenerów oraz umożliwiają synchronizację plików między hostem a kontenerem (niezbędne dla hot reload).

Nazwane wolumeny (dane trwałe)#

volumes:
  postgres-data:
    driver: local

Nazwane wolumeny (np. postgres-data) przechowują dane bazy danych niezależnie od cyklu życia kontenera. Usunięcie kontenera PostgreSQL poleceniem docker compose down nie usunie danych. Aby wyczyścić dane, trzeba jawnie użyć docker compose down -v.

Bind mounts (synchronizacja plików)#

volumes:
  - ./frontend/src:/app/src        # Kod źródłowy
  - ./frontend/public:/app/public  # Pliki statyczne
  - /app/node_modules              # Anonimowy wolumen - nie nadpisuj!

Bind mounts montują katalogi z hosta bezpośrednio w kontenerze. Zmiana pliku na hoście jest natychmiast widoczna w kontenerze, co jest fundamentem hot reload. Ważne jest użycie anonimowego wolumenu dla node_modules - bez tego katalog node_modules z hosta nadpisałby ten z kontenera, co prowadziłoby do problemów z zależnościami skompilowanymi dla innej platformy.

Zmienne środowiskowe#

Docker Compose obsługuje zmienne środowiskowe na kilka sposobów. Najlepsza praktyka to przechowywanie wartości w pliku .env:

Plik .env#

# .env (NIE commituj do Git!)
POSTGRES_DB=myapp_dev
POSTGRES_USER=devuser
POSTGRES_PASSWORD=devpass123
REDIS_PASSWORD=
NODE_ENV=development
API_PORT=5000
FRONTEND_PORT=3000

Plik .env.example#

# .env.example (commituj do Git jako szablon)
POSTGRES_DB=myapp_dev
POSTGRES_USER=devuser
POSTGRES_PASSWORD=CHANGE_ME
REDIS_PASSWORD=
NODE_ENV=development
API_PORT=5000
FRONTEND_PORT=3000

Użycie w docker-compose.yml#

services:
  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    ports:
      - "${POSTGRES_PORT:-5432}:5432"

Składnia ${POSTGRES_PORT:-5432} ustawia domyślną wartość 5432, jeśli zmienna nie jest zdefiniowana. Plik .env powinien być dodany do .gitignore, a .env.example commitowany do repozytorium jako szablon dla nowych deweloperów.

Networking - komunikacja między kontenerami#

Docker Compose automatycznie tworzy sieć dla każdego projektu. Kontenery w ramach jednej sieci mogą się komunikować używając nazw usług jako hostów DNS.

networks:
  app-network:
    driver: bridge

W naszej konfiguracji backend łączy się z PostgreSQL używając hosta postgres (nazwa usługi):

Host=postgres;Port=5432;Database=myapp_dev

A frontend komunikuje się z backendem przez backend:5000 wewnątrz sieci Docker. Natomiast z przeglądarki (po stronie klienta) komunikacja odbywa się przez localhost:5000, ponieważ przeglądarka działa poza siecią Docker.

Izolacja sieci między projektami#

Każdy projekt Docker Compose ma własną izolowaną sieć. Kontenery z projektu A nie widzą kontenerów z projektu B, nawet jeśli używają tych samych portów wewnętrznych. Konflikty mogą wystąpić jedynie na poziomie portów hosta - dlatego warto parametryzować porty zmiennymi środowiskowymi.

Health checks - kontrola gotowości usług#

Health checki to mechanizm sprawdzania, czy usługa jest w pełni gotowa do obsługi żądań. Bez nich depends_on gwarantuje jedynie, że kontener został uruchomiony, a nie że usługa jest gotowa.

postgres:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U devuser -d myapp_dev"]
    interval: 5s       # Co ile sprawdzać
    timeout: 5s        # Maksymalny czas oczekiwania na odpowiedź
    retries: 5          # Ile razy ponowić próbę przed uznaniem za unhealthy
    start_period: 10s   # Czas na rozruch (healthcheck nie oznacza unhealthy)

backend:
  depends_on:
    postgres:
      condition: service_healthy  # Czekaj aż PostgreSQL będzie healthy
    redis:
      condition: service_healthy  # Czekaj aż Redis będzie healthy

Bez health checków backend mógłby próbować połączyć się z bazą danych, zanim ta zdąży się uruchomić. Dyrektywa condition: service_healthy w depends_on zapewnia prawidłową kolejność startu.

Pliki Compose: development vs production#

W praktyce potrzebujemy różnych konfiguracji dla środowiska deweloperskiego i produkcyjnego. Docker Compose obsługuje nakładanie plików (override), co pozwala na eleganckie rozdzielenie konfiguracji.

docker-compose.yml (bazowa konfiguracja)#

Zawiera wspólne definicje usług, sieci i wolumenów - to co jest identyczne dla obu środowisk.

docker-compose.override.yml (development)#

# docker-compose.override.yml
# Automatycznie ładowany razem z docker-compose.yml
version: "3.9"

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - WATCHPACK_POLLING=true

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend/src:/app/src
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "5000:5000"
      - "5001:5001"   # Port debuggera

  postgres:
    ports:
      - "5432:5432"   # Dostęp z hosta (np. pgAdmin, DBeaver)

  redis:
    ports:
      - "6379:6379"   # Dostęp z hosta (np. RedisInsight)

docker-compose.prod.yml (production)#

# docker-compose.prod.yml
version: "3.9"

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        - NEXT_PUBLIC_API_URL=https://api.mojadomena.pl
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G

  postgres:
    environment:
      - POSTGRES_PASSWORD=${PROD_DB_PASSWORD}
    # Brak mapowania portów na hosta - dostęp tylko z sieci Docker
    ports: []

  redis:
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} --maxmemory 512mb
    ports: []

Uruchamianie:

# Development (domyślnie ładuje docker-compose.yml + docker-compose.override.yml)
docker compose up

# Production (jawne wskazanie plików)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

W środowisku produkcyjnym porty baz danych nie są eksponowane na host, obrazy są budowane z produkcyjnych Dockerfile (wieloetapowe buildy, optymalizacja rozmiaru), a zasoby kontenerów są limitowane.

Hot reload - automatyczne przeładowanie kodu#

Hot reload to kluczowa funkcja dla produktywności deweloperów. Każda zmiana w kodzie powinna być natychmiast widoczna w przeglądarce lub w logach serwera.

Hot reload dla Next.js#

# frontend/Dockerfile.dev
FROM node:20-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

# Next.js domyślnie obsługuje hot reload
# WATCHPACK_POLLING jest potrzebne na Windows/WSL2
ENV WATCHPACK_POLLING=true

EXPOSE 3000
CMD ["npm", "run", "dev"]

Kluczowe elementy to wolumen montujący kod źródłowy (./frontend/src:/app/src) oraz zmienna WATCHPACK_POLLING=true, która jest niezbędna na systemach Windows z WSL2, gdzie standardowe powiadomienia o zmianach plików (inotify) mogą nie działać poprawnie.

Hot reload dla .NET#

# backend/Dockerfile.dev
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app

COPY *.sln .
COPY src/**/*.csproj ./src/
RUN dotnet restore

COPY . .

EXPOSE 5000 5001
CMD ["dotnet", "watch", "run", "--project", "src/MyApi", "--urls", "http://+:5000"]

Polecenie dotnet watch run automatycznie rekompiluje i restartuje aplikację przy każdej zmianie pliku .cs. Wolumen montujący ./backend/src:/app/src zapewnia synchronizację plików.

Migracje bazodanowe w kontenerach#

Migracje bazodanowe powinny być uruchamiane automatycznie przy starcie kontenera lub jako osobna usługa.

Podejście 1: Skrypt inicjalizacyjny PostgreSQL#

-- database/init.sql
-- Uruchamiany automatycznie przy pierwszym starcie kontenera PostgreSQL

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Tabele początkowe
CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS audit_log (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    action VARCHAR(100) NOT NULL,
    details JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Podejście 2: Osobna usługa migracji#

services:
  migrations:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    command: ["dotnet", "ef", "database", "update", "--project", "src/MyApi"]
    environment:
      - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=myapp_dev;Username=devuser;Password=devpass123
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network
    restart: "no"  # Uruchom raz i zakończ

Osobna usługa migracji uruchamia się raz, wykonuje migracje i kończy pracę. Dzięki restart: "no" kontener nie jest restartowany po zakończeniu.

Podejście 3: Entrypoint z migracjami#

# backend/entrypoint.sh
#!/bin/bash
set -e

echo "Uruchamianie migracji..."
dotnet ef database update --project src/MyApi

echo "Uruchamianie aplikacji..."
exec dotnet watch run --project src/MyApi --urls "http://+:5000"
backend:
  entrypoint: ["/bin/bash", "./entrypoint.sh"]

To podejście uruchamia migracje automatycznie przed startem aplikacji. Jest wygodne, ale wydłuża czas uruchamiania kontenera.

Debugowanie w kontenerach#

Debugowanie aplikacji działających w kontenerach wymaga odpowiedniej konfiguracji, ale nie jest trudne.

Debugowanie .NET w kontenerze (Visual Studio Code)#

Dodaj konfigurację debuggera w .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker .NET Attach",
      "type": "docker",
      "request": "attach",
      "platform": "netCore",
      "sourceFileMap": {
        "/app/src": "${workspaceFolder}/backend/src"
      }
    }
  ]
}

Debugowanie Next.js w kontenerze#

Dodaj flagę debugowania do polecenia startowego:

frontend:
  command: ["node", "--inspect=0.0.0.0:9229", "node_modules/.bin/next", "dev"]
  ports:
    - "3000:3000"
    - "9229:9229"   # Port debuggera Node.js

Konfiguracja VS Code:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker Next.js Attach",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "remoteRoot": "/app",
      "localRoot": "${workspaceFolder}/frontend"
    }
  ]
}

Logi i diagnoza problemów#

# Logi konkretnej usługi
docker compose logs backend

# Logi w czasie rzeczywistym (follow)
docker compose logs -f backend postgres

# Logi z ostatnich 100 linii
docker compose logs --tail=100 backend

# Wejście do kontenera (interaktywna powłoka)
docker compose exec backend bash

# Sprawdzenie stanu kontenerów
docker compose ps

# Sprawdzenie health checków
docker inspect --format='{{.State.Health.Status}}' projekt-postgres-1

Przydatne polecenia Docker Compose#

Oto lista najczęściej używanych komend, które przyspieszą codzienną pracę:

# ─── Zarządzanie cyklem życia ───────────────────────────
docker compose up                    # Uruchom wszystko
docker compose up -d                 # Uruchom w tle (detached)
docker compose up --build            # Przebuduj obrazy i uruchom
docker compose up backend postgres   # Uruchom tylko wybrane usługi
docker compose down                  # Zatrzymaj i usuń kontenery
docker compose down -v               # Zatrzymaj i usuń kontenery + wolumeny
docker compose stop                  # Zatrzymaj kontenery (bez usuwania)
docker compose start                 # Uruchom zatrzymane kontenery
docker compose restart backend       # Restart konkretnej usługi

# ─── Budowanie ──────────────────────────────────────────
docker compose build                 # Zbuduj wszystkie obrazy
docker compose build --no-cache      # Zbuduj bez cache
docker compose build backend         # Zbuduj konkretny obraz

# ─── Diagnostyka ────────────────────────────────────────
docker compose ps                    # Status kontenerów
docker compose logs -f               # Logi wszystkich usług (follow)
docker compose logs -f backend       # Logi konkretnej usługi
docker compose top                   # Procesy w kontenerach

# ─── Interakcja z kontenerami ───────────────────────────
docker compose exec postgres psql -U devuser -d myapp_dev    # Konsola PostgreSQL
docker compose exec redis redis-cli                           # Konsola Redis
docker compose exec backend dotnet ef migrations add Init     # Nowa migracja

# ─── Czyszczenie ────────────────────────────────────────
docker compose down -v --rmi all     # Usuń wszystko (kontenery, wolumeny, obrazy)
docker system prune -a               # Wyczyść nieużywane zasoby Docker
docker volume prune                  # Wyczyść nieużywane wolumeny

Typowe problemy i rozwiązania#

Port już zajęty#

# Sprawdź, co zajmuje port
# Linux/Mac:
lsof -i :5432
# Windows:
netstat -ano | findstr :5432

# Zmień port w .env
POSTGRES_PORT=5433

Wolne wolumeny na Windows/WSL2#

Na systemie Windows z WSL2 bind mounts mogą być bardzo wolne. Rozwiązania:

# 1. Użyj delegated/cached (Docker Desktop)
volumes:
  - ./frontend/src:/app/src:delegated

# 2. Ogranicz synchronizowane katalogi
volumes:
  - ./frontend/src:/app/src        # Tylko kod źródłowy
  - /app/node_modules              # node_modules w kontenerze
  - /app/.next                     # Build cache w kontenerze

Kontener ciągle się restartuje#

# Sprawdź logi
docker compose logs backend

# Sprawdź health check
docker inspect projekt-backend-1 | jq '.[0].State.Health'

# Wejdź do kontenera i debuguj ręcznie
docker compose run --entrypoint bash backend

Podsumowanie#

Docker Compose to niezbędne narzędzie w arsenale każdego zespołu deweloperskiego. Pozwala na:

  • Standaryzację środowisk - koniec z "u mnie działa"
  • Szybki onboarding - nowy deweloper zaczyna pracę w minuty, nie godziny
  • Izolację projektów - wiele projektów na jednej maszynie bez konfliktów
  • Automatyzację - migracje, seed danych i health checki działają automatycznie
  • Produktywność - hot reload, debugowanie i logi w jednym miejscu

Dobrze skonfigurowane środowisko Docker Compose to inwestycja, która zwraca się przy każdym nowym członku zespołu i każdej zmianie w infrastrukturze. Zacznij od prostej konfiguracji i rozbudowuj ją stopniowo - Docker Compose wspiera iteracyjne podejście do budowania infrastruktury.

Potrzebujesz profesjonalnego środowiska deweloperskiego?#

W MDS Software Solutions Group pomagamy zespołom wdrażać Docker Compose w codziennej pracy. Oferujemy:

  • Konfigurację lokalnych środowisk deweloperskich
  • Optymalizację istniejących plików Compose
  • Wdrożenie CI/CD z kontenerami Docker
  • Szkolenia dla zespołów deweloperskich
  • Migrację projektów do architektury kontenerowej

Skontaktuj się z nami, aby przyspieszyć pracę Twojego zespołu!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Docker Compose - Lokalne środowisko deweloperskie od A do Z | MDS Software Solutions Group | MDS Software Solutions Group