G.STANCUTA
Publicat · 2026 · 05 · 269 min de citit

Docker Compose: întregul mediu cu o singură comandă

  • Docker
  • DevOps
  • Self-hosting
  • Infra

Baza de date, cache-ul, object storage-ul, reverse proxy-ul și aplicația în sine, definite o singură dată într-un singur fișier și pornite împreună. Compose este modul în care un dezvoltator solo rulează un stack de producție real fără un runbook.

"Funcționează pe mașina mea" este o mărturisire, nu o actualizare de stare. Soluția este să oprești descrierea mediului într-un wiki pe care nimeni nu-l citește și să începi să-l definești într-un fișier pe care toți îl rulează. Docker Compose este acel fișier. Întregul stack NearYou, aplicația Next.js, worker-ul de fundal, MySQL, Redis, MinIO și Caddy, pornește cu docker compose up și nimic altceva. Pe laptopul meu, pe CI, pe o cutie Hetzner nouă: aceeași comandă, același rezultat.

Stack-ul care a motivat totul

NearYou este o platformă de oferte bazată pe locație. Backend-ul are nevoie de o bază de date relațională pentru oferte și afaceri, un cache pentru datele de sesiune și rate limiting, object storage pentru media încărcate, un proces worker pentru trimiterea notificărilor și un reverse proxy care termină TLS și rutează traficul. Acestea sunt cinci componente în mișcare înainte de o singură linie de cod de produs. Fără Docker, sunt cinci instalări separate cu cinci constrângeri de versiune care diverge în momentul în care se alătură un al doilea dezvoltator, sau în momentul în care provisionezi un server nou.

Cu Compose, acele cinci componente devin douăzeci de linii de YAML și un Dockerfile. Întregul mediu este un artefact care poate fi committit în git. Un nou colaborator clonează repo-ul, rulează docker compose up și este productiv în cinci minute. Nu cinci minute de tastare, cinci minute de descărcare a containerelor.

Diagramă izometrică a serviciilor Docker Compose conectate printr-o rețea partajată
Stack-ul NearYou ca o singură rețea Compose, fiecare serviciu fiind un container cu nume propriu.

Fișierul real docker-compose.yml

Aceasta este o versiune prescurtată, dar reprezentativă a fișierului Compose al NearYou. Cel real adaugă variabile de mediu dintr-un fișier .env și câteva adnotări de label pentru Caddy, dar structura este exact aceasta:

yaml
services:
  mysql:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: nearyou
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 20s

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
      MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
    volumes:
      - minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
    environment:
      DATABASE_URL: mysql://root:${DB_ROOT_PASSWORD}@mysql:3306/nearyou
      REDIS_URL: redis://redis:6379
      MINIO_ENDPOINT: http://minio:9000
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      minio:
        condition: service_healthy

  worker:
    build:
      context: .
      dockerfile: Dockerfile
      target: worker
    environment:
      DATABASE_URL: mysql://root:${DB_ROOT_PASSWORD}@mysql:3306/nearyou
      REDIS_URL: redis://redis:6379
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy

  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    depends_on:
      - app

volumes:
  mysql_data:
  redis_data:
  minio_data:
  caddy_data:

Healthcheck-uri și depends_on: partea pe care toți o sar

Majoritatea tutorialelor Docker arată depends_on ca o listă de nume de servicii. Acea formă controlează doar ordinea de pornire, nu disponibilitatea. MySQL pornește, Docker marchează dependența ca satisfăcută și aplicația începe să se conecteze înainte ca baza de date să fi terminat inițializarea. Rezultatul este o pornire instabilă care eșuează aproximativ o dată din cinci și produce un stack trace care arată ca o eroare reală.

Soluția este forma condition: service_healthy, care necesită un healthcheck trecut înainte ca Docker să deblocheze serviciul dependent. Scrie healthcheck-ul pentru fiecare serviciu cu stare. Pentru MySQL acesta este mysqladmin ping. Pentru Redis este redis-cli ping. Pentru MinIO este comanda mc ready local. Cu toate trei la locul lor, aplicația pornește doar când toate cele trei servicii de suport sunt cu adevărat pregătite să accepte conexiuni. Pornirea încetează să mai fie o cursă.

Dockerfile-ul: un singur build, două ținte

Fișierul Compose referențiază două ținte de build: runner pentru aplicația Next.js și worker pentru procesul de fundal. Ambele provin din același Dockerfile folosind build-uri multi-stage. Stage-urile app și worker partajează aceeași bază și aceeași instalare Node, au doar entrypoint-uri diferite. Aceasta înseamnă că un singur build de imagine acoperă ambele servicii și nu există nicio divergență între runtime-ul care servește cererile și cel care procesează job-urile.

bash
# Dockerfile (condensed for clarity)
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM base AS builder
COPY . .
RUN npm run build

# The web server target
FROM base AS runner
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
ENV NODE_ENV=production
CMD ["node", "server.js"]

# The worker target
FROM base AS worker
COPY --from=builder /app/dist/worker ./worker
ENV NODE_ENV=production
CMD ["node", "worker/index.js"]

În producție, docker build --target runner și docker build --target worker produc două imagini slabe care partajează același layer cache. Worker-ul nu este un stack diferit, sunt aceleași dependențe cu un entry point diferit. Această uniformitate contează când faci debugging: nicio surpriză de tipul "dar funcționează în containerul aplicației".

Volume cu nume: date care supraviețuiesc repornirilor containerelor

Containerele sunt menite să fie eliminabile. Datele pe care le conțin nu sunt. Volume-urile cu nume sunt mecanismul care le menține separate. Când rulezi docker compose down fără --volumes, containerele se opresc și sunt eliminate, dar mysql_data, redis_data și minio_data persistă pe host. Rulează din nou docker compose up și toate cele trei baze de date sunt exact unde erau.

  • Volume-urile cu nume se află în /var/lib/docker/volumes/ și supraviețuiesc ciclului de viață al containerelor.
  • Bind mount-urile (o cale de host în definiția volume-ului) sunt alegerea potrivită pentru Caddyfile, deoarece aceea este o configurație pe care o editezi, nu date pe care le protejezi.
  • docker compose down --volumes este opțiunea nucleară: elimină volume-urile. Nu rula niciodată aceasta în producție fără un backup la îndemână.
  • Fă backup la volume-ul MySQL extern. Volume-urile cu nume nu sunt backup-uri. Un mysqldump nocturn transferat în afara boxului este ceea ce face volume-ul sigur de folosit.
Schemă a unui agent AI de coding care citește fișiere de memorie markdown pentru a opera un stack Docker Compose
Un agent cu context markdown persistent poate reproduce orice operațiune de mediu fără a mai întreba din nou.

Oferirea memoriei pe termen lung a stack-ului unui agent AI

Folosesc Claude Code ca agent principal de coding pe acest proiect. Modelul cunoaște Docker în general, dar nu știe ce tag-uri de imagine fixăm, ce servicii au nevoie de healthcheck-uri cu ce comenzi, cum se numesc volume-urile sau de ce adaptorul MariaDB se află în configurația Prisma în locul celui MySQL. Fără acel context, fiecare sesiune a agentului care atinge infrastructura pornește de la zero și produce output plauzibil dar greșit.

Soluția este un fișier de memorie markdown persistent committit în repo. AGENTS.md (sau un DOCKER.md dedicat) înregistrează convențiile, comenzile și capcanele în text simplu. Agentul îl citește la începutul oricărei sesiuni care implică stack-ul. Deoarece fișierul trăiește în git, rămâne precis pe măsură ce stack-ul evoluează: actualizează fișierul Compose, actualizează documentația, commitează ambele.

Iată formatul real pe care îl folosesc. Nu este un tutorial, este o referință densă pe care agentul o poate asimila în zece secunde:

md
# Docker Stack Reference (NearYou)

## Services
| Service | Image            | Role                          |
|---------|------------------|-------------------------------|
| mysql   | mysql:8.4        | Primary database              |
| redis   | redis:7-alpine   | Sessions, rate limits, queues |
| minio   | minio/minio      | Object storage (S3-compat)    |
| app     | build ./         | Next.js app (target: runner)  |
| worker  | build ./         | BullMQ worker (target: worker)|
| caddy   | caddy:2-alpine   | Reverse proxy, TLS            |

## Key commands
- Start: `docker compose up -d`
- Rebuild app after code change: `docker compose build app worker && docker compose up -d app worker`
- Tail logs: `docker compose logs -f app worker`
- MySQL shell: `docker compose exec mysql mysql -u root -p nearyou`
- MinIO CLI: `docker compose exec minio mc ls local/uploads`

## Healthchecks
Every stateful service has a healthcheck. The app and worker use
`depends_on: condition: service_healthy`. Never remove them.
MySQL needs `start_period: 20s` because it is slow on first init.

## Volumes
Named volumes: mysql_data, redis_data, minio_data, caddy_data.
`docker compose down` is safe. `docker compose down --volumes` deletes data.

## Production rebuild (fresh Hetzner box)
1. Install Docker and Docker Compose plugin.
2. Clone repo, copy .env from secrets manager.
3. `docker compose pull && docker compose build`
4. `docker compose up -d`
5. Restore latest MySQL dump into mysql container.

## Gotchas
- Prisma uses the MariaDB adapter against MySQL 8.4. Do not swap to the MySQL
  adapter without testing migrations first.
- MinIO healthcheck uses `mc ready local`, not curl. The mc binary is bundled
  in the minio image.
- Caddy auto-provisions TLS. Ports 80 and 443 must be open. Caddyfile lives at
  ./Caddyfile in the repo root.

Când agentul deschide o sesiune pentru a adăuga un nou serviciu, a depana un eșec de pornire sau a scrie un script de migrare, citește mai întâi acest fișier. Cunoaște tag-ul de imagine fixat pentru MySQL înainte de a scrie o comandă healthcheck. Știe să nu atingă flag-urile de volume înainte de a sugera un teardown. Calitatea output-ului corelează direct cu calitatea acestui fișier. Menținerea lui actualizată costă cinci minute de muncă per modificare. Alternativa este să reexplici întregul stack la fiecare sesiune.

Reconstruirea unui server nou în câteva minute

Testul final al unui stack definit cu Compose este: cât timp durează să ajungi de la o cutie Hetzner goală la un mediu de producție funcțional? Pentru NearYou răspunsul este aproximativ cincisprezece minute, și cea mai mare parte din aceasta înseamnă să aștepți ca Docker să descarce imaginile.

  1. 01Provisionează cutia, instalează Docker Engine și plugin-ul Compose.
  2. 02Clonează repo-ul și pune fișierul .env din managerul de secrete.
  3. 03docker compose pull pentru a pre-descărca imaginile, apoi docker compose build pentru țintele personalizate.
  4. 04docker compose up -d pentru a porni totul în modul detached.
  5. 05Restaurează cel mai recent dump MySQL: docker compose exec -T mysql mysql -u root -p nearyou < backup.sql.
  6. 06Îndreaptă înregistrarea DNS A spre noul IP. Caddy provisionează TLS automat la prima cerere.

Nu există un runbook. Nu există o listă de verificare din care ai putea uita un element. Mediul este definit în repo, comenzile sunt în DOCKER.md, iar agentul poate executa procedura de restaurare fără ca eu să o nareze pas cu pas. Acesta este beneficiul compus: fișierul Compose face stack-ul reproductibil, iar memoria markdown face stack-ul operabil de către agent. Împreună înseamnă că un server mort este o neplăcere de cincisprezece minute, nu o criză.

Mediul este cod. Dacă nu este în repo, nu există.

Portofoliu · Indicator
Desenat de
G. STANCUTA
Disciplină
AI & AUTOMATION
Locație
MORTER · SÜDTIROL
Stare
Disponibil
Limbi
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revizie
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Proiectat cu AI, construit să funcționeze singur.