Fins ara hem fet feina amb imatges oficials disponibles a Docker Hub. Però en projectes reals sovint necessitam construir les nostres pròpies imatges per empaquetar aplicacions personalitzades. Docker Compose facilita aquest procés amb la clau build, que permet definir com construir imatges directament des del fitxer compose.yaml.
En aquest article aprendrem a construir imatges amb Compose, optimitzar-les amb multi-stage builds i aplicar les bones pràctiques que garanteixen imatges lleugeres, segures i eficients.
Imatges pròpies #
No sempre cal construir imatges personalitzades. Usam imatges oficials quan:
- L’aplicació ja existeix com a imatge (bases de dades, servidors web, eines).
- Només necessitam configurar l’aplicació via variables d’entorn o fitxers muntats.
Construïm imatges pròpies quan:
- Desenvolupam una aplicació que no existeix com a imatge.
- Necessitam dependències específiques no incloses a la imatge oficial.
- Volem empaquetar el codi font amb les seves dependències.
- Necessitam optimitzacions específiques per al nostre entorn.
La clau build
#
La forma més senzilla d’usar build és indicar el directori que conté el Dockerfile:
services:
api:
build: docker/api
ports:
- "3000:3000"Això equival a executar docker build docker/api i usar la imatge resultant per al servei. Ara bé, si volem exercir més control, podem usar la sintaxi estesa:
services:
api:
build:
context: docker/api
dockerfile: Dockerfile
args:
NODE_ENV: production
image: myapp/api:1.0
ports:
- "3000:3000"Les opcions usades són:
context: Directori que conté els fitxers necessaris per a la construcció, que podem entendre com el context sota el qual s’executarà la construcció.dockerfile: Nom del Dockerfile (per defecte,Dockerfile).args: Arguments de construcció que es passen al Dockerfile.
A més, també podem especificar target, que permet construir una imatge amb una etapa diferent d’un multi-stage build (ho veurem més avall en aquest article).
La clau image és opcional però recomanable. Si la definim, la imatge construïda tendrà aquest nom i etiqueta. Si no, Compose genera un nom automàtic basat en el projecte i servei.
Comandes #
Docker Compose ofereix diverses comandes per gestionar la construcció d’imatges. Per a construir totes les imatges definides amb build usarem:
docker compose buildSi només volem construir la imatge d’un servei específic, podem usar:
docker compose build apiSi volem construir imatges sense fer ús de la memòria cau, podem usar:
docker compose build --no-cachePer construir i arrencar els serveis usarem:
docker compose up --buildFinalment, podem usar build amb --progress=plain per veure el procés de construcció en detall:
docker compose build --progress=plainLa diferència entre docker compose build i docker compose up --build és important:
build: Només construeix les imatges, no arrenca els serveis.up --build: Construeix les imatges (si cal) i després arrenca els serveis.
Per defecte,
docker compose upnomés construeix les imatges si no existeixen. Usa--buildper forçar la reconstrucció.
Arguments #
Els arguments de construcció permeten parametritzar el Dockerfile. Es defineixen amb ARG al Dockerfile i es passen des del compose.yaml:
# Dockerfile
# Versió de Node.js amb valor per defecte
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine
# Entorn, amb valor per defecte
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}# compose.yaml
services:
api:
build:
context: docker/api
args:
# Sobreescrivim els valors per defecte
NODE_VERSION: 24
NODE_ENV: productionEls arguments només estan disponibles durant la construcció. Si necessitam que un valor estigui disponible en temps d’execució, hem de convertir-lo en variable d’entorn amb ENV.
Multi-stage builds #
Els multi-stage builds permeten crear imatges optimitzades separant el procés de construcció en múltiples etapes. Cada etapa pot usar una imatge base diferent i només el resultat final s’inclou a la imatge definitiva.
Una etapa #
Sense multi-stage, una imatge per a una aplicació Node.js sol incloure:
- El codi font.
- Les dependències de producció (
node_modules). - Les dependències de desenvolupament (eines de build, compiladors).
- Fitxers temporals i cau.
Això resulta en imatges innecessàriament grans.
Múltiples etapes #
Amb multi-stage builds, separam les etapes de construcció dins el mateix Dockerfile:
# Etapa 1: Construcció
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm clean-install
COPY src ./src
RUN npm run build
# Etapa 2: Producció
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm clean-install --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Vegem pas a pas què fa cada instrucció:
Etapa 1 (builder):
FROM node:24-alpine AS builder— Partim d’una imatge amb Node.js i li donam el nombuilderper poder-la referenciar després.WORKDIR /app— Cream el directori de feina i ens situam a dins.COPY package.json package-lock.json ./— Copiam els fitxers que declaren les dependències del projecte.RUN npm clean-install— Instal·lam totes les dependències (producció i desenvolupament) exactament com estan fixades apackage-lock.json. La comandanpm clean-installés més ràpida i reproduïble quenpm install.COPY src ./src— Copiam tot el codi font de l’aplicació.RUN npm run build— Executam l’script de construcció definit apackage.json, que normalment compila el codi i genera els fitxers optimitzats al directoridist/.
Etapa 2 (producció):
FROM node:24-alpine— Partim d’una imatge neta, sense res de l’etapa anterior.COPY package.json package-lock.json ./— Tornam a copiar els fitxers de dependències.RUN npm clean-install --only=production— Instal·lam només les dependències de producció, excloent les eines de desenvolupament (compiladors, eines de test, etc.).COPY --from=builder /app/dist ./dist— Copiam el codi compilat des de l’etapabuilder. Aquesta és la clau del multi-stage: agafam només el resultat, no les eines.USER node— Canviam a un usuari no privilegiat per seguretat.CMD ["node", "dist/index.js"]— Definim la comanda per arrencar l’aplicació.
L’etapa final només conté les dependències de producció i el codi compilat. Les eines de build (TypeScript, Webpack, Vite, etc.) i els fitxers temporals queden a l’etapa builder i no s’inclouen a la imatge final.
Un dels avantatges dels multi-stage builds és la reducció de la mida de la imatge. Pots consultar-ho amb:
docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}"Fixar l’etapa #
Podem construir fins a una etapa específica amb target:
services:
api:
build:
context: docker/api
target: builderAixò és útil per a desenvolupament, on podem voler una imatge amb les eines de build incloses.
Exemple pràctic #
Per demostrar i practicar la construcció d’imatges personalitzades amb multi-stage, vegem com crear una API REST amb Express.js i PostgreSQL per gestionar una llista de tasques. Abans de res, anem a crear el directori de feina:
mkdir --parents ~/Projects/tasks-api/api/src/middlewares
cd ~/Projects/tasks-apiEstructura del projecte #
L’estructura proposada del projecte és la següent:
tasks-api/
├── compose.yaml
├── .env
└── api/
├── Dockerfile
├── .dockerignore
├── package.json
└── src/
├── app.js
├── db.js
└── middlewares/
└── logger.jsFitxer .env
#
El fitxer .env contendrà les credencials d’accés a la base de dades PostgreSQL i l’entorn d’execució:
# PostgreSQL
POSTGRES_USER=tasks
POSTGRES_PASSWORD=canvia-aquest-password
POSTGRES_DB=tasks
# API
NODE_ENV=productionFitxer package.json
#
El fitxer api/package.json conté la configuració del projecte de Node.js:
{
"name": "tasks-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/app.js",
"dev": "node --watch src/app.js"
},
"dependencies": {
"express": "^5.2.1",
"pg": "^8.20.0"
}
}Tres aspectes a destacar:
-
- L’opció
typeamb valormoduleindica que usam ESModules (sintaxiimport/export) en lloc de CommonJS (sintaxirequire).
- L’opció
- L’opció
scriptsdefineix comandes personalitzades d’execució de l’aplicació. - L’aplicació té dues dependències, el framework web
expressi la llibreriapgper gestionar la base de dades PostgreSQL.
Fitxer db.js
#
El mòdul api/src/db.js gestiona la connexió a PostgreSQL i les operacions amb la taula de tasques. Usam la llibreria pg, que és el client natiu de PostgreSQL per a Node.js.
import pg from "pg";
/**
* Pool de connexions a PostgreSQL.
* Un pool manté múltiples connexions obertes i les reutilitza,
* evitant el cost de crear una connexió nova per cada consulta.
*/
const pool = new pg.Pool({
host: process.env.DB_HOST || "localhost",
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || "tasks",
user: process.env.DB_USER || "tasks",
password: process.env.DB_PASSWORD || "tasks",
});
/**
* Inicialitza la base de dades creant la taula si no existeix.
*/
export async function initDB() {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log("Database initialized");
} finally {
client.release();
}
}
/**
* Retorna totes les tasques ordenades per data de creació.
* @returns {Array<Object>} Llista de tasques
*/
export async function getTasks() {
const result = await pool.query(
"SELECT * FROM tasks ORDER BY created_at DESC"
);
return result.rows;
}
/**
* Crea una nova tasca amb el títol indicat.
* @param {string} title - Títol de la tasca
* @returns {Object} La tasca creada amb tots els seus camps
*/
export async function createTask(title) {
const result = await pool.query(
"INSERT INTO tasks (title) VALUES ($1) RETURNING *",
[title]
);
return result.rows[0];
}
/**
* Marca una tasca com a completada pel seu identificador.
* @param {number} id - Identificador de la tasca
* @returns {Object|undefined} La tasca actualitzada, o undefined si no existeix
*/
export async function completeTask(id) {
const result = await pool.query(
"UPDATE tasks SET completed = TRUE WHERE id = $1 RETURNING *",
[id]
);
return result.rows[0];
}
/**
* Elimina una tasca pel seu identificador.
* @param {number} id - Identificador de la tasca
* @returns {Object|undefined} La tasca eliminada, o undefined si no existeix
*/
export async function deleteTask(id) {
const result = await pool.query(
"DELETE FROM tasks WHERE id = $1 RETURNING *",
[id]
);
return result.rows[0];
}
export default pool;En projectes més grans, és habitual usar un ORM (Object-Relational Mapping) com Prisma, Sequelize o Drizzle per gestionar la base de dades. Aquí usem consultes SQL directes per claredat i perquè l’alumnat ja coneix SQL d’altres assignatures.
Fitxer logger.js
#
El mòdul api/src/middlewares/logger.js defineix un middleware que registra cada petició HTTP a la consola. Els middlewares a Express són funcions que s’executen entre la recepció de la petició i l’enviament de la resposta.
/**
* Middleware que registra les peticions HTTP.
* Mostra el mètode (GET, POST, DELETE, etc.) i la ruta sol·licitada.
*/
const loggerMiddleware = (req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.url}`);
next();
};
export default loggerMiddleware;Fitxer app.js
#
El fitxer api/src/app.js és el punt d’entrada de l’aplicació. Configura el servidor Express, registra els middlewares i defineix els endpoints de l’API.
import express from "express";
import { initDB, getTasks, createTask, deleteTask } from "./db.js";
import loggerMiddleware from "./middlewares/logger.js";
/**
* Instància de l'aplicació Express.
*/
const app = express();
const PORT = process.env.PORT || 3000;
/**
* Middlewares globals.
* - express.json(): Permet rebre i parsejar JSON al cos de les peticions.
* - loggerMiddleware: Registra cada petició a la consola.
*/
app.use(express.json());
app.use(loggerMiddleware);
/**
* Endpoint principal. Retorna informació sobre l'API.
*/
app.get("/", (req, res) => {
res.json({
message: "Tasks API",
version: "1.0.0",
endpoints: {
"GET /tasks": "Llista totes les tasques",
"POST /tasks": "Crea una nova tasca",
"DELETE /tasks/:id": "Elimina una tasca",
},
});
});
/**
* GET /tasks - Retorna totes les tasques.
*/
app.get("/tasks", async (req, res) => {
try {
const tasks = await getTasks();
res.json(tasks);
} catch (error) {
console.error("Error getting tasks:", error);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* POST /tasks - Crea una nova tasca.
* Espera un cos JSON amb el camp "title".
*/
app.post("/tasks", async (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: "Title is required" });
}
try {
const task = await createTask(title);
res.status(201).json({ message: "Task created", task });
} catch (error) {
console.error("Error creating task:", error);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* PATCH /tasks/:id - Marca una tasca com a completada.
*/
app.patch("/tasks/:id", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: "Invalid task ID" });
}
try {
const task = await completeTask(id);
if (!task) {
return res.status(404).json({ error: "Task not found" });
}
res.json({ message: "Task completed", task });
} catch (error) {
console.error("Error updating task:", error);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* DELETE /tasks/:id - Elimina una tasca pel seu identificador.
*/
app.delete("/tasks/:id", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: "Invalid task ID" });
}
try {
const task = await deleteTask(id);
if (!task) {
return res.status(404).json({ error: "Task not found" });
}
res.json({ message: "Task deleted", task });
} catch (error) {
console.error("Error deleting task:", error);
res.status(500).json({ error: "Internal server error" });
}
});
/**
* Inicialitza la base de dades i arrenca el servidor.
*/
initDB()
.then(() => {
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((error) => {
console.error("Failed to initialize database:", error);
process.exit(1);
});Fitxer .dockerignore
#
El fitxer api/.dockerignore funciona com .gitignore però per a Docker. Exclou fitxers i directoris del context de construcció, reduint el temps de build i evitant incloure fitxers innecessaris a la imatge.
node_modules
npm-debug.log
.git
.gitignore
.env
*.mdEn aquest exemple s’exclouen:
node_modules: Les dependències instal·lades localment. S’instal·laran dins la imatge ambnpm clean-install.npm-debug.log: Fitxer de log generat quan npm troba errors..git: El directori de control de versions, innecessari dins la imatge..gitignore: Fitxer de configuració de Git..env: Variables d’entorn locals que poden contenir secrets. Les variables es passen viacompose.yaml.*.md: Fitxers de documentació (README.md,CHANGELOG.md, etc.).
Recorda que el
.dockerignoreha d’estar al directori del context de build, és a dir, al mateix nivell que elpackage.json, no amb elcompose.yaml.
Fitxer Dockerfile
#
Com que la nostra aplicació és JavaScript pur i no requereix compilació, no necessitem un multi-stage build. El multi-stage és útil quan hi ha un pas de build que genera artefactes (com el directori dist/) i volem excloure les eines de compilació de la imatge final. En el nostre cas, un Dockerfile d’una sola etapa és suficient.
Per tant, el nostre fitxer api/Dockerfile queda així:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm clean-install --only=production
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/app.js"]Analitzem cada instrucció:
FROM node:24-alpine— Partim de la imatge oficial de Node.js basada en Alpine Linux.WORKDIR /app— Cream el directori de feina i ens situam a dins.COPY package.json package-lock.json ./— Copiam els fitxers de dependències abans que el codi font per aprofitar la memòria cau de Docker.RUN npm clean-install --only=production— Instal·lam només les dependències de producció.COPY src ./src— Copiam el codi font de l’aplicació.USER node— Executam l’aplicació amb l’usuarinode, que ja ve inclòs a les imatges oficials de Node.js.EXPOSE 3000— Documentam el port que usa l’aplicació (no el publica, només informa).CMD ["node", "src/app.js"]— Definim la comanda per arrencar l’aplicació.
Fitxer compose.yaml
#
Finalment, el fitxer compose.yaml orquestra els dos serveis: l’API que construïm amb el nostre Dockerfile i la base de dades PostgreSQL que usa una imatge oficial.
services:
api:
build:
context: ./api
image: tasks-api:1.0
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: ${NODE_ENV}
DB_HOST: db
DB_PORT: 5432
DB_NAME: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "3000:3000"
restart: unless-stopped
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres-data:Observa com el servei api usa build.context per indicar el directori que conté el Dockerfile, mentre que db usa directament una imatge oficial amb image. Quan definim ambdues claus (build i image) al servei api, la imatge construïda es guardarà amb el nom i etiqueta especificats.
Arrencada #
Usarem la comanda habitual per a arrencar els serveis:
docker compose up --build --detachVerificam que els serveis funcionen:
docker compose psSi escau, també podem revisar els logs:
docker compose logs apiUna vegada arrencats els serveis, podem començar a provar l’API. Comencem creant un parell de tasques:
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Aprendre Docker Engine"}'
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Aprendre Docker Compose"}'
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Aprendre Docker Swarm"}'
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Tornar a veure The Expanse"}'Ara ja podem llistar les tasques:
curl http://localhost:3000/tasksTambé podem marcar una tasca com a completada:
curl -X PATCH http://localhost:3000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'I, finalment, podem eliminar una tasca:
curl -X DELETE http://localhost:3000/tasks/1Bones pràctiques #
Algunes recomanacions per construir imatges de manera eficient i segura.
Ordre de les instruccions
L’ordre de les instruccions al Dockerfile afecta directament l’eficiència de la memòria cau. Docker reutilitza capes anteriors si no han canviat. Ordena les instruccions de manco a més propenses a canviar:
# 1. Imatge base (canvia rarament)
FROM node:24-alpine
# 2. Directori de feina (canvia rarament)
WORKDIR /app
# 3. Dependències (canvia quan actualitzam paquets)
COPY package.json package-lock.json ./
RUN npm clean-install --only=production
# 4. Codi font (canvia freqüentment)
COPY src ./src
# 5. Configuració d'execució
USER node
EXPOSE 3000
CMD ["node", "src/app.js"]Amb aquest ordre, si només canvia el codi font, Docker reutilitza les capes 1, 2 i 3 i només reconstrueix a partir de la 4.
El fitxer .dockerignore
Inclou sempre un .dockerignore per excloure:
- El directori on s’instal·len les dependències localment, ja que es tornaran a instal·lar dins la imatge durant el build. Alguns exemples segons l’ecosistema:
- Node.js:
node_modules/ - Python:
.venv/,__pycache__/,*.pyc,.eggs/,*.egg-info/. - Java:
target/(Maven),build/(Gradle) - PHP:
vendor/ - Ruby:
vendor/bundle/
- Node.js:
- Fitxers de control de versions (
.git). - Fitxers de configuració local (
.env,.env.local). - Documentació i fitxers innecessaris.
- Logs i fitxers temporals.
Recorda que el
.dockerignoreha d’estar al directori del context de build (el que especifiques abuild.context), és a dir, al mateix nivell que elpackage.json, no amb elcompose.yaml.
Imatges base
Prefereix imatges lleugeres com les variants -alpine:
| Imatge | Mida aproximada |
|---|---|
node:24 |
~1.1 GB |
node:24-slim |
~250 MB |
node:24-alpine |
~150 MB |
Les imatges Alpine són més petites però usen musl en comptes de glibc, cosa que pot causar problemes amb algunes dependències natives. Si tens problemes, prova amb -slim.
Usuari no privilegiat
Mai executis aplicacions com a root dins el contenidor. Les imatges oficials de Node.js ja inclouen un usuari node que podem usar directament:
USER nodeSi uses una imatge base que no inclou un usuari no privilegiat, pots crear-lo:
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup
USER appuserUsa CMD amb sintaxi exec
Prefereix la sintaxi exec (amb claudàtors) per a la instrucció CMD:
CMD ["node", "src/app.js"]Evita la sintaxi shell
CMD node src/app.jsLa sintaxi exec executa el procés directament, sense passar per una shell. Això permet que els senyals del sistema (com SIGTERM per aturar el contenidor) arribin correctament al procés Node.js.
Exercicis #
Es proposen dos exercicis pràctics per facilitar l’aprenentatge progressiu.
Exercici 1 #
Afegir un frontend React
L’equip de frontend ha preparat una aplicació React senzilla que consumeix l’API de tasques. Descarrega’t l’aplicació de frontend.
La teva tasca és:
- Descarrega i extreu el codi del frontend al directori
frontend/dins el projecte. - Crea un
Dockerfilemulti-stage per al frontend que:- Etapa 1 (
builder): Usanode:24-alpine, instal·la dependències i compila l’aplicació ambnpm run build - Etapa 2: Usa
nginx:1.29-alpineper servir els fitxers estàtics
- Etapa 1 (
- Modifica el
compose.yamlper afegir el serveifrontendal port 8080. - Verifica que pots accedir al frontend a
http://localhost:8080/i que es comunica correctament amb l’API.
Pista: El resultat de
npm run buildes genera al directoridist/. NGINX serveix fitxers des de/usr/share/nginx/html.
Respostes
L’estructura del projecte després d’afegir el frontend:
tasks-api/
├── compose.yaml
├── .env
├── api/
│ └── ...
└── frontend/
├── Dockerfile
├── .dockerignore
├── package.json
├── index.html
├── vite.config.js
└── src/
├── main.jsx
├── App.jsx
└── App.cssCrea el fitxer frontend/.dockerignore:
node_modules
dist
.git
.gitignore
*.mdCrea el fitxer frontend/Dockerfile:
# Etapa 1: Construcció
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm clean-install
COPY . .
RUN npm run build
# Etapa 2: Servir amb Nginx
FROM nginx:1.29-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Modifica el fitxer compose.yaml per afegir el servei frontend:
services:
api:
build:
context: ./api
image: tasks-api:1.0
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: ${NODE_ENV}
DB_HOST: db
DB_PORT: 5432
DB_NAME: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "3000:3000"
restart: unless-stopped
frontend:
build:
context: ./frontend
image: tasks-frontend:1.0
depends_on:
- api
ports:
- "8080:80"
restart: unless-stopped
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres-data:Construeix i arrenca tots els serveis:
docker compose up --build --detachVerifica que els contenidors han arrencat:
docker compose psI, finalment, obre el navegador a http://localhost:8080/ per veure l’aplicació completa.
Exercici 2 #
Optimització d’un Dockerfile
El següent Dockerfile funciona però té diversos problemes d’optimització. Identifica els problemes i reescriu-lo seguint les bones pràctiques:
FROM node:19
WORKDIR /app
COPY . .
RUN npm install
RUN npm cache clean --force
EXPOSE 3000
CMD ["npm", "start"]Problemes a identificar:
- Quin tipus d’imatge base s’hauria d’usar?
- Quin és el problema amb l’ordre de les instruccions?
- Com es podria millorar la instal·lació de dependències?
- Falta alguna cosa relacionada amb la seguretat?
- Hi ha instruccions que es podrien combinar?
Respostes
Problemes identificats:
- Imatge base: Usa
node:19(1.1 GB) en lloc denode:24-alpine(140 MB i LTS). - Ordre de les instruccions: Copia tot el codi abans d’instal·lar dependències, invalidant la cau cada vegada que canvia qualsevol fitxer.
- Instal·lació de dependències: Usa
npm installen lloc denpm ci, i instal·la dependències de desenvolupament. - Seguretat: No crea ni usa un usuari no privilegiat.
- Instruccions separades:
npm installinpm cache cleanpodrien combinar-se, però millor no fercache cleanambnpm ci.
Dockerfile optimitzat:
# Etapa 1: Dependències
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Etapa 2: Producció
FROM node:24-alpine
WORKDIR /app
# Copiar dependències i codi
COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
COPY package.json ./
# Configuració
USER node
EXPOSE 3000
CMD ["node", "src/app.js"]Millores aplicades:
- Imatge base
alpine(molt més lleugera) - Multi-stage build per separar construcció i execució
npm ciper a instal·lacions reproduïbles--only=productionper excloure dependències de desenvolupament- Ordre optimitzat: primer
package.json, després el codi - Usuari no privilegiat per seguretat
CMDexecuta directamentnodeen lloc denpm start(un procés manco)