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.

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:
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.
# 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 --volumeseste 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
mysqldumpnocturn transferat în afara boxului este ceea ce face volume-ul sigur de folosit.

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:
# 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.
- 01Provisionează cutia, instalează Docker Engine și plugin-ul Compose.
- 02Clonează repo-ul și pune fișierul
.envdin managerul de secrete. - 03
docker compose pullpentru a pre-descărca imaginile, apoidocker compose buildpentru țintele personalizate. - 04
docker compose up -dpentru a porni totul în modul detached. - 05Restaurează cel mai recent dump MySQL:
docker compose exec -T mysql mysql -u root -p nearyou < backup.sql. - 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ă.