Indice dei contenuti
- Architettura e decisioni
- Preparare un Dockerfile
- Definire una tagging strategy
- Creare il workflow deploy.yml completo
- Deploy automatico (via SSH) o pull manuale?
- Autenticare il server al registry privato
- Preparare il server e il file .env
- Creare il docker-compose.prod.yml
- Rollout con Docker Compose
- Configurare Nginx
- Osservabilità del deploy
- Rollback e gestione immagini
- Backup, volumi e persistenza
- Esempio di deploy sul server
- Sicurezza e produzione reale
- Quando passare a Kubernetes
- Conclusioni
- Riferimenti
Non tutte le applicazioni hanno bisogno di Kubernetes, Helm, ingress controller, secret operator e relativo carico operativo. In molti casi, soprattutto per piccoli team, prodotti interni, SaaS con una singola installazione, ambienti self-hosted o architetture con uno o pochi host ben controllati, un pattern molto più semplice regge benissimo: build con GitHub Actions, pubblicazione delle immagini su GHCR, deploy su server Linux via Docker Compose dietro Nginx.
È un approccio che funziona bene quando il requisito principale è avere una pipeline chiara, ripetibile, economica da gestire e abbastanza robusta per la produzione reale, senza introdurre un orchestratore che il team non ha tempo, necessità o competenze per mantenere. Vale ancora di più quando non serve autoscaling aggressivo, non ci sono decine di microservizi indipendenti e il numero di deploy al giorno resta ragionevole.
Naturalmente ci sono anche casi in cui questo pattern inizia a stare stretto: più host attivi contemporaneamente, zero-downtime molto rigoroso, rollout avanzati, self-healing distribuito, scheduling multi-node, service discovery evoluto, gestione nativa dei secret e workload che cambiano scala con rapidità. Di questo parleremo alla fine, perché è importante sapere quando un approccio sobrio è sufficiente e quando invece conviene alzare il livello.
In questa guida vedremo l’intero percorso end-to-end: architettura, motivazioni iniziali, Dockerfile multi-stage, workflow GitHub Actions, tagging strategy, deploy con Docker Compose, rollback, hardening, osservabilità e misure di sicurezza realistiche. L’obiettivo non è solo mostrare dei file YAML, ma spiegare perché certe scelte funzionano bene in produzione e quali limiti hanno.
Architettura e decisioni
Perché GHCR
GHCR, cioè GitHub Container Registry, è spesso una scelta molto sensata se il codice vive già su GitHub e la pipeline gira su GitHub Actions. Il vantaggio principale è la coerenza dell’ecosistema: repository, workflow, pacchetti, permessi e token stanno tutti nello stesso perimetro. Per un piccolo team o per una codebase gestita interamente su GitHub, riduce attrito operativo, elimina un pezzo di integrazione e rende molto lineare il flusso build-push-deploy.
Rispetto a Docker Hub, GHCR è in genere più comodo quando si vogliono legare immagini e repository in modo stretto, evitare alcuni limiti del modello public/private di Docker Hub e sfruttare direttamente il GITHUB_TOKEN del workflow per il push. Docker Hub resta perfettamente valido, ma GHCR tende a risultare più naturale in progetti GitHub-centrici.
Rispetto ad ACR o ECR, il discorso cambia: i registry cloud nativi diventano molto interessanti quando l’infrastruttura gira già in Azure o AWS, quando si vogliono integrazioni IAM profonde, private networking, pull locali nello stesso cloud o politiche organizzative già standardizzate. Se però il deploy avviene su una VM Linux tradizionale, magari self-hosted, e il team usa GitHub per tutto il resto, GHCR spesso rappresenta il miglior compromesso tra semplicità e solidità.
In sintesi, questo pattern ha molto senso per:
- piccoli e medi team;
- server singoli o pochi host controllati;
- ambienti self-hosted o VPS dedicati;
- applicazioni .NET, Node.js o altri workload containerizzati senza necessità immediate di Kubernetes;
- organizzazioni che hanno già GitHub come centro operativo del ciclo di rilascio.
Ha meno senso, o comunque va rivalutato, quando il registry deve vivere strettamente dentro un cloud provider, quando la rete è molto segmentata, quando servono policy enterprise già standardizzate o quando il numero di ambienti, team e servizi rende preferibile una piattaforma più strutturata.
Topologia di riferimento
La topologia che useremo come riferimento è volutamente semplice:
- repository GitHub con codice applicativo e Dockerfile;
- GitHub Actions che costruisce e pubblica l’immagine su GHCR;
- un server Ubuntu con Docker Engine, Docker Compose e Nginx;
- una o più applicazioni containerizzate dietro reverse proxy TLS;
- eventuali servizi di supporto locali o esterni, come database, Redis, broker o object storage.
È la classica architettura che, se impostata bene, permette di gestire deploy ripetibili senza dover portare subito in casa tutta la complessità di un orchestratore.
Componenti minimi del flusso
Il flusso completo è il seguente: il developer fa push su main, GitHub Actions esegue build e push dell’immagine verso GHCR, il server effettua il pull del nuovo tag e Docker Compose aggiorna il servizio. Attorno a questo nucleo si aggiungono i pezzi che fanno la differenza in produzione: tag immutabili, health check, rollback, secret management, notifiche, logging e regole di sicurezza sull’accesso SSH.
Preparare un Dockerfile
Un buon deploy parte quasi sempre da un buon Dockerfile. Il multi-stage build è importante per due motivi: tiene separata la fase di compilazione dalla runtime image e riduce drasticamente la superficie finale, sia in termini di dimensioni sia di dipendenze inutili. Vale per .NET, ma anche per Node.js, Go, Java e praticamente qualsiasi stack moderno.
Ecco un esempio neutro per una web app .NET:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY . ./ RUN dotnet restore ./MyApp/MyApp.csproj RUN dotnet publish ./MyApp/MyApp.csproj -c Release -o /app/publish /p:UseAppHost=false FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app RUN adduser --disabled-password --home /app --gecos "" appuser USER appuser COPY --from=build /app/publish ./ EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1 ENTRYPOINT ["dotnet", "MyApp.dll"] |
Ci sono quattro dettagli importanti che vale la pena sottolineare:
- la build stage usa l’SDK, la runtime stage usa solo il runtime necessario;
- il container finale gira con un utente non root;
- la porta esposta è esplicita e coerente con la configurazione dell’app;
- il
HEALTHCHECKrende il container osservabile anche da Compose e dagli script di deploy.
Se non stai distribuendo una web app .NET ma, per esempio, un’app Node.js, la logica resta identica: build stage completa, runtime stage minimale, utente non privilegiato e health check credibile.
Definire una tagging strategy
Uno degli errori più comuni nei deploy containerizzati è affidarsi troppo al tag latest. Comodo in sviluppo, rischioso in produzione. Il problema non è che latest sia “sbagliato” in assoluto; il problema è che non è immutabile. Oggi punta a un’immagine, domani a un’altra. Quando qualcosa va storto, ricostruire cosa c’era davvero in esecuzione diventa molto più fragile.
In produzione conviene sempre pubblicare almeno un tag immutabile, tipicamente basato sul commit SHA. Se in più hai release versionate, puoi aggiungere anche tag semver. Un set ragionevole è questo:
latestper comodità, se proprio vuoi mantenerlo;sha-abc1234come riferimento immutabile reale del deploy;v1.4.2quando esiste una release semantica.
Il tag da usare nel deploy vero dovrebbe essere quello immutabile. È la scelta più sicura, più auditabile e molto più facile da usare nei rollback.
Creare il workflow deploy.yml completo
Qui entriamo nel cuore della pipeline. Un workflow serio deve fare almeno queste cose: checkout del codice, login al registry, generazione dei metadata, build e push dell’immagine con cache, eventuale attestazione o SBOM, e infine deploy o notifica. Di seguito trovi un esempio completo, volutamente neutro, adatto a una singola app containerizzata.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
name: Build and Deploy on: push: branches: [ "main" ] workflow_dispatch: permissions: contents: read packages: write id-token: write env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository_owner }}/myapp jobs: build: runs-on: ubuntu-latest outputs: image_tag: ${{ steps.meta.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest type=sha,prefix=sha- type=ref,event=tag flavor: | latest=true - name: Build and push image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true sbom: true deploy: runs-on: ubuntu-latest needs: build if: github.ref == 'refs/heads/main' steps: - name: Deploy over SSH uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} port: ${{ secrets.SSH_PORT }} script: | cd ${{ secrets.DEPLOY_PATH }} export IMAGE_TAG=${{ needs.build.outputs.image_tag }} docker compose pull app docker compose up -d --no-deps app docker image prune -f - name: Post-deploy health check run: | for i in 1 2 3 4 5; do curl -fsS https://example.com/health && exit 0 sleep 5 done exit 1 |
Diamo un'occhiata ai parametri principali di questo file; permissions merita particolare attenzione. contents: read basta per leggere il repository, packages: write è necessario per pubblicare l’immagine su GHCR, mentre id-token: write torna utile se la pipeline deve emettere token OIDC verso sistemi esterni. È bene partire sempre dal minimo indispensabile, non dal massimo.
docker/login-action è il modo più pulito per autenticare il runner verso il registry. In GHCR, per il push dal workflow, il GITHUB_TOKEN è quasi sempre la scelta giusta: evita di introdurre PAT inutili nella pipeline.
docker/metadata-action è preziosa perché evita di gestire a mano i tag. Può generare tag basati su SHA, branch, semver o altri eventi del repository, mantenendo coerenza tra immagine e sorgente.
docker/build-push-action è il blocco che fa il lavoro vero. La cache type=gha sfrutta lo storage interno di GitHub Actions e accelera moltissimo le build ripetute, soprattutto su immagini multi-stage. Le opzioni provenance: true e sbom: true aggiungono attestazioni e Software Bill of Materials, due elementi che iniziano a diventare molto utili anche fuori dai contesti enterprise più rigidi.
Nel job di deploy, l’uso di --no-deps è importante: evita di toccare servizi dipendenti che non devono essere riavviati. È una piccola scelta, ma in ambienti reali evita parecchi restart inutili.
Deploy automatico (via SSH) o pull manuale?
Qui conviene essere pragmatici. Il deploy automatico via SSH da GitHub Actions è comodo e veloce, ma non è sempre la scelta migliore. Se il server è protetto da IP allowlist strette, se non vuoi esporre SSH ai runner GitHub-hosted o se il team preferisce un controllo umano sul rollout, può avere più senso fermarsi al push su GHCR e completare il deploy manualmente sul server.
Il pattern automatico va bene quando:
- il server può accettare connessioni dai runner usati;
- il team vuole ridurre interventi manuali;
- il flusso di rilascio è già sufficientemente standardizzato.
Il pattern manuale o semi-manuale va meglio quando:
- l’accesso SSH è molto ristretto;
- serve validare il contesto prima del rollout;
- si vuole pubblicare l’immagine e scegliere più tardi quando attivarla.
Non esiste una risposta giusta in assoluto. Per molti ambienti piccoli o self-hosted, la soluzione più solida è: build e push automatici, deploy controllato manualmente con pochi comandi espliciti.
Autenticare il server al registry privato
Se le immagini GHCR sono private, il server deve autenticarsi prima di poter fare pull. La strada più comune è un docker login ghcr.io eseguito con un PAT che abbia almeno read:packages. Quel login produce il file ~/.docker/config.json, che Docker userà per i pull successivi.
|
1 |
docker login ghcr.io -u YOUR_GITHUB_USERNAME |
Se vuoi automatizzare questa parte, puoi eseguirla una volta con un utente dedicato al deploy. In ambienti più rigorosi, si possono usare tool come crane o soluzioni a token a scadenza, ma per molti server self-hosted un PAT limitato a read:packages, gestito bene e assegnato a un account tecnico dedicato, resta una soluzione pragmatica.
Qui la prudenza conta: evitare di fare il login come utente personale, evitare token con scope eccessivi, evitare di spargere il file config.json in home directory condivise o backup poco controllati.
Preparare il server e il file .env
Una volta pronto il server, conviene creare una directory di deploy stabile, per esempio /var/www/myapp o /opt/myapp, con dentro il file docker-compose.prod.yml, l’eventuale .env e gli asset di configurazione necessari.
Il file .env resta ancora oggi una soluzione diffusa e sensata, ma va trattato come si deve: permessi 600, owner dedicato, niente commit nel repository, niente copie in cartelle accessibili ad altri utenti, niente backup pubblici o sincronizzati in modo disinvolto.
|
1 2 |
chmod 600 .env chown deploy:deploy .env |
Se il livello di maturità dell’infrastruttura lo consente, vale la pena considerare soluzioni migliori per i segreti più sensibili: Docker secrets, SOPS, Vault o servizi equivalenti. Non sempre è necessario introdurli subito, ma almeno per credenziali critiche e ambienti più esposti conviene sapere che .env non è l’unico modello possibile.
Creare il docker-compose.prod.yml
Il file Compose di produzione non dovrebbe essere una copia meccanica di quello di sviluppo. In produzione servono almeno immagini da registry, restart policy sensate, health check coerenti e volumi dichiarati in modo esplicito.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
services: app: image: ghcr.io/myorg/myapp:${IMAGE_TAG:-latest} container_name: myapp env_file: - .env restart: unless-stopped ports: - "127.0.0.1:8080:8080" healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8080/health"] interval: 30s timeout: 5s retries: 3 start_period: 20s volumes: - app-data:/app/data volumes: app-data: |
restart: unless-stopped è una scelta semplice ma utile: il container si rialza dopo un reboot o un crash, senza costringerti a script artigianali. L’esposizione sulla loopback locale, invece che direttamente su tutte le interfacce, è una buona abitudine quando davanti c’è Nginx.
Rollout con Docker Compose
Docker Compose può fare deploy molto dignitosi, ma non è Kubernetes. Questo va detto chiaramente. Il classico:
|
1 2 |
docker compose pull app docker compose up -d --no-deps app |
funziona bene per aggiornare un servizio senza toccare i dipendenti, ma non garantisce da solo un rolling update sofisticato o uno zero-downtime assoluto. In molti casi la finestra di interruzione è minima, ma esiste: il vecchio container viene fermato e quello nuovo viene avviato.
Per applicazioni non critiche o con tolleranza a micro-interruzioni, spesso basta. Se però vuoi ridurre ulteriormente il rischio, devi aggiungere almeno health check, warm-up ragionevoli e un minimo di strategia di switching davanti, di solito tramite Nginx.
Pattern blue/green minimale con Nginx
Una forma semplice di blue/green su singolo host consiste nel mantenere due servizi, per esempio app_blue e app_green, e far puntare Nginx all’uno o all’altro tramite upstream. Si aggiorna il container inattivo, si verifica l’health check, poi si cambia l’upstream e si ricarica Nginx. Non è elegante come un orchestratore vero, ma per alcuni ambienti è sorprendentemente efficace.
È più complesso del classico compose up -d, certo, ma molto meno di un cluster Kubernetes. Anche qui conta il contesto: se il business non richiede zero-downtime rigoroso, spesso non vale la pena complicare troppo il flusso.
Configurare Nginx
Il reverse proxy non dovrebbe limitarsi a inoltrare traffico. In produzione ha senso usare Nginx anche per TLS, forwarding corretto degli header, security headers, rate limiting di base, compressione e limiti sensati sulla dimensione delle richieste.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
server { listen 443 ssl http2; server_name example.com; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; client_max_body_size 50m; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } |
client_max_body_size andrebbe motivato in base all’applicazione: se il sistema accetta upload, conviene impostarlo con cognizione, non lasciarlo implicito. Lo stesso vale per rate limiting e compressione: misure utili, ma da calibrare sul traffico reale e non copiate alla cieca.
Osservabilità del deploy
Un deploy che non lascia tracce è un deploy difficile da diagnosticare. Anche in ambienti piccoli conviene prevedere almeno tre cose: raccolta log, notifica in caso di fallimento e controllo post-deploy.
Per i log, una base molto ragionevole è usare journald o i log Docker raccolti e poi spediti, quando serve, verso Loki o altro sistema centrale. Non occorre partire da subito con una piattaforma gigantesca, ma bisogna almeno sapere dove andare a guardare quando qualcosa smette di funzionare.
Nel workflow GitHub Actions, una notifica Slack o Teams in caso di fallimento aiuta molto. Non serve essere sofisticati: basta sapere che il deploy non è andato a buon fine, e su quale commit.
Il controllo post-deploy dovrebbe essere esplicito, per esempio un curl con retry su un endpoint /health. È un passaggio semplice e molto più utile di tanti “deploy riuscito” dichiarati solo perché il comando SSH è terminato senza errori.
Rollback e gestione immagini
Il rollback è uno di quei temi che tutti danno per scontati finché non serve davvero. Se il deploy usa tag immutabili, tornare indietro è banale: basta riallineare il servizio a un tag precedente e rialzarlo.
|
1 2 3 |
export IMAGE_TAG=sha-abc1234 docker compose pull app docker compose up -d --no-deps app |
Se invece tutto ruota attorno a latest, il rollback diventa incerto: bisogna sapere quale immagine corrispondeva al “latest” precedente, e spesso questa informazione non è più così immediata. È il motivo per cui il tag immutabile non è un dettaglio estetico, ma una misura operativa molto concreta.
Backup, volumi e persistenza
Le immagini si possono sempre ricostruire. I dati no. Per questo, quando si parla di deploy, bisogna sempre chiarire dove stanno i dati persistenti e come vengono protetti. I volumi Docker sono spesso una scelta pulita per dati applicativi e database containerizzati; i bind mount possono andar bene quando serve controllo esplicito sul path host, ma vanno gestiti con maggiore disciplina.
Se il deploy include migrazioni di database, conviene prevedere uno snapshot o un backup prima del rollout, soprattutto quando lo schema cambia in modo non banale. È una precauzione che costa poco rispetto a quanto può costare un deploy riuscito “a metà”.
Esempio di deploy sul server
Con tutto il resto al suo posto, la procedura reale sul server resta sorprendentemente semplice:
|
1 2 3 4 5 |
cd /var/www/myapp export IMAGE_TAG=sha-abc1234 docker compose pull app docker compose up -d --no-deps app docker image prune -f |
Per un bootstrap iniziale o un refresh completo dello stack, si può usare un docker compose pull generale seguito da up -d senza --no-deps. Per i deploy normali, invece, vale la pena toccare solo ciò che serve davvero.
Sicurezza e produzione reale
Sicurezza della pipeline
Una pipeline CI/CD non è solo automazione: è anche una superficie d’attacco. Per questo conviene partire da tre regole sobrie ma importanti: permessi minimi nel workflow, niente credenziali long-lived se esistono alternative migliori, e tracciabilità dell’artefatto prodotto.
Dove possibile, OIDC è preferibile a chiavi statiche o segreti di lunga durata. Se il workflow deve parlare con servizi cloud compatibili, il modello a token federato temporaneo è più pulito e più sicuro. In GHCR il push può spesso vivere tranquillamente con il solo GITHUB_TOKEN, evitando PAT aggiuntivi.
Per alzare ulteriormente il livello, conviene valutare la firma delle immagini con cosign e le attestazioni generate dalla build. La SBOM prodotta da docker/build-push-action non risolve tutto, ma migliora parecchio visibilità e auditabilità del contenuto distribuito.
Sicurezza dell’accesso SSH per il deploy
Se scegli il deploy via SSH, non trattare quella chiave come una scorciatoia innocua. L’utente remoto dovrebbe essere dedicato, senza sudo o con privilegi molto limitati. Ancora meglio, la chiave dovrebbe essere vincolata a un comando specifico tramite command= in authorized_keys, oppure gestita con ForceCommand o uno script wrapper che accetti solo le operazioni di deploy previste.
Questa è una delle differenze tra un setup “funzionante” e uno pensato davvero per la produzione. Una chiave SSH che apre una shell piena su un server di produzione è comoda, ma non è la scelta migliore.
Se i runner GitHub-hosted non si adattano bene alle policy di rete, ha senso valutare runner self-hosted o IP allowlist più strette. Anche qui, la scelta dipende dal contesto: non sempre ha senso complicare l’infrastruttura, ma è bene sapere quali sono i trade-off.
Gestione dei segreti oltre il file .env
Il file .env va benissimo come baseline, ma non è una scusa per gestire i segreti in modo approssimativo. Oltre a permessi stretti e ownership dedicata, conviene evitare che finisca in backup leggibili da terzi, repository, artifact di pipeline o home directory condivise.
Quando i requisiti aumentano, Docker secrets, SOPS o Vault permettono di alzare il livello. Non serve per forza adottarli subito tutti, ma vale la pena sapere quando il passaggio inizia a essere giustificato: più ambienti, più operatori, più compliance, più credenziali critiche.
Hardening del reverse proxy e del container host
Nginx e il server host meritano un minimo di hardening. Oltre agli header già visti, conviene curare gli aggiornamenti di sistema, limitare le porte esposte, usare firewall espliciti, monitorare il disco, tenere Docker e Compose aggiornati e ridurre al minimo i container che girano con privilegi inutili. Anche il semplice fatto di eseguire l’app come utente non root nel container è una misura piccola ma concreta.
Notifiche, audit operativo e controlli finali
In produzione non basta che il deploy “parta”. Deve anche essere visibile. Una notifica Slack o Teams su successo e fallimento, un health check finale, un log consultabile e la possibilità di sapere quale tag è in esecuzione sono il minimo sindacale per lavorare senza andare alla cieca.
Quando passare a Kubernetes
È giusto chiudere con una considerazione onesta. GitHub Actions + GHCR + Compose è un pattern eccellente per molti casi reali, ma non è universale. Inizia a mostrare i suoi limiti quando il numero di host cresce, quando l’autoscaling diventa necessario, quando il rolling update deve essere realmente trasparente, quando servono strategie avanzate di scheduling, self-healing distribuito, service discovery più ricca o una gestione nativa di segreti e workload molto dinamici.
Una soglia tipica è questa: finché hai uno o pochi host, team piccoli, architettura relativamente lineare e requisiti di disponibilità compatibili con restart controllati o blue/green minimali, questo approccio è spesso la scelta migliore. Quando invece ti ritrovi a progettare workaround sempre più sofisticati per ottenere ciò che Kubernetes offre nativamente, probabilmente è arrivato il momento di cambiare piattaforma.
Il valore di questa soluzione, però, resta intatto: permette di arrivare molto lontano con strumenti comprensibili, economici e facili da mantenere. E in un numero sorprendentemente alto di progetti, è esattamente ciò che serve.
Conclusioni
Distribuire applicazioni .NET, Node.js o qualsiasi app containerizzata con GitHub Actions e GHCR è molto meno complicato di quanto spesso si pensi, a patto di non ridurre tutto a un paio di comandi copiati da un tutorial. Il cuore del sistema è semplice: build multi-stage, push su un registry coerente con il proprio workflow, deploy con Compose, reverse proxy davanti e tag immutabili per poter tornare indietro senza improvvisare.
La parte che distingue un how-to da un setup davvero usabile in produzione sta nei dettagli: permessi minimi nella pipeline, gestione realistica dei segreti, login sicuro al registry, health check, notifica dei fallimenti, hardening del reverse proxy, rollback chiaro e una valutazione onesta dei limiti di Compose rispetto a un orchestratore vero. Se questi elementi vengono trattati bene, il risultato è un processo di rilascio molto solido, senza introdurre complessità prematura.
Riferimenti
- GitHub Actions documentation - Documentazione ufficiale su workflow, job, permissions e automazione CI/CD su GitHub.
- GitHub Container Registry - Guida ufficiale a GHCR, autenticazione, visibilità dei package e gestione delle immagini.
- Docker GitHub Actions - Documentazione ufficiale delle action Docker per login, metadata, build e push delle immagini.
- Docker Build cache backend gha - Spiegazione del backend di cache
type=ghaper accelerare le build su GitHub Actions. - Docker Compose documentation - Riferimento ufficiale per definire e aggiornare stack containerizzati con Compose.
- Multi-stage builds - Best practice ufficiali per costruire immagini più piccole, pulite e adatte alla produzione.
- Cosign - Introduzione alla firma delle immagini container e alla supply chain security.
- NGINX Reverse Proxy - Guida alla configurazione di Nginx come reverse proxy per applicazioni HTTP containerizzate.
- Containerize a .NET app - Guida Microsoft alla containerizzazione di applicazioni .NET.
