# LXC 103 - Dokploy + Traefik (Control Plane Public) ## Informații Generale - **CTID:** 103 - **IP:** 10.0.20.167 - **Rol:** Dokploy Control Plane + Traefik routing public - **Host Proxmox:** pvemini (10.0.20.201) - **Status:** Running (onboot: enabled) --- ## Arhitectura LXC 103 este nodul central pentru deployment-ul aplicațiilor publice ROMFAST. ``` Internet → 188.26.14.103 → VM 201 IIS (SSL termination) │ *.roa.romfast.ro, roa-qr, dokploy │ LXC 103 :443 (Traefik) ├── dokploy.romfast.ro → Dokploy UI :3000 ├── roa-qr.romfast.ro → pdf-qr-app container ├── space.roa.romfast.ro → space-booking container ├── app1.roa.romfast.ro → app1 container └── app2.roa.romfast.ro → app2 container ``` ### De ce app-urile publice stau pe LXC 103 Dokploy instalează Traefik SEPARAT pe fiecare server. Traefik LXC 103 și Traefik LXC 100 nu comunică între ele. Wildcadul `*.roa.romfast.ro` poate fi conectat doar la UN singur Traefik — LXC 103. **LXC 100** se folosește pentru: - Backend-uri consumate intern (fără DNS public) - Job-uri cron, workers, servicii administrative --- ## Servicii Instalate | Serviciu | Port | Descriere | |----------|------|-----------| | Dokploy UI | 3000 | Management deployment, CI/CD | | Traefik | 443 | Routing HTTPS pentru toate app-urile publice | | Traefik Dashboard | 8080 | Monitoring routelor (intern) | --- ## Domenii Gestionate | Domeniu | Destinație | |---------|-----------| | `dokploy.romfast.ro` | Dokploy UI (port 3000) | | `roa-qr.romfast.ro` | pdf-qr-app container | | `space.roa.romfast.ro` | space-booking container | | `*.roa.romfast.ro` | Orice app deployată prin Dokploy | --- ## Setup Inițial ### Pasul 1 — Oprire nginx existent pe LXC 100 > **Executat pe LXC 100** (10.0.20.170) via Portainer terminal sau Proxmox console: ```bash docker stop docker-nginx-1 docker rm docker-nginx-1 # Verifică porturile sunt libere ss -tlnp | grep -E ':80|:443' ``` ### Pasul 2 — Generare SSH Key în Dokploy + Adăugare LXC 100 **În Dokploy UI** (https://dokploy.romfast.ro): 1. Settings → SSH Keys → **Create SSH Key** 2. Copiază public key-ul generat **Pe LXC 100** (10.0.20.170): ```bash echo "ssh-ed25519 AAAA...[cheia copiată din Dokploy]" >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys ``` **În Dokploy UI:** 1. Servers → **Add Server** 2. IP: `10.0.20.170`, User: `root` 3. **Test Connection** → **Setup Server** 4. Verificare: pe LXC 100, `docker ps` → trebuie container Traefik ### Pasul 3 — Deploy pdf-qr-app pe LXC 103 **În Dokploy UI:** 1. Services → **Create Service** → Docker Compose 2. **Server:** LXC 103 (local) 3. Docker Compose: conținutul app-ului pdf-qr (vezi `docs/pdf-qr-app.md`) 4. Domain: `roa-qr.romfast.ro` 5. **Deploy** --- ## Tipuri de Servicii în Dokploy — Când să Alegi Ce Dokploy v0.28.2 oferă trei tipuri de servicii. Alegerea corectă evită probleme de rețea și simplifică mentenanța. ### Comparație rapidă | Criteriu | Application | Compose | Database | |----------|-------------|---------|----------| | Nr. containere | 1 | 2+ | 1 (managed) | | Comunicare inter-servicii | ✗ | ✓ | — | | Gestionat de Swarm | ✓ | ✗ | ✓ | | Race condition overlay network | nu | **da** (mitigat) | nu | | Traefik routing | `.yml` static | Docker labels | — | | Build din Git | ✓ | ✓ | — | | Build automat fără Dockerfile | ✓ (Nixpacks) | ✗ | — | --- ### Tip 1: Application — un singur container **Când îl alegi:** - Aplicație simplă: un singur proces (API, frontend static, bot, worker) - Nu are nevoie să vorbească cu alt container din același proiect - Exemple pe LXC 103: `roa-qr`, `qr-generator`, `icon-generator` **Avantaje:** Gestionat nativ de Swarm — nu are race condition la restart. Traefik primește config static (`.yml` în `/etc/dokploy/traefik/dynamic/`), persistent indiferent de starea containerului. **Pași în Dokploy UI:** ``` 1. Projects → [proiect] → Add Service → Application 2. Tab General: - Name: numeapp - Provider: GitHub / GitLab / Gitea / URL public - Repository + Branch 3. Tab Build: - Build Type: Dockerfile → dacă repo-ul are Dockerfile (recomandat pentru control) Nixpacks → dacă nu ai Dockerfile, Dokploy detectează limbajul automat Docker Image → dacă folosești o imagine deja construită (ex: nginx:alpine) 4. Tab Domains → Add Domain: - Host: numeapp.roa.romfast.ro - Container Port: portul pe care ascultă aplicația (ex: 80, 3000, 8000) - HTTPS: OFF (SSL e pe IIS VM 201) - Certificate: None 5. Tab Environment → adaugă variabile de mediu dacă e nevoie 6. → Deploy ``` --- ### Tip 2: Compose — mai multe containere **Când îl alegi:** - Aplicația are frontend + backend care comunică intern (ex: nginx face `proxy_pass http://backend:8000`) - Ai nevoie de o rețea bridge shared între servicii - Exemple pe LXC 103: `space-booking` (frontend nginx + backend Python) **Atenție:** Docker Compose pe Swarm are race condition la restart Docker (bug Dokploy #2033, docker/compose #12862). Mitigat pe acest server prin `dokploy-compose-heal.service` — **fără intervenție manuală la restart**. **Cerințe obligatorii în `docker-compose.yml`:** ```yaml services: frontend: # ... restart: unless-stopped # obligatoriu pe toate serviciile networks: - internal - dokploy-network # obligatoriu pe serviciul cu Traefik labels: - traefik.enable=true # ... (labels Traefik, populate automat de Dokploy la Add Domain) backend: # ... restart: unless-stopped networks: - internal # suficient dacă nu e accesat direct din exterior networks: internal: driver: bridge dokploy-network: external: true # rețeaua Swarm gestionată de Dokploy ``` **Pași în Dokploy UI:** ``` 1. Projects → [proiect] → Add Service → Compose 2. Tab General: - Name: numeapp - Provider: GitHub / GitLab / Gitea / URL public - Repository + Branch - Compose File Path: docker-compose.yml (sau subpath dacă e în subdirector) 3. Tab Domains → Add Domain: ← OBLIGATORIU prin UI, nu doar în labels! - Service Name: alege serviciul care are portul 80 (ex: frontend) - Host: numeapp.roa.romfast.ro - Container Port: 80 - HTTPS: OFF - Certificate: None → Dokploy generează .yml static în Traefik dynamic config 4. Tab Environment → variabile de mediu 5. → Deploy ``` > **Important:** Dacă adaugi domeniul NUMAI în labels din docker-compose.yml > (fără să-l înregistrezi în UI), Traefik pierde ruta când containerul e oprit. > Înregistrarea prin UI generează config static persistent. --- ### Tip 3: Database — baze de date gestionate **Când îl alegi:** - Ai nevoie de PostgreSQL, MySQL, MongoDB, Redis, MariaDB - Preferi ca Dokploy să gestioneze backupurile și volumele - Alternativa: include DB direct în docker-compose.yml al aplicației **Pași:** ``` 1. Projects → [proiect] → Add Service → Database 2. Alege tipul (PostgreSQL / MySQL / Redis etc.) 3. Configurează credențialele și versiunea 4. → Create ``` Conexiunea din aplicație folosește hostname-ul serviciului pe `dokploy-network`. --- ## Workflow: Modificare Cod + Redeploy Toate serviciile sunt legate la Git. Fluxul pentru orice modificare: ``` 1. Modifici fișierele local (docker-compose.yml, Dockerfile, cod sursă etc.) 2. git commit + git push → repo-ul sursă (Gitea / GitHub) 3. Dokploy preia modificările și face redeploy ``` **Pasul 3 are două variante:** ### Varianta A — Redeploy manual Intri în Dokploy UI → proiect → serviciu → tab **Deployments** → buton **Deploy**. Dokploy face `git pull` + rebuild + restart. Necesar dacă nu e configurat webhook. ### Varianta B — Auto-deploy prin webhook (recomandat) La fiecare `git push`, Gitea/GitHub notifică automat Dokploy → redeploy fără intervenție. **Configurare o singură dată per serviciu:** ``` Dokploy UI → proiect → serviciu → tab General → Auto Deploy → ON → copiază Webhook URL afișat Gitea (gitea.romfast.ro): repo → Settings → Webhooks → Add Webhook Payload URL: [webhook-ul din Dokploy] Content Type: application/json Trigger: Push events → Save GitHub: repo → Settings → Webhooks → Add webhook Payload URL: [webhook-ul din Dokploy] Content Type: application/json Events: Just the push event → Add webhook ``` **Repo-uri curente și sursa lor:** | Serviciu | Repo | Branch | |----------|------|--------| | space-booking (Compose) | `gitea.romfast.ro/romfast/space-booking` | master | | icon-generator (Application) | `github.com/Romfast/icon-generator` | main | | qr-pdfqrapp (Application) | `gitea.romfast.ro` (detalii în Dokploy UI) | — | | qr-qrgenerator (Application) | `gitea.romfast.ro` (detalii în Dokploy UI) | — | --- ## Workflow: Adăugare App Nouă (Rezumat) ``` 1. Alege tipul: - Un container, fără comunicare internă → Application - Frontend + backend care comunică intern → Compose - Bază de date → Database 2. Dokploy UI → Projects → [proiect] → Add Service → [tipul ales] 3. DNS: numeapp.roa.romfast.ro → acoperit automat de wildcard *.roa.romfast.ro (nu trebuie A record nou dacă e sub *.roa.romfast.ro) 4. SSL: certificatul wildcard de pe VM 201 acoperă automat subdomeniile noi (Win-ACME — necesită reînnoire manuală doar dacă adaugi domenii non-wildcard) 5. Domeniu: adaugă ÎNTOTDEAUNA prin tab Domains în Dokploy UI → nu doar în docker-compose.yml labels 6. Deploy → verificare: curl -I https://numeapp.roa.romfast.ro/ ``` --- ## Verificare ```bash # Traefik funcționează (din LAN) curl -I https://10.0.20.167/ # Dokploy UI accesibil curl -I https://dokploy.romfast.ro/ # pdf-qr-app accesibil curl -I https://roa-qr.romfast.ro/ # Test wildcard (după deploy app cu hostname) curl -I https://app1.roa.romfast.ro/ # LXC 100 Portainer funcționează în continuare curl -sk https://10.0.20.170:9443/api/status ``` --- ## Configurare Traefik (gestionat automat de Dokploy) Traefik pe LXC 103 este configurat și actualizat automat de Dokploy la fiecare deploy. Nu modificați manual configurația Traefik fără să înțelegeți impactul. ```bash # Status containere pe LXC 103 docker ps # Logs Traefik docker logs traefik -f # Verificare routere Traefik curl http://localhost:8080/api/http/routers | jq . ``` --- ## Fix: Docker Swarm VIP DNS (Bug Permanent Rezolvat) ### Problema Docker Swarm folosește implicit **VIP (Virtual IP)** pentru load balancing intern. IPVS (mecanismul kernel care implementează VIP) **nu funcționează în LXC containers**. Când Traefik încearcă să rezolve `http://qr-qrgenerator-vqkwsu:80`, primea VIP-ul serviciului (ex: 10.0.1.8) în loc de IP-ul real al task-ului (ex: 10.0.1.12) → **502 Bad Gateway**. ### Soluția Schimbăm endpoint mode-ul serviciilor Swarm din `vip` → `dnsrr` (DNS Round Robin). Cu `dnsrr`, DNS-ul rezolvă direct la IP-ul real al containerului, bypassing IPVS. ### Implementare: `dokploy-dnsrr-fix` (systemd service) Un listener permanent care prinde orice serviciu Swarm nou creat/actualizat de Dokploy și îl setează automat la `dnsrr`: ```bash # Script: /usr/local/bin/dokploy-dnsrr-fix.sh # Service: /etc/systemd/system/dokploy-dnsrr-fix.service ``` ```bash # Verificare status systemctl status dokploy-dnsrr-fix # Logs (confirmare că fixul s-a aplicat la ultimul deploy) journalctl -u dokploy-dnsrr-fix -n 20 ``` Output normal după un deploy: ``` Setting dnsrr for qr-qrgenerator-vqkwsu (was: vip) OK - qr-qrgenerator-vqkwsu now uses dnsrr ``` ### Timeline la fiecare redeploy ``` 0s Dokploy creează/actualizează serviciul Swarm (vip mode) → Traefik: 502 Bad Gateway 1s Listener prinde evenimentul update 3s docker service update --endpoint-mode dnsrr 13s Service converge, DNS rezolvă corect → Traefik: 200 OK ``` **Downtime la redeploy: ~13 secunde** (inevitabil cu Swarm în LXC). ### Dacă fix-ul nu funcționează (manual recovery) ```bash # 1. Identifică serviciul cu problemă docker service ls --format '{{.Name}} {{.Mode}}' # 2. Setează manual dnsrr docker service update --endpoint-mode dnsrr # 3. Repornește listener dacă e oprit systemctl restart dokploy-dnsrr-fix ``` --- ## Fix: Docker Compose — Overlay Network Race Condition la Restart (Bug Permanent Rezolvat) ### Problema Proiectele **Docker Compose** din Dokploy (ex: space-booking) care folosesc `dokploy-network` ca rețea externă (`external: true`) **pică la restart Docker**. Eroarea din `docker inspect `: ``` failed to set up container networking: could not find a network matching network mode dokploy-network: network dokploy-network not found ``` Container-ul iese cu **exit code 128** și nu mai pornește singur. ### Cauza Race condition confirmat în: - **Dokploy** issues #2033, #1802 — fără fix livrat - **Docker Compose** upstream issue #12862 — nerezolvat (aprilie 2026) La restart Docker daemon: 1. Docker Swarm inițializează rețelele overlay (`dokploy-network`) — durează câteva secunde 2. Docker Compose containers cu `restart: unless-stopped` pornesc **imediat** 3. `dokploy-network` nu e încă disponibilă → exit 128 Serviciile **Swarm** (Application type în Dokploy) nu au această problemă — orchestratorul știe să aștepte rețelele înainte de a programa task-urile. ### De ce Docker Compose în loc de Application (Swarm) Proiectele multi-serviciu (ex: frontend + backend) rămân Compose deoarece serviciile comunică intern între ele (ex: nginx `proxy_pass http://backend:8000` pe o rețea bridge shared). Conversia la Swarm Application ar necesita modificări în codul aplicației. ### Soluția Script generic care rulează la boot, așteaptă `dokploy-network` și repornește **orice** container Compose eșuat din cauza lipsei rețelei: ```bash # Script: /usr/local/bin/dokploy-compose-heal.sh # Service: /etc/systemd/system/dokploy-compose-heal.service (enabled) ``` Logica scriptului: - Polling `dokploy-network` disponibil (max 90s, interval 3s) - Caută containere cu `exit code 128` + eroare "network" + label `com.docker.compose.project` - Le repornește — **nu atinge** containerele oprite intenționat (exit 0) ```bash # Verificare status systemctl status dokploy-compose-heal # Logs la ultimul boot journalctl -u dokploy-compose-heal -n 30 # Rulare manuală (după un docker restart neașteptat) systemctl start dokploy-compose-heal ``` ### Fix aditional: DNS Docker daemon Docker containers foloseau implicit DNS-ul Tailscale (`100.100.100.100`) care **nu e accesibil** din interiorul containerelor — Dokploy primea `ESERVFAIL` la validarea domeniilor. Fix aplicat în `/etc/docker/daemon.json`: ```json {"dns": ["8.8.4.4", "8.8.8.8"]} ``` ### Pentru orice proiect Compose nou în Dokploy 1. Adaugă domeniul în **Dokploy UI → Domains → Add Domain** (nu doar în labels din docker-compose.yml) — Dokploy generează `.yml` static în Traefik dynamic config, persistent la restart 2. Dacă containerul pică după restart Docker → `systemctl start dokploy-compose-heal` rezolvă automat --- ## Documentație Asociată - **Arhitectură completă IIS:** `../vm201-windows/docs/vm201-dokploy-infrastructure.md` - **Setup IIS VM 201:** `../vm201-windows/scripts/setup-new-iis-sites.ps1` - **Web.config IIS proxy:** `../vm201-windows/iis-configs/` - **LXC 100 (Remote Node):** `../lxc100-portainer/README.md` *(de creat)* - **space-booking deploy guide:** `docs/space-booking-app.md` - **pdf-qr-app deploy guide:** `docs/pdf-qr-app.md` --- **Ultima actualizare:** 2026-04-28 **Autor:** Marius Mutu **Proiect:** ROMFASTSQL - LXC 103 Dokploy