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 (exziarist), 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
ziaristcu parolaziarist(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 serviciuluiapicu cele ale framework-ului vostru. Numele containerelor +expose+networksraman 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:
csproj:Microsoft.EntityFrameworkCore.SqlServer->Npgsql.EntityFrameworkCore.PostgreSQLProgram.cs:options.UseSqlServer(cs)->options.UseNpgsql(cs)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: dbdinapi(nu există container de bază de date). Program.cs:options.UseInMemoryDatabase("app")în loc deUseNpgsql/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
/apivs/î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"], nuports:
Cannot connect to db
- Server în connection string = numele containerului db (ex
student-<username>-db), nulocalhost/127.0.0.1 docker compose exec api ping student-<username>-db- se vede?dbecondition: 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țidb.Database.Migrate()(sauEnsureCreated()) înProgram.csCannot 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 pruneperiodic
Vedeți ceva ciudat (alți studenți, comportament neașteptat)? Teams DAW. NU “fixați” voi.
Linkuri
- Lab newsportal-deploy (concepte Docker + nginx + TLS + DNS): newsportal-deploy
- Cerințe proiect 40%: proiect
- Arhivă demo: https://daw.danielwagner.ro/downloads/newsportal.zip
- Adminer DB UI: https://db.student-dev.ro
- Aplicația voastră:
https://<subdomeniul-vostru>.student-dev.ro(din email)