Docker bàsic
- Introducció
- Conceptes fonamentals
- Primers passos
- Dockerfile: crear imatges pròpies
- Volums i variables d’entorn
- Docker Compose
- Distribució d’imatges
- Resum de comandes
- Annexos
- Recursos addicionals
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

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 Virtual | Contenidor | |
|---|---|---|
| Aïllament | Complet (Guest OS propi) | A nivell de procés (namespaces) |
| Pes | GBs | MBs |
| Arrencada | Minuts | Segons |
| Rendiment | Overhead de virtualització | Quasi natiu |
| Kernel | Propi (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
| Flag | Significat | Exemple |
|---|---|---|
-d | Execució en segon pla (detached) | docker run -d nginx |
-it | Shell interactiva | docker run -it ubuntu bash |
-p | Mapejar ports (host:contenidor) | -p 8080:80 |
--name | Assignar un nom al contenidor | --name web |
--rm | Eliminar automàticament en aturar | docker run --rm nginx |
-e | Variable d’entorn | -e LOG_LEVEL=debug |
-v | Muntar 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:
ENTRYPOINTdefineix el que és el contenidor. Un contenidor que serveix prediccions sempre arrencaràuvicorn, independentment de com l’executis.CMDdefineix com s’executa per defecte. Quina app, quin port, quins flags. Es pot sobreescriure endocker runsense 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
| Tipus | Quan usar | Gestió |
|---|---|---|
| Named volume | Producció, dades persistents | Docker |
| Bind mount | Desenvolupament, compartir fitxers | Host |
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-slimdirectament.
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
| Comanda | Descripció |
|---|---|
docker build -t nom:tag . | Construir una imatge des d’un Dockerfile |
docker run | Crear i executar un contenidor |
docker ps / docker ps -a | Llistar contenidors actius / tots |
docker stop <id> | Aturar un contenidor (SIGTERM → SIGKILL) |
docker rm <id> | Eliminar un contenidor aturat |
docker exec -it <id> bash | Executar comanda dins d’un contenidor en marxa |
docker logs -f <id> | Veure logs d’un contenidor |
docker images | Llistar imatges locals |
docker pull / docker push | Descarregar / pujar imatge al registre |
docker volume ls | Llistar volums |
docker network ls | Llistar xarxes |
docker system prune | Eliminar 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.0construï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:
| Compose | Equivalent Docker CLI | Descripció |
|---|---|---|
docker compose build | docker build | Construir les imatges dels serveis |
docker compose up | docker build + docker network create + docker run | Crear/arrencar els serveis |
docker compose down | docker stop + docker rm + docker network rm | Aturar i eliminar tot |
docker compose run | docker run | Contenidor puntual |
docker compose exec | docker exec | Comanda dins d’un contenidor en marxa |
docker compose logs | docker logs (per cada contenidor) | Logs de tots els serveis |
docker compose ps | docker 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 preexistentdepends_on— garanteix quedbarrenca abans queapi(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 sí 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,unhealthyostarting)depends_onambcondition: service_healthy— l’API no arrenca fins que PostgreSQL passi el healthcheckrestart: unless-stopped— reinicia el servei automàticament si falla (excepte si l’atures manualment)env_fileambrequired: false— carrega variables d’un fitxer.envsi 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/myapp — db 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 —
dbno necessitaports: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, api té ports: "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
- Documentació oficial: docs.docker.com
- Docker Hub: hub.docker.com — Registre d’imatges públiques
- Best practices: docs.docker.com/build/building/best-practices