NewsPortal Deploy - Docker, nginx, VPS

Partea 2 din 7

Actualizat 2026-05-24Sursă pe GitHub ↗Cod start

Parte 1 - Dockerfile pentru API .NET

Un Dockerfile e o rețetă care descrie cum se construiește o imagine pas cu pas. Fiecare linie e o instrucțiune (FROM, RUN, COPY, etc.) care produce un layer - un strat al filesystem-ului final. Layer-ele sunt cached: dacă o linie nu se schimbă, build-ul reutilizează layer-ul existent.

Structura

Instrucțiunile esențiale:

Instrucțiune Ce face
FROM imagine:tag imaginea de bază pe care construim (linie obligatorie, prima)
WORKDIR /path setează directorul curent în container
COPY src dest copiază fișiere din build context în imagine
RUN comanda execută o comandă la build time (generează un layer nou)
ENV VAR=value setează variabile de mediu
EXPOSE 8080 declarativ - documentare port (nu publică efectiv)
USER nume schimbă user-ul pentru comenzile următoare și runtime
ENTRYPOINT [...] comanda principală la docker run
CMD [...] argumentele default (sau comanda standalone) la docker run

ENTRYPOINT vs CMD: ENTRYPOINT e binarul principal (ex dotnet), CMD e argumentele default (ex ["app.dll"]). La docker run, argumentele suplimentare se adaugă la ENTRYPOINT.

Multi-stage build pentru .NET

Pentru .NET aveți nevoie de două stagii:

  1. Build stage - folosește imaginea SDK (~700MB), face dotnet restore + dotnet publish.
  2. Runtime stage - folosește imaginea aspnet (~110MB pe alpine), copiază output-ul de publish din stage 1.

Imaginea finală conține doar runtime-ul + DLL-urile aplicației. SDK-ul, sursele, build-ul intermediar - nu sunt acolo. Atac surface mai mic, transferuri mai rapide, deploy mai eficient.

Multi-stage build: SDK image construiește, doar artefactele se copiază în imaginea runtime

# Stage 1: Build cu .NET 8 SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src

# Layer cache: COPY *.csproj inainte de COPY restul sursei.
# Daca dependintele NuGet nu se schimba, layer-ul de restore e cached.
COPY *.csproj ./
RUN dotnet restore Lab12.csproj

# Acum copiem restul sursei si compilam
COPY . .
RUN dotnet publish Lab12.csproj -c Release -o /out --no-restore /p:UseAppHost=false

# Stage 2: Runtime ASP.NET Core 8
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app

# Copiem output-ul publish-ului din stage 1
COPY --from=build /out .

# User non-root pentru security (least privilege)
RUN mkdir -p /app/logs \
 && addgroup -S app \
 && adduser -S -G app app \
 && chown -R app:app /app
USER app

# Kestrel asculta default pe localhost:5000 - in container nu accepta conexiuni externe.
# Forteaza ascultarea pe 0.0.0.0:8080 prin env var.
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production

ENTRYPOINT ["dotnet", "Lab12.dll"]

De ce ordinea instrucțiunilor contează

Layer caching merge secvențial: dacă o instrucțiune se schimbă, toate cele de după se re-execută. Asta înseamnă că puneți ce se schimbă rar la început și ce se schimbă des la final.

Exemplul concret: dotnet restore rulează pe baza lui *.csproj. Dacă .csproj nu s-a schimbat (NuGet packages nu s-au schimbat), restore-ul e cached - economisiți 30-60s de download.

Dacă scrieți:

COPY . .
RUN dotnet restore
RUN dotnet publish ...

Atunci la fiecare modificare în cod, layer-ul COPY . . se invalidează, deci dotnet restore rulează din nou (download NuGet de la zero). Lent.

Cu pattern-ul corect:

COPY *.csproj ./    # se schimba doar la add/remove package
RUN dotnet restore
COPY . .            # se schimba la fiecare salvare cod
RUN dotnet publish

Restore-ul rămâne cached până când modificați .csproj. Magnific.

Layer caching: ordine greșită reface restore-ul la fiecare modificare; ordine corectă îl păstrează cached

.dockerignore

Ca .gitignore, dar pentru Docker. Definește ce NU intră în build context. Util pentru:

  • Performanță (context-ul mai mic = upload mai rapid către Docker daemon)
  • Security (nu trimiteți .env, chei, etc. către daemon)
  • Corectness (nu copiați bin/ cu artefacte de Windows într-un container Linux)

Pentru .NET API tipic:

**/bin/
**/obj/
**/.vs/
**/*.user
.git/
.github/
**/.DS_Store
README.md
*.md

# Frontend separat - alt build context
news-portal-app/

# Tests nu se livreaza in productie
Lab12.Tests/

# Sensitive
*.env
!.env.example
certs/
**/logs/

Fără .dockerignore, primul COPY . . ar copia 200+MB de bin/Debug/net8.0/. Cu el, doar codul real (~5MB).

Build + run (când ajungem la deploy)

Comenzile uzuale pentru un Dockerfile, rulate pe server-ul cursului (după ce am instalat Docker în secțiunea 04):

# Build din folder-ul cu Dockerfile (build context = .)
docker build -t newsportal-api .

# Verificati imaginea construita
docker images | grep newsportal-api
# REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
# newsportal-api    latest    abc123def456   12 seconds ago  118MB

# Pornire standalone (mapeaza port 8080 host -> 8080 container)
docker run -d --name newsportal-api-test -p 8080:8080 newsportal-api

# Logs
docker logs -f newsportal-api-test

# Cleanup
docker stop newsportal-api-test && docker rm newsportal-api-test

Containerul pornește și crash-ează imediat: aplicația încearcă să se conecteze la Postgres (Host=db;...), dar nu există nicio bază de date încă. Ăsta-i pretextul perfect pentru secțiunea 02 - docker compose, care orchestrează și DB-ul, și API-ul, și proxy-ul, într-un singur ansamblu.

Ce ați învățat

  • Multi-stage build pentru .NET (SDK -> aspnet, imagine finală mică)
  • Layer caching prin ordinea COPY/RUN
  • .dockerignore pentru context curat
  • USER non-root (least privilege)
  • ASPNETCORE_URLS=http://+:8080 (Kestrel ascultă pe 0.0.0.0)
  • EXPOSE (declarativ) vs -p (efectiv la run)

În secțiunea următoare legăm toate piesele cu docker compose.