Questa guida affronta operativamente tutte le fasi per mettere online un portfolio personale gestito interamente in autonomia. Il seguente documento è stato sviluppato parallelamente alla progettazioen del mio sito "michelemacellari.me". Pertanto, l'approccio utilizzato nella stesura delle seguente guida è puramente pratico. Dunque, non verranno discussi, da un punto di vista teorico, i pro e i contro di ogni scelta ma mi limiterò ad elencare ciò che ho fatto.
Il dominio è il nome del tuo sito. Costa pochi euro all'anno e si compra da un registrar: Namecheap, Cloudflare Registrar, Aruba, OVH, Porkbun sono tutte scelte ragionevoli. io ho scelto Aruba in quanto mi sembrava il più economico.
nomecognome.me è il pattern più riconoscibile.com è la più nota, .dev per profili tech (ha HTTPS forzato — è un vantaggio)Una volta scelto il nome, vai sul registrar (Es. Aruba) cerchi il nome e acquisti (costa pochi euro all'anno).
Il costo del dominio va dai 4 agli 8 euro all'anno. io per il primo anno ho speso 6,99 euro
Un VPS (Virtual Private Server) è una macchina virtuale in un datacenter. Per un portfolio io ho preso un server da 4 GB di RAM e 40 GB di SSD sul sito Hetzner (consigliato: costa poco) a 4 euro/mese. Tuttavia, per un portfolio personale è sovradimensionato e consiglio di cercare alternative gratuite come: Oracle Cloud (problema lista di attesa su server di Milano, Torino, Francofort (Prima opzione, tuttavia lista d'attesa troppo lunga).
portfolio)Prima di creare il server, generati una coppia di chiavi SSH sul PC. Su Windows apri PowerShell e digita:
ssh-keygen -t ed25519 -C "tua-email@example.com"
Premi invio ai prompt (puoi anche non mettere passphrase, per semplicità). Verranno creati due file in C:\Users\TUO_UTENTE\.ssh\:
id_ed25519 — chiave privata, non condividerla mai con nessunoid_ed25519.pub — chiave pubblica, questa la dai al server
Apri id_ed25519.pub con un editor, copia tutto il contenuto e nel pannello Hetzner aggiungilo come SSH key sul server in fase di creazione.
Una password può essere indovinata da un bot. Una chiave ed25519 no: è un numero così grande che non si fa nemmeno a bruteforzarlo. Inoltre niente più password da digitare ogni volta.
Dopo qualche secondo dalla creazione, Hetzner ti mostra l'IP pubblico del server (es. 66.254.114.41). Da PowerShell:
ssh root@66.254.114.41
Al primo accesso conferma il fingerprint. Sei dentro al server!
Il dominio sa di esistere, il server sa di esistere, ma non si conoscono. I record DNS sono l'elenco telefonico che collega tuonome.me all'IP del server.
Vai sul pannello del registrar, nella sezione DNS / Advanced DNS, e aggiungi:
| Tipo | Host | Valore | TTL |
|---|---|---|---|
| A | @ | 66.254.114.41 | auto |
| A | www | 66.254.114.41 | auto |
| A | * | 66.254.114.41 | auto |
Il record @ punta il dominio nudo (tuonome.tld), www punta www.tuonome.tld, e il wildcard * punta qualsiasi sottodominio (utile per il futuro). Sostituisci l'IP con quello del tuo server.
La propagazione DNS può richiedere da qualche minuto a qualche ora. Per verificare, dal server:
apt install dnsutils -y
dig tuonome.me +short
Quando ti restituisce l'IP che hai messo, sei a posto.
Ubuntu include ufw (Uncomplicated Firewall). Sul server:
ufw allow 22 # SSH
ufw allow 80 # HTTP
ufw allow 443 # HTTPS
ufw enable
Tutto il resto è chiuso. Se in futuro un progetto avesse bisogno di altre porte aperte verso l'esterno (raro, di solito basta il reverse proxy), aggiungi una regola.
Docker esegue applicazioni in tutti i linguaggi di programmazione all'interno di "Container". Fondamentalmente, all'interno di questi "Container", c'è tutto il necessario per far funzionare il software(Codice, librerie, impostazioni).
Sul server, lo script ufficiale fa tutto:
curl -fsSL https://get.docker.com | sudo sh
docker --version
docker compose version
Sei sul server (SSH). Tutto quello che segue lo digiti nella stessa sessione SSH che hai aperto al punto 2 (ssh root@TUO_IP). Iniziamo creando l'albero di cartelle che ospiterà il sito.
cd /root
mkdir sito
cd sito
mkdir portfolio
mkdir projects
Verifica con ls di vedere le due cartelle appena create. La struttura finale (con i file che creiamo nei prossimi punti) sarà:
/root/sito/
├── Caddyfile
├── docker-compose.yml
├── portfolio/
│ └── index.html
└── projects/
Sul server. Apri l'editor nano per creare il primo file (il portfolio statico):
nano portfolio/index.html
Incolla dentro questo contenuto:
<!doctype html>
<html lang="it">
<head><meta charset="utf-8"><title>Ciao</title></head>
<body><h1>Funziona!</h1></body>
</html>
Per salvare in nano: Ctrl+O, poi Invio per confermare il nome, poi Ctrl+X per uscire.
Caddy è un reverse proxy moderno che richiede e rinnova automaticamente i certificati HTTPS Let's Encrypt. È la sua killer feature: zero configurazione TLS, basta indicare i domini.
Sul server, mentre sei in /root/sito:
nano Caddyfile
Incolla dentro (sostituendo tuonome.me con il tuo dominio vero):
tuonome.me, www.tuonome.me {
encode gzip
handle {
root * /srv/portfolio
file_server
}
}
Salva ed esci (Ctrl+O, Invio, Ctrl+X).
Sul server, sempre nella cartella /root/sito:
nano docker-compose.yml
Incolla dentro:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./portfolio:/srv/portfolio:ro
- caddy_data:/data
- caddy_config:/config
networks:
- web
networks:
web:
volumes:
caddy_data:
caddy_config:
Salva ed esci (Ctrl+O, Invio, Ctrl+X).
Il file docker-compose.yml è in formato YAML, che è maniacale sull'indentazione: deve essere fatta con spazi (non tab), e ogni livello rientra di 2 spazi. Se copi-incolli, di solito è tutto a posto. Se errore al primo avvio, verifica gli spazi.
Sul server, dalla cartella /root/sito:
docker compose up -d
La prima volta Docker scarica l'immagine di Caddy (qualche secondo). Poi Caddy parte, vede dal Caddyfile che deve gestire tuonome.me, chiede automaticamente il certificato HTTPS a Let's Encrypt e lo riceve. Aspetta 20–30 secondi, poi apri nel browser:
https://tuonome.me
Vedi Funziona! con il lucchetto verde accanto all'URL. Primo traguardo: il tuo sito è online, accessibile da tutto il mondo, con HTTPS.
Per verificare lo stato dal server:
docker compose ps
Deve mostrare il container caddy con stato Up.
docker compose ps — stato dei container attivi.
docker compose logs -f caddy — log Caddy in tempo reale (Ctrl+C per uscire).
docker compose restart caddy — riavvia solo Caddy (serve quando modifichi il Caddyfile: Caddy non rilegge da solo il file).
docker compose down && docker compose up -d — riavvio pulito completo.
A questo punto il sito gira sul server, ma esiste solo lì: se per qualsiasi motivo perdi accesso o formatti il server, hai perso tutto. Inoltre, modificare i file via nano in SSH non è il modo più comodo di lavorare. Spostiamo subito il codice in tre posti — PC, GitHub, server — prima di aggiungere progetti veri. Così tutti i progetti che aggiungerai dopo nasceranno già dentro questo flusso.
Sul PC, apri PowerShell e digita:
ssh-keygen -t ed25519 -f C:\Users\TUO_UTENTE\.ssh\github -C "github"
Premi Invio ai due prompt successivi (senza passphrase, per semplicità). Verranno creati:
C:\Users\TUO_UTENTE\.ssh\github — chiave privata (NON condividere)C:\Users\TUO_UTENTE\.ssh\github.pub — chiave pubblica (da dare a GitHub)Mostra il contenuto della chiave pubblica per copiarlo:
type C:\Users\TUO_UTENTE\.ssh\github.pub
Seleziona tutto l'output (da ssh-ed25519 fino a fine riga) e copialo.
PC personale (un nome qualsiasi). Key type: Authentication Key
Sul PC (PowerShell), crea (o modifica) il file C:\Users\TUO_UTENTE\.ssh\config:
notepad C:\Users\TUO_UTENTE\.ssh\config
(Se chiede di crearlo nuovo, accetta.) Incolla dentro:
Host github.com
HostName github.com
User git
IdentityFile C:/Users/TUO_UTENTE/.ssh/github
Attenzione: su Windows il path deve essere assoluto e con slash /, non backslash. Salva e chiudi Notepad. Testa:
ssh -T git@github.com
Alla prima esecuzione conferma il fingerprint con yes. Deve rispondere: "Hi TUO_USERNAME! You've successfully authenticated, but GitHub does not provide shell access."
sito (o come preferisci)
Nella pagina che appare, in alto, segna l'URL SSH: git@github.com:TUO_USERNAME/sito.git — lo useremo subito.
Sul PC (PowerShell), una sola volta in vita tua:
git config --global user.name "Tuo Nome"
git config --global user.email "tua-email@example.com"
Senza questa configurazione, il primo commit fallirà con "Author identity unknown".
Sul PC (PowerShell), scegli dove tenere il sito (es. C:\Sito):
mkdir C:\Sito
cd C:\Sito
scp -r root@TUO_IP:/root/sito/. .
Sostituisci TUO_IP con l'IP del tuo server. Il comando scp (Secure Copy) usa la chiave SSH già configurata e copia ricorsivamente tutto il contenuto di /root/sito/ dentro C:\Sito. Verifica con dir: devi vedere Caddyfile, docker-compose.yml e la cartella portfolio (al momento niente projects: la aggiungeremo dopo).
Sul PC, nella cartella C:\Sito:
notepad .gitignore
(Se Notepad chiede di crearlo, accetta.) Incolla dentro:
__pycache__/
*.pyc
.DS_Store
node_modules/
.env
*.log
Salva e chiudi. Questo file dice a Git quali file ignorare (cache, log, segreti) per non finire mai su GitHub.
Sul PC (PowerShell), sempre dentro C:\Sito:
git init
git remote add origin git@github.com:TUO_USERNAME/sito.git
git add .
git commit -m "primo commit"
git branch -M main
git push -u origin main
Cosa fa ognuno:
git init — trasforma la cartella in un repository Git localegit remote add origin … — collega il repo locale al repo su GitHubgit add . — mette tutti i file nello staging (pronti per la fotografia)git commit -m "…" — scatta la fotografia con un messaggiogit branch -M main — rinomina il ramo predefinito in main (convenzione moderna)git push -u origin main — manda tutto su GitHub e ricorda che main traccia origin/mainApri la pagina GitHub del repo nel browser: ora vedi tutti i tuoi file lì. Terzo traguardo.
Password, chiavi API, token, file .env, chiavi SSH private. Anche se il repo è privato adesso, lo storico Git conserva tutto: se per errore committi un segreto, NON basta toglierlo in un commit successivo — devi ruotare la chiave dal provider perché va considerata compromessa. Per questo serve .gitignore.
Adesso il codice esiste su PC e su GitHub, ma sul server è ancora "scollegato" — è una copia che non sa di GitHub. Vogliamo che il server sappia tirarsi giù gli aggiornamenti via git pull. Serve una deploy key dedicata, in sola lettura.
Sul server (SSH):
ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -N ""
Il flag -N "" dice "senza passphrase" (serve perché il server faccia i pull non interattivi). Crea due file in /root/.ssh/: github_deploy (privata) e github_deploy.pub (pubblica).
Mostra il contenuto della chiave pubblica:
cat ~/.ssh/github_deploy.pub
Seleziona tutto l'output e copialo. Poi:
sito → Settings (in alto a destra del repo, non del profilo)server hetznerSul server (SSH):
nano ~/.ssh/config
Incolla:
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_deploy
IdentitiesOnly yes
Salva ed esci. Sistema i permessi e testa:
chmod 600 ~/.ssh/config
ssh -T git@github.com
Deve rispondere: "Hi TUO_USERNAME/sito! You've successfully authenticated…". Nota: per le deploy key il messaggio cita anche il nome del repo.
Sul server (SSH). Adesso vogliamo che /root/sito non sia più una cartella "qualsiasi" ma un clone Git del repo. Prima fermiamo i container e mettiamo da parte la cartella attuale come backup:
cd /root/sito
docker compose down
cd ~
mv ~/sito ~/sito-backup
git clone git@github.com:TUO_USERNAME/sito.git
cd sito
docker compose up -d
Cosa abbiamo fatto:
sito-backup (in caso qualcosa vada storto)~/sito è ora un repo Git collegato a GitHub e contiene gli stessi file di prima
Apri nel browser https://tuonome.me per verificare che la vetrina risponda ancora. Quando sei tranquillo, elimina il backup:
rm -rf ~/sito-backup
deploySul server (SSH):
nano ~/.bashrc
Vai in fondo al file (Ctrl+End) e aggiungi:
alias deploy='cd ~/sito && git pull && docker compose up -d --build'
Salva ed esci. Ricarica la configurazione della shell:
source ~/.bashrc
Da adesso, ogni volta che sul server digiti deploy, vengono eseguite in sequenza: vai in ~/sito, scarica gli aggiornamenti da GitHub, ricostruisci e riavvia i container. Una sola parola per pubblicare le modifiche.
Adesso che il workflow Git è in piedi, non tocchiamo più il server a mano. Tutti i progetti (questo primo e tutti i successivi) nascono sul PC, vengono pushati su GitHub e arrivano sul server con un solo comando deploy. L'esempio assume una app Python/FastAPI, ma il pattern è identico per Node, Go, Rust o qualsiasi altra cosa che parli HTTP.
Sul PC, dentro C:\Sito:
mkdir projects
mkdir projects\mioprogetto
Dentro projects\mioprogetto\ metti il codice della tua app (qui non spiego come scrivere una FastAPI: assumo che tu abbia già il codice). Quello che serve aggiungere SEMPRE è un file Dockerfile.
Sul PC, apri il tuo editor (VS Code, Notepad) e crea il file C:\Sito\projects\mioprogetto\Dockerfile (senza estensione) con dentro (esempio FastAPI; cambia in base al tuo stack):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.api:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--proxy-headers", \
"--forwarded-allow-ips=*", \
"--root-path", "/mioprogetto"]
Sul PC, apri C:\Sito\docker-compose.yml e aggiungi il blocco mioprogetto: sotto al servizio caddy (stessa indentazione). Il file diventa:
services:
caddy:
# … blocco già esistente …
mioprogetto:
build: ./projects/mioprogetto
restart: unless-stopped
networks:
- web
networks:
web:
volumes:
caddy_data:
caddy_config:
Sul PC, apri C:\Sito\Caddyfile e modificalo così (le righe nuove vanno prima del blocco handle):
tuonome.me, www.tuonome.me {
encode gzip
handle_path /mioprogetto/* {
reverse_proxy mioprogetto:8000
}
redir /mioprogetto /mioprogetto/ 308
handle {
root * /srv/portfolio
file_server
}
}
handle_path rimuove il prefisso /mioprogetto prima di girare la richiesta al container (l'app dentro pensa di vivere alla radice). La riga redir aggiunge automaticamente lo slash finale se l'utente arriva su /mioprogetto senza slash.
Sul PC, apri C:\Sito\portfolio\index.html e aggiungi un link al progetto, anche solo:
<a href="/mioprogetto/">Apri il mio progetto</a>
Sul PC (PowerShell), dentro C:\Sito:
git add .
git commit -m "aggiunto primo progetto"
git push
Sul server (SSH):
deploy
Il server scarica le novità da GitHub, costruisce l'immagine del progetto (la prima volta scarica Python + dipendenze: 1–2 minuti) e riavvia tutto. Quando finisce, apri nel browser:
https://tuonome.me/mioprogetto/
L'applicazione risponde. Secondo traguardo.
L'alias deploy esegue docker compose up -d --build, che copre il caso più comune. Quando in futuro vorrai agire più chirurgicamente, ricordati che:
Modifica al codice di un progetto → docker compose up -d --build (ricostruisce l'immagine).
Modifica al Caddyfile → docker compose restart caddy (Caddy non ricarica automaticamente).
Modifica al docker-compose.yml → docker compose up -d (ricrea solo ciò che è cambiato).
Modifica alla vetrina statica HTML → niente, Caddy serve i file via bind mount.
Sul PC, dentro C:\Sito:
git add .
git commit -m "descrizione cambiamento"
git push
Poi sul server (SSH):
deploy
30 secondi e la modifica è online. Apri il browser per verificare (con Ctrl+Shift+R se sospetti la cache).
Tutto il workflow Git si riduce a tre verbi.
git add . — prende tutte le modifiche che hai fatto e le prepara per essere "fotografate".
git commit -m "..." — scatta la fotografia con un messaggio che descrive cos'è cambiato (sarà la tua memoria futura, scrivi qualcosa di sensato).
git push — manda la fotografia su GitHub.
Stesso identico flusso del primo progetto, riassunto:
Sul PC, dentro C:\Sito:
projects/nuovoprogetto/ con codice + Dockerfiledocker-compose.ymlhandle_path /nuovoprogetto/* { reverse_proxy nuovoprogetto:PORTA } + redir al Caddyfileportfolio/index.htmlgit add . && git commit -m "…" && git pushSul server: deploy.
Sul server (SSH):
cd ~/sito
git log --oneline # vedi i commit recenti, copia l'hash che vuoi
git reset --hard HASH # torna a quel commit (sostituisci HASH)
docker compose up -d --build
In 30 secondi sei alla versione di prima. Questo è il bello di tenere tutto in Git: ogni commit è un punto di ripristino.
Dominio: ~10–15 €/anno. Server Hetzner CX22: ~4 €/mese (~50 €/anno). Tutto il resto (HTTPS, GitHub repo privato, monitoring base) è gratis. Totale: ~60–70 €/anno per un portfolio personale tuo al 100%, espandibile senza limiti aggiungendo container.
Hai un'infrastruttura che regge i prossimi cinque progetti senza cambiare nulla. La prossima volta che vuoi aggiungere qualcosa, basta una cartella in projects/, una riga in docker-compose.yml, un blocco in Caddyfile, un push, un deploy. Buon lavoro.