Alege-ți stack-ul ca să vezi doar pașii relevanți. Restul (alte baze de date, alte limbaje) rămâne ascuns până schimbi filtrul.

Deploy proiect pe student-dev.ro

Ghid pentru deploy-ul proiectului vostru pe VPS-ul cursului. Aceiași pași merg și local cu Docker Desktop (vezi --profile local la Pas 6) - doar SSH-ul + URL-ul public diferă.

Modelul: proiectul vostru e un repo cu docker-compose.yml

Nu există “template magic” pe care îl completați pe server. Proiectul vostru e un repo care conține, pe lângă cod, și rețeta de rulare: un docker-compose.yml + câte un Dockerfile per serviciu. Îl aduceți întreg pe server (git clone / rsync / scp) și rulați docker compose up -d --build. Atât.

Arhiva demo newsportal.zip e exemplul de referință - urmați structura ei, dar adaptată la stack-ul vostru:

NewsPortal/
  backend/            cod backend + Dockerfile-ul lui          (.NET / Node / Python / Java)
  news-portal-app/    cod frontend SPA + Dockerfile + nginx.conf  (Angular / React / Vue) - optional
  nginx/              reverse proxy DOAR pentru rulare locala (profile: local)
  certs/              cert self-signed pt local (gen-cert.sh / .bat)
  docker-compose.yml  orchestreaza db + api + frontend
  .env.example        toate variabilele sunt OPTIONALE (au default-uri)
  credentials.sh      printeaza credentialele efective (pt Adminer)

Contractul cu serverul cursului (invarianții pe care NU-i schimbați)

Indiferent de stack, docker-compose.yml-ul vostru trebuie să respecte:

Regulă De ce
name: student-${USER:-CHANGE_ME} Proiect compose scoped per-user; $USER = username-ul vostru Linux
Containere student-${USER}-api, -db, -frontend nginx-ul cursului rutează <sub>.student-dev.ro -> containerele voastre după nume
expose: nu ports: Mapare ports: pe 80/443 ar intra în conflict cu nginx-ul cursului
networks: [studlab_net] cu external: true Rețeaua partajată unde nginx-ul cursului vă vede containerele
API ascultă pe 8080, frontend pe 80 nginx-ul cursului trimite /api/* /swagger /Auth/* /images/* -> api:8080, restul /* -> frontend:80

Pe studlab, nginx-ul cursului face TLS termination (Let’s Encrypt) + routing-ul de mai sus. Voi nu rulați niciun nginx pe server - proxy-ul din arhivă (nginx/) e marcat profile: local și pornește doar pe laptop.

Ce primiți

Email cu subiectul “DAW 2025-2026 - Cont pe serverul de deploy” conține:

  • SSH: host student-dev.ro, username (ex ziarist), parolă inițială
  • URL public: https://<subdomeniul-vostru>.student-dev.ro
  • Adminer (UI web pentru DB): https://db.student-dev.ro

Cont demo public: ca să vedeți serverul fără cont propriu, vă logați ca ziarist cu parola ziarist (ssh ziarist@student-dev.ro). E cont de demo - nu puneți codul vostru acolo. Pentru deploy real folosiți contul vostru din email.

Pas 1 - Aduceți repo-ul pe server

Conectați-vă și aduceți codul. Trei variante, în ordinea recomandată:

ssh <username>@student-dev.ro    # parola din email

A. git clone (recomandat)

Dacă proiectul vostru e pe GitHub, e cel mai curat:

mkdir -p ~/apps && cd ~/apps
git clone https://github.com/<voi>/<proiect>.git
cd <proiect>

La fiecare update: git pull pe server, apoi rebuild (Pas 6).

B. rsync de pe laptop (fără git, sau pentru cod nepublicat)

rsync transferă doar fișierele schimbate. Windows (PowerShell + cwRsync, choco install rsync):

$env:RSYNC_RSH = "ssh"
rsync -rlptD --delete --chmod=D755,F644 `
  --exclude='.git' --exclude='bin' --exclude='obj' --exclude='node_modules' `
  --exclude='*.user' --exclude='.vs' --exclude='.env' `
  C:/cale/spre/proiect/ <username>@student-dev.ro:~/apps/<proiect>/

Linux / Mac:

rsync -rlptD --delete --chmod=D755,F644 \
  --exclude='.git' --exclude='bin' --exclude='obj' --exclude='node_modules' \
  --exclude='*.user' --exclude='.vs' --exclude='.env' \
  ~/cale/spre/proiect/ <username>@student-dev.ro:~/apps/<proiect>/

--exclude='.env' e important: nu suprascrieți .env-ul de pe server (parolele voastre) cu unul local. --delete șterge pe server ce ați șters local.

C. scp (simplu, fără cwRsync)

cd /cale/spre/proiect
rm -rf bin obj node_modules .vs      # build artifacts - Docker le reface
scp -r . <username>@student-dev.ro:~/apps/<proiect>/

D. Doar pentru demo NewsPortal - curl direct pe server

Pentru tema “hello world”, luați arhiva gata făcută direct pe server (nimic de pe laptop):

cd ~ && curl -fLO https://daw.danielwagner.ro/downloads/newsportal.zip
mkdir -p apps && cd apps && unzip ~/newsportal.zip && cd NewsPortal
docker compose up -d --build         # default-urile merg fara .env

Pas 2 - docker-compose.yml (schelet)

Structura generala a fisierului. Alegeti DB-ul si frontend-ul din bara de sus - mai jos apar blocurile concrete si la final composeul complet gata de copiat.

name: student-${USER:-CHANGE_ME}

services:

  # BLOCUL DB ─ copiati din sectiunea "Serviciul db" de mai jos
  db: ...

  api:
    build:
      context: ./backend            # folderul cu codul backend + Dockerfile-ul lui
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      # CONNECTION STRING ─ copiati din sectiunea DB de mai jos
      # ConnectionStrings__DefaultConnection: "..."
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]                # NU ports:
    networks: [studlab_net]
    restart: unless-stopped

  # FRONTEND SPA ─ omiteti tot serviciul daca nu aveti SPA separat
  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Pentru un stack non-.NET inlocuiti cheile environment: ale serviciului api cu cele ale framework-ului vostru. Numele containerelor + expose + networks raman identice.

(Optional) proxy pentru rulare locala pe laptop

Daca vreti sa rulati standalone pe laptop (fara serverul cursului), adaugati serviciul proxy - exact ca in arhiva. E gated cu profiles: [local], deci nu porneste pe studlab:

  proxy:
    profiles: [local]
    image: nginx:1.27-alpine
    container_name: student-${USER:-CHANGE_ME}-proxy
    depends_on: [api, frontend]
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certs:/etc/nginx/certs:ro
    networks: [studlab_net]
    restart: unless-stopped

Serviciul db - PostgreSQL

  db:
    image: postgres:16-alpine
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      POSTGRES_USER:     ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      POSTGRES_DB:       ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks: [studlab_net]
    restart: unless-stopped

Connection string pentru api.environment:

ConnectionStrings__DefaultConnection: "Host=student-${USER:-CHANGE_ME}-db;Port=5432;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};Username=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}"

Gotcha .NET + Postgres: daca veniti de la SqlServer, Npgsql cere DateTime cu Kind=Utc. Fix intr-o linie la inceputul Program.cs:

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

Serviciul db - MySQL / MariaDB

  db:
    image: mysql:8
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      MYSQL_DATABASE:      ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_USER:          ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_PASSWORD:      ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/var/lib/mysql]
    networks: [studlab_net]
    restart: unless-stopped

Connection string pentru api.environment:

ConnectionStrings__DefaultConnection: "Server=student-${USER:-CHANGE_ME}-db;Port=3306;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};User=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026};"

Serviciul db - MongoDB (NoSQL)

  db:
    image: mongo:7
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/data/db]
    networks: [studlab_net]
    restart: unless-stopped

Connection string pentru api.environment:

ConnectionStrings__DefaultConnection: "mongodb://${DB_USER:-app}:${DB_PASSWORD:-demo-pass-2026}@student-${USER:-CHANGE_ME}-db:27017/${DB_NAME:-app}?authSource=admin"

(Adminer nu suporta Mongo nativ - puneti un container mongo-express in propriul compose daca vreti UI web.)

SQLite (fără container db)

SQLite e file-based. Ștergeți complet serviciul db + depends_on din api, și dați un volum pentru fișierul DB:

  api:
    # ...
    volumes: [db_data:/app/data]
volumes:
  db_data:

Connection string: Data Source=/app/data/app.sqlite

SQL Server (NU recomandat pe studlab)

MSSQL ocupă ~1.5GB RAM (quota e ~5GB) și cere icu-libs extra în imaginea .NET Alpine. Recomandare fermă: migrați la Postgres - 3 modificări:

  1. csproj: Microsoft.EntityFrameworkCore.SqlServer -> Npgsql.EntityFrameworkCore.PostgreSQL
  2. Program.cs: options.UseSqlServer(cs) -> options.UseNpgsql(cs)
  3. Program.cs (sus): AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

Datele din DB-ul local NU se transferă automat; cel mai simplu re-seed la deploy (EnsureCreated()/migrări + câteva POST-uri din Swagger). Dacă chiar rămâneți pe MSSQL, adăugați în Dockerfile după FROM aspnet:8.0-alpine:

RUN apk add --no-cache icu-data-full icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

In-memory (EF Core InMemory - date volatile)

Provider-ul Microsoft.EntityFrameworkCore.InMemory ține datele în RAM: dispar la fiecare restart al containerului. Bun pentru un demo rapid, nu pentru ceva ce vrei să rămână.

  • Ștergeți serviciul db + depends_on: db din api (nu există container de bază de date).
  • Program.cs: options.UseInMemoryDatabase("app") în loc de UseNpgsql / UseSqlServer.
  • Fără connection string, fără volum, fără Adminer (nu există DB de inspectat).
  • Seed la pornire (EnsureCreated() + câteva înregistrări demo) ca să ai ce arăta.

Pentru prezentare e ok, dar spune explicit că datele sunt volatile. Dacă vrei persistență cu efort minim, SQLite (un fișier pe un volum) e cel mai simplu upgrade.

Pas 3 - Dockerfile backend (în ./backend)

Câte un Dockerfile per serviciu, în folderul lui. Context-ul de build din compose (context: ./backend) înseamnă că COPY e relativ la acel folder.

Mai jos apare doar Dockerfile-ul limbajului ales în bara de sus.

Backend .NET 8 (ca în NewsPortal)

# Stage build - SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore <Proiect>.csproj
COPY . .
RUN dotnet publish <Proiect>.csproj -c Release -o /out --no-restore /p:UseAppHost=false

# Stage runtime - doar ASP.NET, fara SDK (~110MB)
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=build /out .
RUN (addgroup -S app 2>/dev/null || true) \
 && (adduser -S -G app app 2>/dev/null || true) \
 && chown -R app:app /app
USER app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENTRYPOINT ["dotnet", "<AssemblyName>.dll"]

<Proiect>.csproj = numele fișierului csproj (în NewsPortal: Lab12.csproj). <AssemblyName>.dll = numele assembly-ului produs, care poate diferi de csproj (în NewsPortal: NewsPortal.dll). Dacă nu știți assembly name-ul: dotnet publish îl afișează, sau verificați <AssemblyName> în csproj (default = numele csproj).

Backend Node.js / Express

FROM node:20-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

FROM node:20-alpine
WORKDIR /app
COPY --from=build /src .
RUN addgroup -S app && adduser -S -G app app && chown -R app:app /app
USER app
EXPOSE 8080
ENV NODE_ENV=production PORT=8080
CMD ["node", "server.js"]

environment: în compose: variabilele voastre (DATABASE_URL, JWT_SECRET, …). Asigurați-vă că serverul ascultă pe 0.0.0.0:8080.

Backend Python / FastAPI

FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN addgroup -S app && adduser -S -G app app && chown -R app:app /app
USER app
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Backend Java / Spring Boot

FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/target/*.jar app.jar
RUN addgroup -S app && adduser -S -G app app && chown -R app:app /app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Spring: server.port=8080 + server.address=0.0.0.0 în application.properties.

Pas 4 - Dockerfile frontend (SPA separat, în ./news-portal-app sau ./client)

SPA-ul se build-uiește cu Node, apoi se servește static cu nginx. Aceeași rețetă pentru Angular / React / Vue - diferă doar folderul de output.

# Build SPA
FROM node:20-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build -- --configuration production   # Angular; React/Vue: doar `npm run build`

# Serve static cu nginx
FROM nginx:1.27-alpine
# Angular 21: dist/<numele-proiectului>/browser/   (in NewsPortal: dist/news-portal-app/browser/)
# React (CRA): build/      | Vite (React/Vue): dist/
COPY --from=build /src/dist/<numele-proiectului>/browser/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

nginx.conf (lângă Dockerfile) - SPA fallback ca rutele client-side să nu dea 404:

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    location / { try_files $uri $uri/ /index.html; }

    location ~* \.(js|css|woff|woff2|svg|png|jpg|jpeg|gif|ico)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Acest nginx rulează în containerul frontend și servește doar fișierele statice + fallback. Routing-ul /api vs / între containere îl face nginx-ul cursului, nu acesta.

Pas 4 - Fără frontend separat (backend servește static)

Dacă backend-ul servește SPA-ul ca fișiere statice (.NET app.UseStaticFiles() peste wwwroot/, Express express.static('dist')), nu aveți serviciu frontend - build-uiți SPA-ul și copiați output-ul în wwwroot/ înainte de docker build. Omiteți complet serviciul frontend din docker-compose.yml.

La fel pentru un proiect doar Razor / MVC (fără SPA): paginile sunt randate de backend, nu există container frontend. nginx-ul cursului trimite atunci tot traficul către api:8080.

Compose complet - PostgreSQL + SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: postgres:16-alpine
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      POSTGRES_USER:     ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      POSTGRES_DB:       ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Host=student-${USER:-CHANGE_ME}-db;Port=5432;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};Username=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - PostgreSQL + fara SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: postgres:16-alpine
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      POSTGRES_USER:     ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      POSTGRES_DB:       ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Host=student-${USER:-CHANGE_ME}-db;Port=5432;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};Username=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - MySQL / MariaDB + SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mysql:8
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      MYSQL_DATABASE:      ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_USER:          ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_PASSWORD:      ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/var/lib/mysql]
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Server=student-${USER:-CHANGE_ME}-db;Port=3306;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};User=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026};"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - MySQL / MariaDB + fara SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mysql:8
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      MYSQL_DATABASE:      ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_USER:          ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MYSQL_PASSWORD:      ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/var/lib/mysql]
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Server=student-${USER:-CHANGE_ME}-db;Port=3306;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};User=${DB_USER:-${SUBDOMAIN:-${USER:-app}}};Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026};"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - MongoDB + SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mongo:7
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/data/db]
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "mongodb://${DB_USER:-app}:${DB_PASSWORD:-demo-pass-2026}@student-${USER:-CHANGE_ME}-db:27017/${DB_NAME:-app}?authSource=admin"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - MongoDB + fara SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mongo:7
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${DB_USER:-${SUBDOMAIN:-${USER:-app}}}
      MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
    volumes: [db_data:/data/db]
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "mongodb://${DB_USER:-app}:${DB_PASSWORD:-demo-pass-2026}@student-${USER:-CHANGE_ME}-db:27017/${DB_NAME:-app}?authSource=admin"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - SQLite + SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    environment:
      ConnectionStrings__DefaultConnection: "Data Source=/app/data/app.sqlite"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    volumes:
      - db_data:/app/data
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - SQLite + fara SPA separat

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    environment:
      ConnectionStrings__DefaultConnection: "Data Source=/app/data/app.sqlite"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    volumes:
      - db_data:/app/data
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - In-memory + SPA separat

Atentie: datele dispar la fiecare restart. Bun pentru demo, nu pentru ceva persistent.

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    environment:
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - In-memory + fara SPA separat

Atentie: datele dispar la fiecare restart. Bun pentru demo, nu pentru ceva persistent.

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    environment:
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - SQL Server + SPA separat

Atentie: MSSQL ocupa ~1.5GB RAM (cota e ~5GB). Recomandam ferm migrarea la PostgreSQL.

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      ACCEPT_EULA: Y
      SA_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      MSSQL_DB: ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
    volumes: [db_data:/var/opt/mssql]
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P ${DB_PASSWORD:-demo} -C -Q 'SELECT 1'"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Server=student-${USER:-CHANGE_ME}-db,1433;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};User Id=sa;Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026};TrustServerCertificate=True;"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

  frontend:
    build:
      context: ./news-portal-app    # ex: ./client, ./frontend
    container_name: student-${USER:-CHANGE_ME}-frontend
    depends_on: [api]
    expose: ["80"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Compose complet - SQL Server + fara SPA separat

Atentie: MSSQL ocupa ~1.5GB RAM (cota e ~5GB). Recomandam ferm migrarea la PostgreSQL.

Copiati direct in docker-compose.yml la radacina repo-ului.

name: student-${USER:-CHANGE_ME}

services:
  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: student-${USER:-CHANGE_ME}-db
    environment:
      ACCEPT_EULA: Y
      SA_PASSWORD: ${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026}
      MSSQL_DB: ${DB_NAME:-${SUBDOMAIN:-${USER:-app}}}
    volumes: [db_data:/var/opt/mssql]
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P ${DB_PASSWORD:-demo} -C -Q 'SELECT 1'"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [studlab_net]
    restart: unless-stopped

  api:
    build:
      context: ./backend
    container_name: student-${USER:-CHANGE_ME}-api
    depends_on:
      db:
        condition: service_healthy
    environment:
      ConnectionStrings__DefaultConnection: "Server=student-${USER:-CHANGE_ME}-db,1433;Database=${DB_NAME:-${SUBDOMAIN:-${USER:-app}}};User Id=sa;Password=${DB_PASSWORD:-${SUBDOMAIN:-${USER:-app}}-demo-pass-2026};TrustServerCertificate=True;"
      Jwt__Key: ${JWT_KEY:-${SUBDOMAIN:-${USER:-app}}-jwt-key-2026-not-for-prod-min-32-chars-pls-replace}
      ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
    expose: ["8080"]
    networks: [studlab_net]
    restart: unless-stopped

volumes:
  db_data:

networks:
  studlab_net:
    external: true
    name: studlab_net

Pas 5 - .env (optional) + credențiale

.env e complet optional. Fără el, default-urile din compose derivă din $SUBDOMAIN (setat în ~/.bashrc la provisioning, fallback $USER):

Var Default
Containere student-$USER-{api,db,frontend}
DB_USER, DB_NAME $SUBDOMAIN
DB_PASSWORD $SUBDOMAIN-demo-pass-2026
JWT_KEY $SUBDOMAIN-jwt-key-2026-...

Pentru parole reale (mai sigur + scor mai bun la temă):

cp .env.example .env
nano .env        # DB_PASSWORD=$(openssl rand -base64 24), JWT_KEY=$(openssl rand -base64 48)
chmod 600 .env   # alți studenți pe server NU pot citi parolele voastre

.env e gitignored - niciodată în repo. .env.example rămâne committed cu placeholder-uri.

Ce credențiale am efectiv? Dacă aveți credentials.sh în repo (ca în NewsPortal):

./credentials.sh    # printeaza Server / Username / Password / Database pt Adminer + URL public

Pas 6 - Build + run

cd ~/apps/<proiect>
docker compose up -d --build         # pe studlab: db + api + frontend

Prima rulare: 3-5 min (pull imagini de bază + build cod). Rebuild cu cache: ~30 sec.

Local pe laptop (include proxy-ul nginx cu cert self-signed):

docker compose --profile local up -d --build
# https://localhost  (acceptati warning-ul self-signed)

Pas 7 - Verify + Adminer

docker compose ps                    # toate Up; db (healthy)
docker compose logs api --tail=50    # app porneste, migrari aplicate

Browser: https://<subdomeniul-vostru>.student-dev.ro - aplicația voastră cu HTTPS valid Let’s Encrypt.

Adminer (https://db.student-dev.ro) - pentru screenshot-ul de la temă:

  • System: tipul DB-ului vostru (PostgreSQL / MySQL / …)
  • Server: numele containerului db (ex student-<username>-db)
  • Username / Password / Database: din ./credentials.sh (sau din .env)

Update workflow (la fiecare modificare)

# Codul pe server: git pull (varianta A) SAU re-rsync (varianta B din Pas 1)
ssh <username>@student-dev.ro "cd ~/apps/<proiect> && git pull && docker compose up -d --build"

Probleme comune

502 Bad Gateway

nginx-ul cursului nu ajunge la containerele voastre. Două cauze, în ordinea frecvenței:

1. Numele containerelor nu sunt student-<username>-api / student-<username>-frontend. nginx rutează după username (student-<username>), nu după subdomeniu. Dacă username-ul vostru diferă de subdomeniu, NU numiți containerele după subdomeniu - rămâne student-${USER}-... (sau username-ul vostru literal).

docker ps --format '{{.Names}}'   # trebuie sa apara student-<username>-api si student-<username>-frontend

Dacă vedeți alt nume (ex numele proiectului sau subdomeniul), corectați container_name: în docker-compose.yml și docker compose up -d --build.

2. Containerul api nu pornește sau nu ascultă unde trebuie.

  • docker compose logs api --tail=50
  • Backend ascultă pe 8080 pe 0.0.0.0 (.NET: ASPNETCORE_URLS=http://+:8080)
  • În compose: expose: ["8080"], nu ports:

Cannot connect to db

  • Server în connection string = numele containerului db (ex student-<username>-db), nu localhost/127.0.0.1
  • docker compose exec api ping student-<username>-db - se vede?
  • db e condition: service_healthy și are healthcheck?

Port already in use

Aveți ports: undeva. Pe studlab folosiți doar expose: - ports: pe 80/443 intră în conflict cu nginx-ul cursului.

Disk quota exceeded (~5GB/cont)

docker system prune -af       # sterge imagini/layere nefolosite
du -sh ~/apps/*

EF Core / migrări la startup

  • relation "..." does not exist - DB gol; adăugați db.Database.Migrate() (sau EnsureCreated()) în Program.cs
  • Cannot write DateTime with Kind=Unspecified (Postgres) - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

Cert TLS warning

  • Subdomeniul coincide cu cel din email?
  • Așteptați 1-2 min (cert în emitere/renewal)
  • Persistă? Scrieți pe Teams DAW

Reguli pentru serverul partajat

  • NU --privileged, NU mount la /var/run/docker.sock - înseamnă root pe host
  • NU scan-uri de rețea / port forwarding spre alți studenți sau IP-uri externe
  • Doar expose:, niciodată ports: pe 80/443
  • Parole production niciodată în cod/repo - doar în .env (gitignored)
  • Quota disk ~5GB - docker system prune periodic

Vedeți ceva ciudat (alți studenți, comportament neașteptat)? Teams DAW. NU “fixați” voi.

Linkuri