Parte 2 - docker-compose pentru stack complet
Dacă docker run vă lasă să porniți un container, docker compose vă lasă să orchestrați N containere ca un singur ansamblu - cu network-uri, volume-uri, env vars, dependențe.
Pentru News Portal aveți nevoie de 4 servicii:
- db - Postgres 16 cu volum persistent
- api - .NET 8 din Dockerfile-ul scris în secțiunea 01
- frontend - Angular 18 build static, servit de nginx
- proxy - reverse proxy nginx cu TLS, single point of entry
Toate într-un fișier docker-compose.yml, pornite cu o comandă: docker compose up -d --build.
Structura unui compose file
services: # toate containerele
numele_serviciu:
image: imagine:tag # SAU build:
build: # SAU image:
context: ./folder
dockerfile: Dockerfile
ports:
- "host:container"
expose:
- "container" # doar intern, nu host
environment:
KEY: value
volumes:
- type-volume
networks:
- retea_custom
depends_on:
alt_serviciu:
condition: service_healthy
healthcheck:
test: [...]
restart: unless-stopped
volumes: # named volumes (persistente)
db_data:
networks: # custom networks
reteaua_mea:
driver: bridge
Compose pentru News Portal
services:
db:
image: postgres:16-alpine
container_name: newsportal-db
environment:
POSTGRES_USER: ${DB_USER:-newsportal}
POSTGRES_PASSWORD: ${DB_PASSWORD:-newsportal_dev}
POSTGRES_DB: ${DB_NAME:-newsportal}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-newsportal}"]
interval: 5s
timeout: 3s
retries: 5
networks: [newsportal_net]
restart: unless-stopped
api:
build:
context: ./backend
dockerfile: Dockerfile
container_name: newsportal-api
depends_on:
db:
condition: service_healthy
environment:
ConnectionStrings__DefaultConnection: "Host=db;Port=5432;Database=${DB_NAME:-newsportal};Username=${DB_USER:-newsportal};Password=${DB_PASSWORD:-newsportal_dev}"
Jwt__Key: ${JWT_KEY:-DevKeyAtLeast32BytesLong!!}
Jwt__Issuer: NewsPortal
Jwt__Audience: NewsPortalUsers
ASPNETCORE_ENVIRONMENT: Development
expose: ["8080"]
networks: [newsportal_net]
restart: unless-stopped
frontend:
build:
context: ./news-portal-app
dockerfile: Dockerfile
container_name: newsportal-frontend
depends_on: [api]
expose: ["80"]
networks: [newsportal_net]
restart: unless-stopped
proxy:
image: nginx:1.27-alpine
container_name: newsportal-proxy
depends_on: [api, frontend]
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
networks: [newsportal_net]
restart: unless-stopped
volumes:
db_data:
networks:
newsportal_net:
driver: bridge
Conceptele importante
Networks: bridge custom vs default
Default, compose creează un network bridge cu numele <folder>_default. Toate serviciile sunt automat conectate. Service discovery se face prin DNS: serviciul api e accesibil de către alte containere ca http://api:8080. Numele containerului = hostname-ul DNS.
Când definiți explicit networks: [newsportal_net], aveți control mai mare (puteți separa rețele - ex db pe rețeaua sa, api pe rețeaua sa, doar api e “bridge” între ele). În lab folosim un singur network simplu, dar în producție e idiomatic să izolați.
expose: vs ports:
expose: ["8080"]- portul e disponibil doar în cadrul rețelei Docker. Alte containere îl pot accesa, host-ul nu.ports: ["80:80"]- mapează portul host:container. Accesibil din afara host-ului (de pe altă mașină, internet).
În compose-ul de mai sus:
dbareexpose: 5432(implicit din imagine, nu trebuie repetat). Niciun container nu poate ajunge la DB de pe internet - doar din rețeaua Docker.apiareexpose: 8080. Doarfrontendșiproxyajung la el.frontendareexpose: 80. Doarproxyajunge la el.proxyareports: 80, 443. Punct unic de intrare publică.
Ăsta e principiul “single point of entry”: tot traficul intră prin proxy. Dacă cineva scanează host-ul, nu vede DB sau API direct - vede doar nginx-ul.
Volumes: bind vs named
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # bind mount (path local -> path container)
- db_data:/var/lib/postgresql/data # named volume (gestionat de Docker)
- logs:/app/logs:rw # named volume scriabil
| Tip | Pro | Contra | Când folosiți |
|---|---|---|---|
Bind (./local:/container) |
Vedeți modificările în real-time | Path-ul exact e expus | config files, dev hot-reload |
Named (name:/container) |
Portabile, gestionate de Docker | Mai greu de inspectat | DB data, persistent state |
tmpfs (/tmp:size=50m) |
Numai în RAM, dispar la restart | Volatil | Cache, secrets temporare |
Suffix :ro (read-only) e bun pentru config files - containerul nu trebuie să scrie acolo.
depends_on cu healthcheck
depends_on: simplu garantează doar ordinea de pornire, nu că dependința e gată. Postgres pornește în 2-3s dar nu acceptă conexiuni încă - dacă API-ul încearcă să se conecteze prea devreme, crash.
condition: service_healthy așteaptă până când healthcheck-ul dependinței trece de la starting la healthy. Pentru asta dependința trebuie să aibă healthcheck: definit:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U newsportal"]
interval: 5s # rula la fiecare 5s
timeout: 3s # daca nu raspunde in 3s, esuat
retries: 5 # 5 esuari consecutive -> unhealthy
start_period: 10s # ignora primele 10s (startup time)
Acum api așteaptă până db răspunde la pg_isready. Pornirea poate dura 30s la primul build, dar e robustă.
.env substitution
Compose face substituție automat din fișierul .env de lângă docker-compose.yml:
POSTGRES_PASSWORD: ${DB_PASSWORD:-newsportal_dev}
Sintaxa ${VAR:-default} = “ia VAR din env, sau folosește default dacă nu e setată”.
.env (gitignored!):
DB_USER=newsportal
DB_PASSWORD=O_PAROLA_PUTERNICA
DB_NAME=newsportal
JWT_KEY=AltSecretGeneratCuOpenssl
.env.example (commit-uit):
DB_USER=newsportal
DB_PASSWORD=schimba_ma
DB_NAME=newsportal
JWT_KEY=schimba_ma_min_32_bytes
Workflow-ul: cp .env.example .env, completați valorile reale, rulați compose. Niciodată nu commit-ați .env real!
Dockerfile pentru frontend Angular
Stage 1 build cu node, stage 2 servire static cu nginx:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # ci e mai rapid si determinist decat install
COPY . .
RUN npm run build -- --configuration production
FROM nginx:1.27-alpine
COPY --from=build /app/dist/news-portal-app/browser/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
nginx.conf din folderul news-portal-app/ (lângă Dockerfile):
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA fallback: orice URL care nu e fisier real -> index.html
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|woff|woff2|svg|png|jpg|jpeg|gif|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
try_files e crucial pentru Angular SPA: rutele client-side (ex /articles/123/edit) NU există ca fișiere pe server. Default, nginx ar întoarce 404. try_files zice: “încearcă file, dacă nu fall back la index.html”. Angular preia controlul și afișează ruta corectă.
Pornire + verificare (pe server)
Când ajungeți pe server (după setup-ul din secțiunea 04 + provisionarea de cont), pașii sunt:
# 1. Copiati template-ul .env in fisierul .env real si completati parolele
cp .env.example .env
nano .env # DB_PASSWORD, JWT_KEY, etc.
# 2. Build + pornire (3-5 min cu cache rece)
docker compose up -d --build
Niciun cert de generat manual. nginx-ul cursului face TLS termination cu Let’s Encrypt automat pentru subdomeniul vostru. Compose-ul vostru NU include un container proxy - api-ul vostru se atașează la studlab_net (rețeaua cursului) și nginx-ul cursului face proxy <subdomeniul-vostru>.student-dev.ro -> student-<username>:8080.
Compose-ul de mai sus (cu serviciu proxy propriu și certurile self-signed) e util doar dacă vreți să rulați totul standalone pe un laptop / VPS personal. Pentru deploy pe student-dev.ro folosiți varianta simplificată din docker-deploy - doar db + api, fără proxy și fără frontend în compose.
Output-ul tipic:
[+] Running 5/5
- Network newsportal_net Created
- Container newsportal-db Started
- Container newsportal-api Started
- Container newsportal-frontend Started
- Container newsportal-proxy Started
Verificați:
docker compose ps
# arata toate cu STATUS = Up
docker compose logs -f api
# trebuie sa apara: "Now listening on: http://[::]:8080"
Browser: https://localhost (acceptați warning self-signed). Login: admin@newsportal.com / Admin@123.
Troubleshooting
| Problemă | Cauza probabilă | Fix |
|---|---|---|
connection refused la api |
ASPNETCORE_URLS lipsă sau setat la localhost |
setați http://+:8080 în env compose |
db unhealthy |
Postgres nu primește conexiuni în start_period | măriți start_period: 30s |
frontend 404 pe rute Angular |
nginx fără try_files |
adăugați try_files $uri $uri/ /index.html; |
502 Bad Gateway la /api/ |
api containerul a crashat | docker compose logs api vă arată de ce |
relation "..." does not exist |
DB-ul nu a fost inițializat | docker compose down -v && docker compose up -d (șterge volume!) |
bind: address already in use |
port 80/443 ocupat pe host | opriți IIS/Apache/alt nginx, sau schimbați mapping în compose |
În secțiunea următoare configurăm nginx-ul proxy pentru reverse proxy + TLS.