Dockerfile - Construirea unei imagini proprii de la zero

Dockerfile - Construirea unei imagini proprii de la zero

Acest articol explică ce este un Dockerfile, cum funcționează sistemul de straturi (layers) al Docker și cum poți construi o imagine personalizată pas cu pas.


Ce este un Dockerfile?

Un Dockerfile este un fișier text simplu, fără extensie, care conține o succesiune de instrucțiuni ce descriu cum se construiește o imagine Docker.

Procesul de build urmează pașii:

  1. Scrii un Dockerfile
  2. Rulezi docker build
  3. Docker produce o imagine (read-only, formată din straturi)
  4. Rulezi imaginea ca container (adaugă un strat read-write deasupra)
  5. Opțional, distribui imaginea printr-un Registry (Docker Hub, ECR etc.)

Structura de bază

Fiecare linie dintr-un Dockerfile este o instrucțiune. Ordinea contează - Docker execută instrucțiunile de sus în jos și creează câte un strat (layer) pentru fiecare.

# Comentariile încep cu #
INSTRUCȚIUNE argumente

Instrucțiunile esențiale

FROM

Obligatorie. Prima instrucțiune. Specifică imaginea de bază (base image).

FROM ubuntu:22.04
FROM node:20-alpine
FROM python:3.12-slim
FROM scratch          # imagine goală, pentru binare statice
Recomandare: Folosește variante slim sau alpine pentru imagini mai mici. Evită latest - fixează versiunea explicit.

WORKDIR

Setează directorul de lucru pentru toate instrucțiunile următoare. Dacă nu există, îl creează.

WORKDIR /app

Echivalentul unui cd /app persistent în imagine.

COPY și ADD

Copiază fișiere din contextul de build în imagine.

COPY src/ /app/src/         # copiază directorul src/
COPY package.json .         # copiază în WORKDIR curent
ADD archive.tar.gz /data/   # ADD dezarhivează automat
Instrucțiune Dezarhivare automată URL remote
COPY Nu Nu
ADD Da (.tar, .gz) Da
Recomandare: Preferă COPY în locul ADD dacă nu ai nevoie de dezarhivare - comportamentul este mai previzibil.

RUN

Execută comenzi în shell la momentul build-ului. Fiecare RUN creează un strat nou.

RUN apt-get update && apt-get install -y curl git
RUN npm install
RUN pip install -r requirements.txt
Recomandare: Combină comenzile cu && pe o singură linie RUN pentru a reduce numărul de straturi și dimensiunea imaginii.
# Bine
RUN apt-get update \
    && apt-get install -y curl git \
    && rm -rf /var/lib/apt/lists/*
 
# Evită (3 straturi separate)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

ENV

Setează variabile de mediu disponibile atât la build, cât și la runtime.

ENV NODE_ENV=production
ENV PORT=3000 HOST=0.0.0.0

ARG

Variabile disponibile doar la build-time, nu în containerul final.

ARG VERSION=1.0
ARG SECRET_KEY        # transmis cu --build-arg SECRET_KEY=xyz
Atenție: Nu folosi ARG pentru secrete - valorile sunt vizibile în istoricul imaginii (docker history).

EXPOSE

Documentează portul pe care aplicația ascultă. Nu publică portul - doar înregistrează intenția.

EXPOSE 3000
EXPOSE 8080/tcp
EXPOSE 5353/udp

Publicarea efectivă se face la rulare cu -p 3000:3000.

CMD și ENTRYPOINT

Definesc comanda implicită la pornirea containerului.

Instrucțiune Scop Poate fi suprascris
CMD Comandă implicită Da (docker run … mycommand)
ENTRYPOINT Punctul de intrare fix Greu (–entrypoint)
# Forma exec (preferată - fără shell intermediar)
CMD ["node", "server.js"]
ENTRYPOINT ["python", "-m", "gunicorn"]
 
# Forma shell (evită)
CMD node server.js

Combinarea lor:

ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]       # argumente implicite, suprascribile

VOLUME

Declară un punct de montare pentru date persistente.

VOLUME ["/data", "/logs"]

USER

Schimbă utilizatorul pentru instrucțiunile următoare și pentru procesul final.

RUN useradd -m appuser
USER appuser
Recomandare de securitate: Rulează întotdeauna aplicațiile ca utilizator non-root.

LABEL

Adaugă metadate la imagine (autor, versiune, descriere).

LABEL maintainer="echipa@exemplu.ro"
LABEL version="2.1.0"
LABEL description="API REST pentru serviciul X"

HEALTHCHECK

Definește o comandă pentru verificarea stării containerului.

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Exemplu complet: aplicație Node.js

# ── Etapa 1: dependențe ──────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
 
# ── Etapa 2: build ───────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# ── Etapa 3: imagine finală ───────────────────────────────────
FROM node:20-alpine AS final
LABEL maintainer="dev@exemplu.ro"
LABEL version="1.0.0"
 
ENV NODE_ENV=production
ENV PORT=3000
 
WORKDIR /app
 
# Copiază doar ce e necesar
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
 
# Utilizator non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
 
EXPOSE 3000
 
HEALTHCHECK --interval=30s --timeout=5s \
  CMD wget -qO- http://localhost:3000/health || exit 1
 
CMD ["node", "dist/server.js"]

Aceasta folosește multi-stage build - o tehnică prin care imaginea finală conține doar artefactele necesare, nu întregul toolchain de build.


Fișierul .dockerignore

Similar cu .gitignore, exclude fișiere din contextul de build (măresc dimensiunea și pot expune date sensibile).

# .dockerignore
node_modules/
.git/
*.log
.env
.env.local
dist/
coverage/
**/*.test.ts
Dockerfile
docker-compose*.yml
README.md

Comenzi de lucru

# Construiește imaginea cu tag
docker build -t myapp:1.0 .
 
# Specifică un Dockerfile cu alt nume
docker build -f Dockerfile.prod -t myapp:prod .
 
# Transmite argumente de build
docker build --build-arg VERSION=2.0 -t myapp:2.0 .
 
# Fără cache (rebuild complet)
docker build --no-cache -t myapp:latest .
 
# Construiește o anumită etapă (multi-stage)
docker build --target builder -t myapp:debug .
 
# Rulează containerul
docker run -d -p 3000:3000 --name myapp myapp:1.0
 
# Rulează interactiv (debug)
docker run -it --rm myapp:1.0 sh
 
# Inspectează straturile imaginii
docker history myapp:1.0
 
# Publică imaginea
docker tag myapp:1.0 utilizator/myapp:1.0
docker push utilizator/myapp:1.0

Bune practici

1. Ordinea instrucțiunilor - cache eficient

Docker reutilizează layerele din cache dacă instrucțiunile nu s-au schimbat. Pune fișierele care se schimbă rar sus, cele care se schimbă des jos.

# Bine: package.json se schimbă rar față de codul sursă
COPY package*.json ./
RUN npm ci
COPY . .          # sursele se schimbă des - copiază la final
 
# Evită: orice schimbare în sursă invalidează și npm ci
COPY . .
RUN npm ci

2. Imagini mici

  • Folosește imagini alpine sau slim ca bază
  • Curăță cache-ul managerului de pachete în același RUN:
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

3. Multi-stage builds

Separă etapa de compilare de imaginea finală de producție. Rezultatul: imagini de 5–10× mai mici.

4. Securitate

  • Nu rula ca root - creează un utilizator dedicat cu USER
  • Nu include secrete în Dockerfile - folosește –secret la build sau variabile de mediu la runtime
  • Scanează imaginile cu docker scout sau trivy

5. Un proces per container

Un container = un singur proces principal. Evită să rulezi mai multe servicii (nginx + app + db) în același container.


Referințe