Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Docker bàsic

Introducció

Les màquines virtuals emulen una màquina; els contenidors despleguen una aplicació. Docker és una plataforma que empaqueta una aplicació amb tot el que necessita — i només el que necessita — perquè el kernel del host ja s’encarrega de la resta. D’aquest principi en deriven tres propietats que defineixen com es treballa amb Docker:

  • Predictibilitat: el comportament és idèntic al portàtil i a producció — s’acaba el “a la meva màquina funciona”.
  • Aïllament: cada contenidor té les seves pròpies dependències, sense conflictes entre projectes.
  • Eficiència: sense OS convidat ni serveis de sistema innecessaris, els contenidors arrenquen en mil·lisegons i ocupen MBs en lloc de GBs.

Aquesta filosofia va impulsar els microserveis: sistemes formats per molts contenidors petits, cadascun amb una sola responsabilitat, escalables i substituïbles de manera independent.

Contenidors vs Màquines Virtuals

contenidors vs màquines virtuals

Una màquina virtual inclou un Guest OS complet (kernel + espai d’usuari) sobre un hipervisor. Cada VM emula maquinari i arrenca el seu propi sistema operatiu, cosa que implica GBs de disc i minuts d’arrencada.

Un contenidor comparteix el kernel del host i utilitza funcionalitats del kernel Linux — namespaces, cgroups — per aïllar processos sense necessitar un OS complet (veure annex tècnic per a més detalls).

Màquina VirtualContenidor
AïllamentComplet (Guest OS propi)A nivell de procés (namespaces)
PesGBsMBs
ArrencadaMinutsSegons
RendimentOverhead de virtualitzacióQuasi natiu
KernelPropi (Guest OS)Compartit amb el host

Conceptes fonamentals

Imatge (Image)

Una imatge és una plantilla de només lectura que conté tot el necessari per executar una aplicació: sistema operatiu mínim, runtime, dependencies i codi. Les imatges es construeixen a partir d’un Dockerfile. Internament, una imatge és una pila de capes de només lectura. Això permet reutilitzar capes entre imatges: si dues imatges parteixen de la mateixa base (per exemple ubuntu:22.04), aquella capa es guarda una sola vegada al disc i es comparteix. Quan descarregues una imatge que té capes que ja tens d’una altra, Docker només baixa les que falten.

Contenidor (Container)

Un contenidor és una instància en execució d’una imatge. Podem tenir múltiples contenidors de la mateixa imatge, cadascun aïllat dels altres. Quan en crees un, Docker afegeix una capa d’escriptura a sobre de les capes de la imatge — és on van tots els canvis que fa l’aplicació en temps d’execució. Les capes de la imatge original mai es modifiquen, per això múltiples contenidors poden compartir la mateixa imatge simultàniament.

Analogia:

  • Imatge = Recepta de cuina (plantilla)
  • Contenidor = Pastís cuinat (instància)

Dockerfile

Un Dockerfile és un fitxer de text amb instruccions per construir una imatge. Per a Docker, representa:

  • Instruccions pas a pas — Docker executa cada línia en ordre i genera una capa per cada pas. Si la construcció falla, saps exactament on i per què.
  • L’entorn escrit, no recordat — en lloc de documentar “instal·la això, configura allò”, el Dockerfile és el document. Qualsevol pot llegir-lo i saber exactament què conté la imatge.
  • Canvis controlats — per actualitzar una dependency, edites el Dockerfile i reconstrueixes. No es modifica un contenidor en execució a mà.
  • Mateix resultat a qualsevol lloc — el mateix Dockerfile produeix la mateixa imatge al portàtil i a producció.

Registre (Registry)

Un registre és un repositori d’imatges. El més popular és Docker Hub, però en entorns professionals l’ús de registres privats és la norma, no l’excepció.

Quan fas docker run nginx, Docker primer busca la imatge nginx localment. Si no la troba, la descarrega automàticament de Docker Hub. Això és un pull implícit — la majoria de vegades ni te n’adones.

Primers passos

Hands-on — Segueix els passos a la teva terminal.

Un cop instal·lat Docker (veure annex), comprovem que funciona:

docker run hello-world

Aquesta comanda descarrega la imatge hello-world de Docker Hub, crea un contenidor i l’executa. Si veus el missatge de benvinguda, Docker funciona correctament.

Executar un contenidor: el cicle bàsic

Un contenidor passa per tres estats al llarg de la seva vida:

  • En execució — el PID 1 està viu i el contenidor fa la seva feina. docker run
  • Aturat — el PID 1 ha mort (per error, per docker stop, o perquè ha acabat). El contenidor encara existeix amb el seu sistema de fitxers.
  • Eliminat — el contenidor desapareix completament. docker rm

La distinció important és entre aturat i eliminat: un contenidor aturat encara ocupa espai en disc (la capa d’escriptura es conserva) i es pot tornar a arrencar. docker ps només mostra els contenidors en execució — docker ps -a mostra tots, inclosos els aturats.

Quan executes docker stop, Docker envia SIGTERM al PID 1 perquè pugui tancar-se de manera ordenada. Si no ha acabat en 10 segons, envia SIGKILL. Tots els processos fills moren amb ell i els namespaces i cgroups es desmunten. Si vols que el contenidor s’elimini automàticament en aturar-se, usa --rm:

docker run --rm nginx # el contenidor desapareix quan s'atura

Anem a treballar amb nginx (un servidor web) per veure el cicle de vida d’un contenidor. Primer, l’executem en segon pla (-d):

docker run -d -p 8080:80 --name web nginx

El flag -p 8080:80 és el mapatge de ports (port mapping). Un contenidor té la seva pròpia xarxa aïllada — nginx escolta al port 80 dins del contenidor, però sense -p aquest port és invisible des del host. El mapatge host:contenidor obre un pont: les peticions que arriben al port 8080 del host es reenvien al port 80 del contenidor.

navegador → localhost:8080 (host) → :80 (contenidor nginx)

El port del host i el del contenidor no cal que siguin iguals. Aquí usem 8080 al host perquè el 80 pot estar ocupat o requerir permisos de root.

Ara podem comprovar que està funcionant amb docker ps, que mostra els contenidors actius:

docker ps # CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES # a518f8cca58b nginx "/docker-entrypoint.…" 5 minutes ago Up 5 minutes 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp web

La sortida ens diu que el contenidor web està executant la imatge nginx, porta 5 minuts actiu (Up 5 minutes), i el mapatge de ports confirma que el port 8080 del host (tant IPv4 com IPv6) redirigeix al port 80 del contenidor.

Accedim a http://localhost:8080 al navegador — veurem la pàgina per defecte de nginx.

Podem veure quines imatges tenim descarregades i l’espai que ocupen amb docker images:

docker images # IMAGE ID DISK USAGE CONTENT SIZE EXTRA # hello-world:latest ef54e839ef54 25.9kB 9.52kB U # nginx:latest 0236ee02dcbc 240MB 65.8MB U

Per veure què està fent el contenidor, consultem els seus logs:

docker logs web docker logs -f web # -f: seguir en temps real (Ctrl+C per sortir)

Si necessitem entrar dins del contenidor per inspeccionar-lo o debugar:

docker exec -it web bash # Ara estem dins del contenidor amb una shell interactiva # -i: interactiu (mantenir STDIN obert) # -t: terminal (assignar una pseudo-TTY)

Per aturar i eliminar el contenidor:

docker stop web docker rm web

docker stop envia un senyal SIGTERM al PID 1 donant-li temps per acabar de forma neta, mentre que docker kill envia SIGKILL que atura el procés immediatament sense possibilitat de cleanup — per això és important que el PID 1 sigui el procés de l’aplicació i no un shell. docker rm elimina el contenidor aturat — sense aquest pas, el contenidor queda en estat aturat ocupant espai en disc (visible amb docker ps -a).

A la pràctica, els contenidors aturats rarament són útils — com a molt serveixen per inspeccionar el sistema de fitxers d’un contenidor que ha fallat. En producció, els contenidors es tracten com a descartables: si fallen, se’n crea un de nou en lloc de reiniciar l’antic. Per això, la majoria de contenidors es llancen amb --rm, que els elimina automàticament en aturar-se:

docker run --rm -d -p 8080:80 nginx

Flags més comuns de docker run

FlagSignificatExemple
-dExecució en segon pla (detached)docker run -d nginx
-itShell interactivadocker run -it ubuntu bash
-pMapejar ports (host:contenidor)-p 8080:80
--nameAssignar un nom al contenidor--name web
--rmEliminar automàticament en aturardocker run --rm nginx
-eVariable d’entorn-e LOG_LEVEL=debug
-vMuntar volum (veure Volums)-v ./data:/app/data

Dockerfile: crear imatges pròpies

Un Dockerfile defineix pas a pas com construir la nostra imatge. Cada instrucció crea una capa (layer) — un diff sobre l’estat anterior del sistema de fitxers. La imatge final és l’apilament de totes aquestes capes:

[ layer 4 ] COPY app/ ./ ← el teu codi [ layer 3 ] RUN pip install ... ← les dependències [ layer 2 ] COPY requirements.txt ← el fitxer de dependències [ layer 1 ] FROM python:3.12-slim ← el sistema base

Docker guarda cada capa en cache. Quan reconstrueixes la imatge, només recalcula les capes que han canviat i totes les que en depenen. Per això l’ordre importa: si poses COPY app/ abans de RUN pip install, qualsevol canvi al codi invalida la capa de dependències i Docker les torna a instal·lar des de zero. Posant COPY requirements.txt i RUN pip install abans de copiar el codi, les dependències es mantenen en cache mentre requirements.txt no canviï.

Estructura bàsica

# Dockerfile # FROM: imatge base (obligatòria, sempre primera) FROM python:3.12-slim # WORKDIR: directori de treball dins del contenidor WORKDIR /app # COPY: copiar fitxers del host al contenidor # Primer les dependencies (canvien poc → cache eficient) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Després el codi (canvia sovint) COPY . . # EXPOSE: documentar quin port usa l'app (no el publica!) EXPOSE 8000 # CMD: comando per defecte quan s'executa el contenidor CMD ["python", "app.py"]

Etiquetes d’imatges base

Les imatges oficials ofereixen variants amb diferents mides:

  • python:3.12 — Imatge completa (gran, ~900MB)
  • python:3.12-slim — Versió lleugera, sense eines de compilació (~150MB)
  • python:3.12-alpine — Mínima, basada en Alpine Linux (~50MB, pot donar problemes de compatibilitat)

Per a producció, usa sempre una versió específica (python:3.12-slim), mai latest.

CMD i ENTRYPOINT

Quan un contenidor arrenca, el kernel necessita llançar exactament un procés per iniciar-ho tot. Aquest procés rep el número 1 — el PID 1 — i té un rol especial: quan ell mor, el contenidor s’atura. Tot el que passi dins del contenidor és fill seu.

En una aplicació d’inferència, volem que aquest procés sigui uvicorn directament.

ENTRYPOINT i CMD es reparteixen aquesta responsabilitat de forma complementària:

  • ENTRYPOINT defineix el que és el contenidor. Un contenidor que serveix prediccions sempre arrencarà uvicorn, independentment de com l’executis.
  • CMD defineix com s’executa per defecte. Quina app, quin port, quins flags. Es pot sobreescriure en docker run sense reconstruir la imatge.

Aquesta separació té una conseqüència pràctica molt útil: pots tenir una imatge única i arrencar-la de formes diferents.

# Arrencada normal docker run model-api # Arrencada amb port diferent, sense reconstruir la imatge docker run model-api app.main:app --host 0.0.0.0 --port 9000 # Inspecció interactiva, substituint completament el CMD docker run --entrypoint /bin/bash -it model-api

La bona pràctica és sempre usar la forma exec (llista JSON) tant per ENTRYPOINT com per CMD:

ENTRYPOINT ["uvicorn"] CMD ["app.main:app", "--host", "0.0.0.0", "--port", "8000"]

El nom “forma exec” ve del comportament de la comanda exec en un shell: en lloc de crear un procés fill, substitueix el procés actual pel nou — heretant el mateix PID. Això és exactament el que fa Docker amb la forma exec: uvicorn es converteix en el PID 1 directament, rep els senyals del sistema operatiu, gestiona els seus workers correctament, i quan fas docker stop el contenidor s’atura de forma neta i controlada.

Construir i executar

Hands-on — Segueix els passos a la teva terminal.

Posem en pràctica el Dockerfile de l’Estructura bàsica. Creem un directori amb tres fitxers:

myapp/ ├── Dockerfile ├── requirements.txt └── app.py
# app.py from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello from Docker!" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)
# requirements.txt flask

El Dockerfile és el de la secció anterior (amb el CMD ajustat al port 8000). Per construir la imatge, usem docker build. El flag -t assigna un nom i etiqueta en format nom:versió. El . al final és el context del build — el directori que Docker envia al dimoni per construir la imatge (normalment l’arrel del projecte, on hi ha el Dockerfile i el codi):

docker build -t myapp:1.0 . docker run --rm -d -p 8000:8000 --name myapp myapp:1.0

Accedim a http://localhost:8000 — veurem “Hello from Docker!”.

Per forçar que es reconstrueixin totes les capes ignorant la cache (útil quan les dependencies externes han canviat i no es reflecteix als fitxers locals):

docker build --no-cache -t myapp:1.0 .

ARG: variables de temps de build

ARG defineix variables disponibles només durant el docker build — no persisteixen al contenidor en execució. S’utilitzen per parametritzar el build sense hardcodejar valors al Dockerfile:

ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION}-slim

En construir, podem sobreescriure el valor per defecte amb --build-arg:

docker build --build-arg PYTHON_VERSION=3.12 .

La distinció important: si necessites que l’aplicació pugui llegir un valor en execució, usa ENV. Si només el necessites durant el build (versió de la imatge base, flags de compilació), usa ARG. Un error comú és usar ARG per configurar paths o credencials i després no entendre per què l’app no els troba quan arrenca.

.dockerignore

El fitxer .dockerignore funciona com .gitignore — evita copiar fitxers innecessaris a la imatge, fent-la més petita i segura:

.git/ .venv/ __pycache__/ *.md tests/ .env node_modules/

Directoris com .venv/ i node_modules/ s’exclouen perquè les dependències del host poden estar compilades per una arquitectura o sistema operatiu diferent del contenidor. El Dockerfile ja les instal·la des de zero amb RUN pip install o RUN npm install, assegurant que són compatibles amb l’entorn del contenidor.

Bones pràctiques

Imatges lleugeres — Usa variants -slim o -alpine de les imatges base. La diferència de mida pot ser enorme (900MB vs 50MB). Si instal·les paquets del sistema, neteja la cache en la mateixa instrucció RUN:

RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*

Ordre de les capes — Si una capa canvia, totes les capes posteriors es reconstrueixen. Per això, copia primer els fitxers de dependencies (que canvien poc) i després el codi:

# BÉ: dependencies primer COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # MALAMENT: qualsevol canvi al codi invalida la cache de pip install COPY . . RUN pip install -r requirements.txt

No executar com root — Els processos dins d’un contenidor s’executen com a root per defecte. Crea un usuari sense privilegis i canvia a ell abans d’arrencar l’aplicació:

RUN useradd -m -u 1000 appuser && \ chown -R appuser:appuser /app USER appuser

Usar -u 1000 explícitament fa que l’UID de l’usuari dins del contenidor coincideixi amb el del primer usuari no-root de la majoria de distribucions Linux. Això és important quan muntes bind mounts: el host identifica propietaris de fitxers per UID, no per nom d’usuari. Si el contenidor escriu fitxers amb UID 1000 i el teu usuari del host també és 1000, els fitxers et pertanyeran i podràs llegir-los i modificar-los sense problemes de permisos.

Multi-stage builds

Sense multi-stage, una imatge de producció acaba contenint tot el que vas necessitar durant el build: la imatge base completa, eines de compilació, paquets de test i codi font innecessari. La imatge resultant pot pesar 1GB o més quan l’únic que necessites per executar l’API és Python, les dependencies de producció i el codi de l’aplicació.

Els multi-stage builds solucionen això separant el procés en stages independents. La imatge resultant és només l’últim stage — els anteriors són temporals i es descarten després del build. Només el que copies explícitament amb COPY --from= arriba a la imatge final:

# Dockerfile # Stage 1: Builder — imatge completa amb eines de compilació (gcc, headers...) # Necessària quan les dependències tenen extensions en C (numpy, psycopg2, cryptography...) FROM python:3.12 AS builder WORKDIR /app RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Stage 2: Runtime — imatge lleugera, només el necessari per executar # gcc i les eines de compilació no arriben aquí FROM python:3.12-slim WORKDIR /app COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" COPY app/ ./app/ ENTRYPOINT ["uvicorn"] CMD ["app.main:app", "--host", "0.0.0.0", "--port", "8000"]

AS builder dóna nom al primer stage. El builder usa la imatge completa (python:3.12) que inclou gcc i eines de compilació necessàries per instal·lar dependències amb extensions en C. Instal·la tot dins d’un virtualenv a /opt/venv, que després es copia sencer al stage de runtime — així tant els paquets com els executables (com uvicorn) arriben a la imatge final. El runtime usa python:3.12-slim, molt més lleugera. El codi font, notebooks, fitxers .env i dades de test no arriben mai a la imatge final. Els beneficis: imatge fins a 10x més petita, menys superfície d’atac, i separació clara entre el que cal per compilar i el que cal per executar.

Volums i variables d’entorn

Volums: persistir dades

Per defecte, les dades dins d’un contenidor es perden quan s’elimina. Els volums resolen aquest problema. Hi ha dos tipus principals:

Named volumes — gestionats per Docker, ideals per a dades persistents (bases de dades, logs):

docker run -d -v app_logs:/app/logs myapp:1.0 # app_logs: nom del volum (Docker el gestiona) # /app/logs: path dins del contenidor

Bind mounts — munten un directori del host, ideals per a desenvolupament (els canvis es reflecteixen immediatament):

docker run -d -v $(pwd)/src:/app/src myapp:1.0 # $(pwd)/src: directori del host # /app/src: directori del contenidor
TipusQuan usarGestió
Named volumeProducció, dades persistentsDocker
Bind mountDesenvolupament, compartir fitxersHost

En la pràctica, els named volumes es declaren a compose.yml en lloc de passar-los per línia de comandes — veure l’exemple amb Docker Compose.

Variables d’entorn

Les variables d’entorn permeten configurar l’aplicació sense modificar el codi ni la imatge. Hi ha dues formes de definir-les:

Al Dockerfile — amb ENV, que estableix valors per defecte dins de la imatge:

ENV LOG_LEVEL=info ENV PORT=8000

Aquests valors existiran sempre que s’executi el contenidor. Es poden sobreescriure en docker run amb -e.

En temps d’execució — amb -e o des d’un fitxer .env:

docker run -d -e LOG_LEVEL=debug -e MAX_WORKERS=4 myapp:1.0 # O des d'un fitxer .env docker run -d --env-file .env myapp:1.0

Bones pràctiques: mai incloguis secrets (passwords, API keys) al Dockerfile ni al codi. Usa variables d’entorn o fitxers .env (que no es comitegen a git).

Hands-on: volums i variables d’entorn

Hands-on — Segueix els passos a la teva terminal. No cal cap imatge pròpia — farem servir python:3.12-slim directament.

Creem un script que llegeixi configuració des de variables d’entorn i escrigui un log:

# config.py import os from datetime import datetime env = os.getenv("APP_ENV", "development") port = os.getenv("APP_PORT", "8000") print(f"Entorn: {env} | Port: {port}") with open("/logs/app.log", "a") as f: f.write(f"[{datetime.now()}] Arrencada en {env}:{port}\n") print("Log escrit a /logs/app.log")

Executem el script dins d’un contenidor combinant els tres conceptes — bind mount per al codi, variables d’entorn per a la configuració, i un named volume per als logs:

docker run --rm \ -v $(pwd):/app \ -v app_logs:/logs \ -e APP_ENV=staging \ -e APP_PORT=9000 \ python:3.12-slim python /app/config.py # Entorn: staging | Port: 9000 # Log escrit a /logs/app.log

Executem-ho unes quantes vegades i comprovem que els logs persisteixen entre execucions — el contenidor es destrueix (--rm), però el named volume app_logs sobreviu:

docker run --rm -v app_logs:/logs python:3.12-slim cat /logs/app.log # [2026-03-10 10:00:01] Arrencada en staging:9000 # [2026-03-10 10:00:05] Arrencada en staging:9000

Ara modifiquem config.py al host — per exemple, canviem el format del log — i tornem a executar. El canvi es reflecteix immediatament perquè el bind mount comparteix el fitxer entre host i contenidor, sense reconstruir cap imatge.

Per no repetir les variables a cada docker run, podem usar un fitxer .env:

# .env APP_ENV=production APP_PORT=8080
docker run --rm -v $(pwd):/app -v app_logs:/logs --env-file .env python:3.12-slim python /app/config.py # Entorn: production | Port: 8080

Quan ja no necessitem les dades, podem eliminar el volum:

docker volume rm app_logs

Aquest patró — bind mount per al codi, variables d’entorn per a la configuració, i named volume per a dades persistents — és el flux habitual de desenvolupament local amb Docker.

Comandes principals de Docker

ComandaDescripció
docker build -t nom:tag .Construir una imatge des d’un Dockerfile
docker runCrear i executar un contenidor
docker ps / docker ps -aLlistar contenidors actius / tots
docker stop <id>Aturar un contenidor (SIGTERM → SIGKILL)
docker rm <id>Eliminar un contenidor aturat
docker exec -it <id> bashExecutar comanda dins d’un contenidor en marxa
docker logs -f <id>Veure logs d’un contenidor
docker imagesLlistar imatges locals
docker pull / docker pushDescarregar / pujar imatge al registre
docker volume lsLlistar volums
docker network lsLlistar xarxes
docker system pruneEliminar recursos no usats (imatges, contenidors, xarxes)

Docker Compose

Quan una aplicació té múltiples serveis (API + base de dades, per exemple), gestionar cada contenidor individualment es fa feixuc. Docker Compose permet definir tots els serveis en un fitxer compose.yml i gestionar-los amb una sola comanda.

Abans i després

Hands-on — Segueix els passos a la teva terminal. Necessites la imatge myapp:1.0 construïda a Construir i executar.

Sense Compose — comandes llargues per cada servei, incloent la creació manual d’una xarxa perquè els contenidors es puguin trobar pel nom:

docker network create myapp docker run -d --name db --network myapp -e POSTGRES_PASSWORD=secret postgres:17-alpine docker run -d --name app --network myapp -p 8000:8000 -e DATABASE_URL=postgresql://postgres:secret@db:5432/postgres myapp:1.0

Amb Compose — tot definit en un fitxer. Creem un compose.yml al mateix directori. Cada entrada dins de services defineix un contenidor:

# compose.yml name: myapp services: app: image: myapp:1.0 ports: - "8000:8000" environment: - LOG_LEVEL=info db: image: postgres:17-alpine environment: - POSTGRES_PASSWORD=secret
docker compose up -d # Aixeca tots els serveis docker compose ps # Veure l'estat dels serveis docker compose logs app # Logs d'un servei concret docker compose down # Atura i elimina tot

Accedim a http://localhost:8000 — veurem “Hello from Docker!” servit per l’app, mentre PostgreSQL corre en paral·lel. Amb una sola comanda hem aixecat dos serveis.

Compose també crea automàticament una xarxa compartida perquè els contenidors es trobin pel nom — sense necessitat de docker network create.

Nota: aquest exemple no declara volums, així que les dades de PostgreSQL es perden amb docker compose down. Veure Volums per a persistència.

Comandes principals de Compose

Compose tradueix operacions complexes de Docker en comandes simples:

ComposeEquivalent Docker CLIDescripció
docker compose builddocker buildConstruir les imatges dels serveis
docker compose updocker build + docker network create + docker runCrear/arrencar els serveis
docker compose downdocker stop + docker rm + docker network rmAturar i eliminar tot
docker compose rundocker runContenidor puntual
docker compose execdocker execComanda dins d’un contenidor en marxa
docker compose logsdocker logs (per cada contenidor)Logs de tots els serveis
docker compose psdocker ps (filtrat)Estat dels serveis

up — arrenca tots els serveis (o els que especifiquis) tal com estan definits al compose.yml. És el flux normal de treball: aixecar l’aplicació sencera. Si la imatge no existeix i hi ha build:, la construeix. Amb -d s’executa en segon pla:

docker compose up -d # tots els serveis docker compose up -d api db # només els serveis indicats docker compose up --build -d # reconstruir imatges abans d'arrencar

run — crea un contenidor nou per executar una tasca puntual i surt. Ideal per a migracions, tests, scripts o qualsevol operació que necessiti el mateix entorn (xarxa, variables, volums) però que no és un servei permanent:

docker compose run --rm api python manage.py migrate # migració de base de dades docker compose run --rm api pytest # executar tests docker compose run --rm api python seed.py # script puntual

El flag --rm elimina el contenidor en acabar — sense ell, s’acumulen contenidors aturats. A diferència de up, run no publica els ports per defecte (per evitar conflictes amb el servei principal); si els necessites, afegeix --service-ports.

exec — executa una comanda dins d’un contenidor que ja està en marxa. No crea res nou, sinó que s’uneix al contenidor existent. És l’eina de debugging i inspecció:

docker compose exec db psql -U user myapp # obrir una sessió SQL docker compose exec api bash # shell dins del contenidor docker compose exec api cat /app/config.yml # inspeccionar fitxers

La diferència clau entre run i exec: run crea un contenidor nou (el servei no cal que estigui en marxa), exec entra en un que ja existeix (el servei ha d’estar actiu).

Exemple bàsic: API amb base de dades

Un compose.yml mínim per una aplicació web connectada a PostgreSQL:

# compose.yml name: myapp services: api: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/myapp depends_on: - db db: image: postgres:17-alpine environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=myapp volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:

Punts clau:

  • build: . — construeix la imatge des del Dockerfile local en comptes d’usar una imatge preexistent
  • depends_on — garanteix que db arrenca abans que api (però no que estigui llest — això ho resolem a continuació)
  • volumes: postgres_data — les dades de la base de dades persisteixen entre reinicis
  • L’API accedeix a la base de dades com db:5432 — veure Xarxa entre serveis

Gràcies al volum amb nom, un cicle de docker compose down seguit de docker compose up no perd les dades — el volum sobreviu als contenidors. Compte: docker compose down -v elimina els volums, i aquesta acció és irreversible.

Els volums amb nom s’acumulen al disc del host i no s’eliminen automàticament. Convé revisar-los periòdicament:

docker volume ls # llistar volums docker volume rm <nom> # eliminar un volum concret docker volume prune # eliminar tots els volums no usats per cap contenidor

docker volume prune és útil per alliberar espai, però cal anar amb compte: un volum d’un projecte aturat apareix com a “no usat” i serà eliminat.

Per veure els detalls d’un volum concret:

docker volume inspect myapp_postgres_data # mostra el punt de muntatge, driver i data de creació

El nom del volum es forma com <projecte>_<nom> — en aquest cas myapp_postgres_data.

Backup amb docker exec

docker exec permet executar comandes dins d’un contenidor en marxa. Això és útil per fer backups sense aturar el servei — pg_dump genera una còpia consistent de la base de dades encara que hi hagi consultes actives:

# Executem pg_dump dins del contenidor i redirigim la sortida al host docker exec myapp-db-1 pg_dump -U user myapp > backup.sql # Restaurar el backup (< envia el fitxer com a entrada estàndard, -i activa mode interactiu) docker exec -i myapp-db-1 psql -U user myapp < backup.sql

El nom myapp-db-1 segueix el patró per defecte de Compose: <projecte>-<servei>-<instància>.

No s’han de copiar directament els fitxers del volum amb PostgreSQL en marxa — els fitxers de dades no estaran en un estat consistent.

Producció: healthchecks i disponibilitat

L’exemple anterior funciona, però té un problema: depends_on només garanteix l’ordre d’inici, no que PostgreSQL estigui realment llest per acceptar connexions. En producció, afegim healthchecks i condicions d’arrencada:

# compose.yml name: myapp services: api: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/myapp env_file: - path: .env required: false depends_on: db: condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s db: image: postgres:17-alpine environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=myapp volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U user"] interval: 10s timeout: 5s retries: 5 volumes: postgres_data:

Què hem afegit respecte l’exemple bàsic:

  • healthcheck — Docker executa periòdicament el test per saber si el servei funciona (healthy, unhealthy o starting)
  • depends_on amb condition: service_healthy — l’API no arrenca fins que PostgreSQL passi el healthcheck
  • restart: unless-stopped — reinicia el servei automàticament si falla (excepte si l’atures manualment)
  • env_file amb required: false — carrega variables d’un fitxer .env si existeix, sense fallar si no hi és

Xarxa entre serveis

Compose crea automàticament una xarxa privada per a tots els serveis del fitxer. Dins d’aquesta xarxa, cada servei és accessible pel seu nom: l’API pot connectar-se a PostgreSQL simplement com db:5432, sense necessitat de configurar IPs.

Això és el que permet escriure DATABASE_URL=postgresql://user:pass@db:5432/myappdb es resol automàticament a l’adreça IP del contenidor de PostgreSQL.

La distinció important és entre ports interns i ports publicats:

  • La comunicació entre serveis dins de la xarxa de Compose funciona directament — db no necessita ports: perquè l’API hi accedeix per la xarxa interna.
  • La directiva ports: publica un port al host, fent el servei accessible des de fora (el navegador, curl, etc.). Només cal publicar els ports dels serveis que han de ser accessibles externament.

Per això a l’exemple, apiports: "8000:8000" (per rebre peticions externes) però db no en té — la base de dades només ha de ser accessible per l’API, no des del host.

Per inspeccionar les xarxes creades:

docker network ls # Llistar totes les xarxes docker network inspect <nom_xarxa> # Veure detalls (IPs, contenidors connectats)

Substitució de variables amb valors per defecte

Dins de compose.yml podem usar variables d’entorn amb la sintaxi ${VAR:-default}:

services: app: ports: - "${PORT:-8080}:${PORT:-8080}" environment: - PORT=${PORT:-8080} - HEALTH_ENDPOINT=${HEALTH_ENDPOINT:-/health}

Si PORT no està definit (ni a l’entorn ni a .env), s’usa 8080.

Makefile per simplificar comandes

Un patró comú és usar un Makefile per evitar recordar les comandes llargues de Docker Compose:

docker-build: docker compose build app docker-up: docker compose up -d app docker-down: docker compose down test: docker compose run --build --rm test db-up: docker compose up -d db db-reset: docker compose down -v docker compose up -d db

Així, en lloc de recordar les comandes llargues, n’hi ha prou amb make docker-up o make test.

Distribució d’imatges

Un cop tens la imatge construïda i funcionant localment, el pas següent és distribuir-la perquè altres màquines (servidors de producció, companys d’equip) puguin executar-la. El flux complet d’una imatge al llarg de la seva vida és:

build (local) → push (registre) → pull (servidor) → run (producció)

En un projecte real, aquest flux s’automatitza: el pipeline de CI construeix la imatge, la puja al registre amb una etiqueta que identifica la versió, i el servidor de producció la descarrega i l’executa.

docker tag myapp myapp:1.0.0 # etiqueta la imatge abans de pujar-la docker push myapp:1.0.0 # puja la imatge al registre docker pull myapp:1.0.0 # descarrega la imatge en un altre servidor

Docker Hub és adequat per imatges públiques i projectes personals, però en producció les imatges contenen codi propietari, dependències internes i configuracions sensibles que no poden ser públiques. En aquest cas s’utilitzen registres privats, que requereixen autenticació per accedir-hi. El flux de treball és idèntic — push i pull — però les imatges només són accessibles per als equips autoritzats.

Resum de comandes

# === IMATGES === docker build -t nom:tag . # Construir imatge docker build --no-cache -t nom:tag . # Construir sense cache docker build --build-arg VAR=val -t nom:tag . # Build amb ARG docker tag nom:tag nom:nova-tag # Etiquetar imatge docker pull nom:tag # Descarregar imatge docker push nom:tag # Pujar imatge al registre docker images # Llistar imatges # === CONTENIDORS === docker run -d -p 8000:8000 nom # Executar contenidor docker ps # Contenidors en execució docker ps -a # Tots els contenidors docker stop <id> # Aturar docker rm <id> # Eliminar docker logs -f <id> # Veure logs docker exec -it <id> bash # Entrar al contenidor docker inspect <id> # Detalls del contenidor (config, xarxa, estat) # === VOLUMS === docker volume ls # Llistar volums docker volume rm nom # Eliminar volum # === XARXA === docker network ls # Llistar xarxes docker network inspect nom # Detalls d'una xarxa # === COMPOSE === docker compose build # Construir imatges dels serveis docker compose up -d # Aixecar serveis docker compose up --build # Reconstruir i aixecar docker compose down # Aturar i eliminar docker compose down -v # Aturar i eliminar volums docker compose logs -f # Logs de tots els serveis docker compose ps # Estat dels serveis docker compose exec servei bash # Entrar a un servei docker compose run --rm servei cmd # Executar comando puntual # === NETEJA === docker system prune # Elimina imatges, contenidors i xarxes no usats # Builds iteratius acumulen imatges "dangling" silenciosament docker system df # Veure ús d'espai

Annexos

Instal·lació de Docker (Ubuntu/Debian)

# Instal·lar dependencies i afegir clau GPG oficial sudo apt update sudo apt install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Afegir el repositori a les fonts d'Apt sudo tee /etc/apt/sources.list.d/docker.sources <<EOF Types: deb URIs: https://download.docker.com/linux/debian Suites: $(. /etc/os-release && echo "$VERSION_CODENAME") Components: stable Signed-By: /etc/apt/keyrings/docker.asc EOF # Instal·lar Docker sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Comprovar que el servei de Docker està actiu sudo systemctl status docker # Verificar docker --version docker compose version # Opcional: executar Docker sense sudo sudo groupadd docker sudo usermod -aG docker $USER # Cal tancar sessió i tornar a entrar # Verificar instal·lació docker run hello-world

Què passa quan fas docker run?

Un contenidor no és una màquina virtual lleugera — és un procés ordinari del host. No hi ha hipervisor ni kernel convidat. La “màgia” de Docker no està en cap tecnologia secreta, sinó en com orquestra mecanismes que ja existeixen al kernel Linux per crear la il·lusió d’una màquina independent. Vegem què passa pas a pas quan executes docker run nginx:

┌──────────────────────────── HOST ────────────────────────────────────┐ │ │ │ ┌─────────────────── CONTENIDOR (nginx) ─────────────────────────┐ │ │ │ │ │ │ │ Namespaces cgroups │ │ │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ PID │ NET │ MNT │ UTS │ │ mem: màx 512MB │ │ │ │ │ └──────────────────────────┘ │ cpu: màx 50% │ │ │ │ │ └──────────────────────────┘ │ │ │ │ OverlayFS │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ capa escriptura (efímera — desapareix en eliminar) │ │ │ │ │ ├────────────────────────────────────────────────────────┤ │ │ │ │ │ capes de la imatge (read-only, compartides) │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ nginx → "soc PID 1, estic sol al sistema" │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ │ nginx real: PID 3847, un procés més entre molts │ │ │ └──────────────────────────────────────────────────────────────────────┘

1. Docker Engine rep la petició. Analitza la comanda, descarrega la imatge si cal, i prepara la configuració del contenidor (ports, volums, variables d’entorn, límits de recursos).

2. Munta el sistema de fitxers (OverlayFS). Les capes de la imatge s’apilen com a lectures-només, i Docker afegeix una capa fina d’escriptura a sobre. Després usa pivot_root per fer que aquest sistema de fitxers sigui el / del contenidor. Per això els canvis dins d’un contenidor no afecten la imatge original — les escriptures van a la capa efímera, i les capes de la imatge romanen intactes.

3. Crea namespaces. El kernel Linux permet crear “vistes” aïllades de recursos del sistema. Docker crea un conjunt de namespaces per al nou procés:

  • PID namespace: el procés principal del contenidor és PID 1 dins del contenidor, però des del host és un procés normal amb un PID qualsevol (verificable amb ps aux | grep nginx). El contenidor no pot veure cap altre procés del host.
  • Network namespace: el contenidor té les seves pròpies interfícies de xarxa, taula de rutes i adreça IP. Des de dins, sembla que tingui una xarxa dedicada.
  • Mount namespace: el contenidor veu el seu propi arbre de muntatge, aïllat del host.
  • UTS namespace: el contenidor pot tenir el seu propi hostname.
  • User namespace (opcional): permet mapejar l’usuari root dins del contenidor a un usuari sense privilegis al host.

Quan fas docker exec -it web bash, Docker no “entra” al contenidor — el que fa és llançar un nou procés (bash) que s’uneix als namespaces existents del contenidor. Per això bash veu els mateixos processos, la mateixa xarxa i el mateix sistema de fitxers que nginx.

4. Configura cgroups. Els Control Groups limiten quants recursos pot consumir el contenidor: memòria màxima, percentatge de CPU, ample de banda d’I/O. Quan escrius docker run -m 512m, Docker crea un cgroup amb un límit de 512MB — si el procés el supera, el kernel el mata (OOM kill). Sense límits explícits, el contenidor pot consumir tots els recursos del host, cosa que en producció pot fer caure altres serveis.

5. Connecta la xarxa. Docker crea un parell veth (virtual ethernet) — un cable virtual amb dos extrems. Un extrem va dins del network namespace del contenidor, l’altre es connecta al bridge docker0 del host. El mapatge de ports (-p 8080:80) funciona amb regles d’iptables que reenvien el tràfic del port 8080 del host al port 80 dins del namespace de xarxa del contenidor.

client → host:8080 │ iptables/NAT (-p 8080:80) │ ┌─────────────▼──────────────┐ │ bridge docker0 │ 172.17.0.1 (host) └─────────────┬──────────────┘ │ veth pair (cable virtual) ┌─────────────▼──────────────┐ │ eth0 (contenidor) │ 172.17.0.2 │ nginx escolta :80 │ └────────────────────────────┘

6. Arrenca el procés. Finalment, Docker executa el procés configurat (nginx) que es converteix en el PID 1 del contenidor. Des de la perspectiva del host, és un procés més amb aïllament aplicat. Des de dins, nginx creu que està sol en una màquina dedicada amb el seu propi sistema de fitxers, xarxa i arbre de processos.

No hi ha cap kernel addicional, cap capa de virtualització de maquinari. Només un procés Linux que creu que està sol perquè el kernel li amaga tot el que hi ha fora.

Portàtils ARM en un entorn AMD64

Els portàtils Apple Silicon (M1/M2/M3) i alguns portàtils Linux recents usen arquitectura ARM64 (aarch64). La majoria de companys i els servidors de CI/CD usen AMD64 (x86_64). Les dues arquitectures executen jocs d’instruccions incompatibles: un binari compilat per a AMD64 no s’executa directament en ARM64.

Amb Docker, el problema apareix quan un estudiant ARM construeix una imatge sense especificar plataforma i el company AMD64 intenta executar-la:

WARNING: The requested image's platform (linux/arm64) does not match the detected host platform (linux/amd64) exec /usr/bin/app: exec format error

Solució: fixa platform: linux/amd64 al compose.yml

AMD64 és el denominador comú: Docker Desktop sobre Mac ARM i Linux AMD64 el poden executar tots. L’estudiant ARM executarà el contenidor via emulació QEMU (activada automàticament per Docker Desktop), amb comportament idèntic però lleugerament més lent:

# compose.yml services: app: platform: linux/amd64 # ← tots el poden executar build: . db: platform: linux/amd64 image: postgres:17-alpine

I al Dockerfile, fixa la plataforma base explícitament:

FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine

Si el projecte necessita imatges natives per a les dues arquitectures, usa buildx per construir una imatge multiplataforma d’una sola tirada:

docker buildx create --name multibuilder --use docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag registry.example.com/app:latest \ --push .

Docker triarà automàticament la variant correcta en cada màquina en fer docker pull.

Recursos addicionals