NewsPortal Deploy - Docker, nginx, VPS

Partea 5 din 7

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

Parte 4 - VPS și deploy

În sfârșit ce contează: cum ajunge codul de pe laptop pe un server, accesibil din toată lumea, cu HTTPS valid.

Ce e un VPS

VPS = Virtual Private Server. O mașină virtuală care rulează pe un server fizic al unui provider, dar pe care aveți control complet (root SSH, instalați ce vreți, alegeți OS-ul). Diferit de:

Tip Control Preț Când alegeți
Shared hosting aproape niciun control (panou cPanel) foarte ieftin site static / WordPress
VPS root, OS la alegere ~5-15 EUR/luna proiecte mici-medii, deploy-uri custom
Dedicated server hardware fizic propriu ~50-200 EUR/luna aplicații cu cerințe resource mari
Cloud (AWS, GCP, Azure) instanțe elastice + N servicii pay-per-use, scalabil producție cu trafic variabil

Pentru proiectul de DAW, VPS-ul e fix ce vă trebuie.

Cum alegeți un VPS

Provideri uzuali cu raport preț/performanță decent:

Provider Note
Hetzner DE-based, billing orar, plan-uri x86 și ARM (CAX = ARM), API + UI bune
Netcup DE-based, ieftin, contract minim 1 lună, mai mult disk per euro
Contabo Mai mult disk și RAM la preț mic, dar CPU oversubscribed (bursty)
OVH / Kimsufi FR-based, plan-uri entry-level OK
DigitalOcean / Vultr / Linode US/UE, plan-uri standard, UI prietenoasă pentru beginner
Romarg RO-based, scump față de Hetzner/Netcup la spec similar, dar suport în română + datacenter în țară

Locații relevante (latency din România):

  • Frankfurt / Falkenstein / Nurnberg (DE) - 30-40ms
  • Vienna (AT), Helsinki (FI) - 25-35ms
  • București (RO) - 5-10ms (dar oferte mai limitate / mai scumpe)

Specs suficiente pentru un proiect tip .NET API + DB + frontend static:

  • 1-2 vCPU, 2 GB RAM, 20-40 GB SSD - perfect. Docker, Postgres și .NET runtime stau confortabil aici.
  • 4 GB RAM + - dacă ai mai multe servicii sau procese de build mai grele pe același server.

Containerele sunt ușoare: un .NET runtime alpine + Postgres mic + un nginx ocupă împreună ~300-400 MB RAM în regim normal. Lab-ul vostru rulează fără probleme pe un entry-level de 5 EUR/luna.

Trafic: 1-20 TB/lună inclus la majoritatea providerilor - mult mai mult decât vă trebuie.

Setup bază Ubuntu

După ce comandați VPS-ul, primiți:

  • IP public (în lab-ul nostru 178.105.43.73, VPS-B Hetzner pentru student-dev.ro)
  • root SSH (cu parolă sau key)

Primii pași:

1. SSH key only, no password

# Pe local (laptop), generati o cheie SSH daca nu aveti deja
ssh-keygen -t ed25519 -C "your-email@example.com"
# Output: ~/.ssh/id_ed25519 (privata) si id_ed25519.pub (publica)

# Copiati publica pe server
ssh-copy-id root@178.105.43.73    # sau editati manual ~/.ssh/authorized_keys

# Login fara parola
ssh root@178.105.43.73

Pe server, edit /etc/ssh/sshd_config:

PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes

sudo systemctl reload sshd. Acum nu mai poate nimeni intra cu parolă - doar cu cheie privată.

2. User non-root

adduser admin                              # creeaza user, seteaza parola
usermod -aG sudo admin                     # poate rula sudo
mkdir -p /home/admin/.ssh
cp ~/.ssh/authorized_keys /home/admin/.ssh/
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys

Login ca admin@178.105.43.73 de acum (sau, după configurarea DNS-ului, admin@student-dev.ro). Niciodată nu lucrați ca root direct - cu sudo doar când aveți nevoie.

3. UFW firewall

sudo apt update && sudo apt install -y ufw
sudo ufw default deny incoming      # blocheaza tot inbound
sudo ufw default allow outgoing     # permite outbound (altfel apt fail-uieste)
sudo ufw allow 22/tcp               # SSH
sudo ufw allow 80/tcp               # HTTP (pentru certbot HTTP-01 + redirect)
sudo ufw allow 443/tcp              # HTTPS
sudo ufw enable
sudo ufw status verbose

4. fail2ban (anti brute-force)

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

Default config protejează SSH. Dacă cineva încearcă să se logheze de 5 ori cu parolă greșită (chiar dacă e dezactivată, scan-urile încearcă), IP-ul e banat 1 oră.

5. Docker

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker admin       # admin poate rula docker fara sudo
# Reconnect SSH ca sa intre in vigoare
exit && ssh admin@178.105.43.73
docker --version

6. Updates automate (security patches)

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Cumpărat domeniu și configurat DNS

  1. Mergeți pe registrar (Spaceship/Cloudflare/Namecheap/Romarg), cumpărați domeniul vostru (myapp.dev, prenume-nume.ro, etc., ~5-12 EUR/an). În lab-ul nostru domeniul cursului e student-dev.ro la Romarg.
  2. În panoul de DNS, configurați:
A    student-dev.ro      178.105.43.73
A    *.student-dev.ro    178.105.43.73    (wildcard pentru subdomenii voastre)

Așteptați ~5-30 min pentru propagare. Verificați:

dig +short student-dev.ro
# 178.105.43.73

dig +short newsportal.student-dev.ro
# 178.105.43.73   (wildcard prinde orice subdomeniu)

Două căi de deploy

Path 1: SSH manual + rsync

Cea mai educațională, “feel the server”:

# Local (din folder-ul cu codul) - cu user-ul vostru de pe VPS-ul cursului (ex: ziarist)
rsync -az --delete \
  --exclude='.git' --exclude='bin' --exclude='obj' --exclude='node_modules' \
  ./ ziarist@student-dev.ro:~/apps/

# SSH si build
ssh ziarist@student-dev.ro
cd ~/apps
docker compose up -d --build

# Logs
docker compose logs -f api

Sau și mai simplu, dacă cwRsync nu vă merge pe Windows, folosiți scp:

scp -r ./backend ziarist@student-dev.ro:~/apps/
# parola la prompt (cea primita pe email)

Pro: simplu, înțelegeți exact ce se întâmplă. Contra: manual la fiecare modificare; dacă aveți 10 commit-uri, faceți 10 deploys.

Path 2: GitHub Actions

Workflow automat: git push -> GH Actions -> SSH la server -> rebuild containere.

Deploy flow: laptop -> github -> runner GH Actions (ssh + rsync + compose up) -> VPS cu UFW filtrând inbound

.github/workflows/deploy.yml:

name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch:           # permite rulare manuala din UI

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/key
          chmod 600 ~/.ssh/key
          ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

      - name: Sync code
        run: |
          rsync -az --delete \
            --exclude='.git' --exclude='.github' \
            --exclude='bin' --exclude='obj' --exclude='node_modules' \
            -e "ssh -i ~/.ssh/key" \
            ./ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/apps/

      - name: Rebuild on server
        run: |
          ssh -i ~/.ssh/key ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
            "cd ~/apps && docker compose up -d --build"

În Settings > Secrets and variables > Actions aveți nevoie de:

  • SSH_HOST = student-dev.ro (sau domeniul vostru)
  • SSH_USER = username-ul vostru (ex: ziarist)
  • SSH_PRIVATE_KEY = conținutul lui ~/.ssh/id_ed25519 (cheia privată)

Pentru VPS-ul cursului: studenții au autentificare cu parolă (Match Group students), nu cu cheie - GitHub Actions necesită o ajustare. Cel mai simplu pentru lab: rămâneți pe SSH manual + rsync (Path 1).

NICIODATĂ nu commit-ați SSH_PRIVATE_KEY în repo - doar în secrets.

Diferențe dev vs prod

Aspect Dev local Prod VPS
Cert TLS self-signed Let’s Encrypt
Image tags :latest (acceptabil) pin la versiune (:1.2.3 sau digest)
Secrets .env local env vars la runtime / vault
Logs stdout în compose log driver json + rotation
Restart manual restart: unless-stopped (deja în compose)
Healthchecks opțional obligatoriu
Backup DB nu există weekly pg_dump cu cron
Updates manual când aveți timp unattended-upgrades pe OS, dependabot pe NuGet/npm
Resource limits nu deploy.resources.limits per service

Ce am evitat în lab dar contează în producție

  • Container hardening: read_only: true, cap_drop: [ALL], security_opt: [no-new-privileges]. Mai mult în OWASP Container Security.
  • Image scanning: docker scout cves <image> sau Trivy. Vedeți câte CVE-uri are imaginea înainte de deploy.
  • Container egress filtering: blochează outbound spre porturi neautorizate (anti spam relay).
  • Niciodată --privileged, niciodată -v /var/run/docker.sock:/var/run/docker.sock (= root pe host).

În secțiunea 06 (deploy proiect) aveți pași concreti pentru deploy-ul proiectului pe VPS-ul cursului.

Demo live

În sesiunea live vedeți:

  1. VPS-ul cursului wipe + rebuild de la zero pe Hetzner (Ubuntu 24.04, doar IP + parolă inițială).
  2. Eu configurez serverul de la zero în fața voastră: SSH key auth, admin user cu sudo, dezactivare root login, UFW (firewall), fail2ban (anti brute-force), SSH hardening, swap, hostname, Docker, stack-ul studlab (nginx + certbot + Postgres + Adminer), cert-uri Let’s Encrypt HTTP-01.
  3. Cont demo ziarist (parolă ziarist, accesibil tuturor) provisionat live, cu home chmod 750, grupuri students + docker, sudo whitelist limitat la apt-get install/update/upgrade. Demonstrez ce poate și ce NU poate să facă.
  4. Deploy News Portal ca ziarist: scp sursă de pe laptop -> server, editare docker-compose.yml + .env, docker compose up -d --build.
  5. Verify live: https://newsportal.student-dev.ro/api/articles cu cert Let’s Encrypt valid + https://db.student-dev.ro (Adminer, vede DB-ul lui ziarist).
  6. Bulk provision live: scriptul provision_user.ps1 -FromFile users_new.csv creează ~30 conturi pe server în 30 secunde + scriptul send_provision_emails.py --send trimite credențialele via Resend la cei înscriși.

După demo, cei care au optat pentru deploy primesc un email cu credențialele lor. Secțiunea 06 vă ghidează prin pașii voștri concreți.