Setup del server e primo deploy
Cosa otterrai: un server Hetzner con security hardening, Docker, Coolify e una connessione validata. Poi pushi codice, Coolify lo builda, e apri un URL per vedere la tua app. Nessun dominio richiesto. Per dominio e SSL, vedi la sezione successiva.
Prerequisito: Lockdown con Tailscale e firewall. Falla per prima. SSH al server via Tailscale, poi esegui il setup qui sotto.
Setup del server
Chiave SSH
Genera una chiave ED25519 (o usane una esistente):
ssh-keygen -t ed25519 -f ~/.ssh/hetzner-platform
Copia la chiave pubblica e aggiungila a Hetzner prima di creare il server:
cat ~/.ssh/hetzner-platform.pub
Incollala in Hetzner → + Add SSH key → salva.
Crea il server Hetzner
Quando crei il server:
| Opzione | Valore |
|---|---|
| Tipo | Regular Performance, x86 (AMD), CPX41 (8 vCPU, 16 GB RAM) |
| Location | Falkenstein o Norimberga |
| Networking | IPv4 + IPv6 pubblici |
| SSH Keys | Aggiungi la tua (richiesta) |
| Firewalls | Salta, oppure creane uno con TCP 2222, 80, 443 (lo script usa 2222 per SSH; il firewall Hetzner di default permette solo la 22) |
| Volumes, Backups, Cloud config | Salta |
| Name | platform-1 o platform-prod |
Esegui lo script di setup
Connettiti come root (porta 22 prima che lo script venga eseguito):
ssh -i ~/.ssh/hetzner-platform -o IdentitiesOnly=yes root@YOUR_SERVER_IP
Copia ed esegui lo script:
# Dalla tua macchina locale
scp -i ~/.ssh/hetzner-platform -o IdentitiesOnly=yes scripts/server-setup.sh root@YOUR_SERVER_IP:/root/server-setup.sh
# Sul server
chmod +x /root/server-setup.sh
/root/server-setup.sh
IdentitiesOnly=yes evita "too many authentication failures" quando hai molte chiavi nell'ssh-agent.
Cosa fa lo script
| Area | Dettagli |
|---|---|
| Sicurezza | Utente deploy con sudo, SSH sulla porta 2222, auth solo per chiave, login root con chiave (richiesto da Coolify), UFW, Fail2ban, unattended updates |
| Docker | CE + Compose, deploy nel gruppo docker, log rotation (10 MB × 3 file) |
| Coolify | Installato alla fine; permessi /data/coolify impostati; chiave SSH verificata |
| Sistema | Timezone Europe/Rome, 2 GB di swap, zsh + Oh My Zsh, tool (htop, ncdu, fzf, ripgrep, eza, bat, fd, jq, tree) |
Verifica prima di chiudere root
Apri un nuovo terminale e prova:
ssh -i ~/.ssh/hetzner-platform -o IdentitiesOnly=yes -p 2222 deploy@YOUR_SERVER_IP
Se funziona, sei a posto. Se no, sistemalo dalla sessione root ancora aperta.
Hetzner Cloud Firewall (richiesto)
Docker bypassa UFW. Le porte Coolify (8000, 6001, 6002) si possono restringere solo via Hetzner Cloud Firewall.
Aggiungi le regole inbound:
| Porta | Protocollo | Sorgente | Scopo |
|---|---|---|---|
| 2222 | TCP | Any | SSH |
| 80 | TCP | Any | HTTP |
| 443 | TCP | Any | HTTPS |
| 8000 | TCP | Tuo IP + IP GitHub | Dashboard Coolify (gli IP GitHub servono per webhook / auto deploy) |
| 6001 | TCP | Tuo IP | Coolify real-time |
| 6002 | TCP | Tuo IP | Terminale Coolify |
Rimuovi la porta 22 dopo il setup. Restringi 8000/6001/6002 al tuo IP per sicurezza. Per l'auto deploy on push, aggiungi i range IP webhook GitHub (vedi hooks) sulla porta 8000. Vedi server-setup-quick.md per i CIDR esatti.
Setup iniziale di Coolify
Accedi alla dashboard su http://YOUR_SERVER_IP:8000 (Coolify 4.x usa la porta 8000).
Crea il tuo account
Al primo caricamento, Coolify ti chiede di creare l'account admin root.

Riempi nome, email e una password forte (min 8 caratteri, maiuscola, minuscola, numero, simbolo).
Aggiungi il tuo server
Subito dopo la creazione dell'account, Coolify potrebbe mostrare un wizard. Saltalo e aggiungi il server a mano.

Step A: aggiungi la tua chiave SSH
- Settings → Keys & Tokens → SSH Keys → + New
- Incolla la tua chiave privata:
cat ~/.ssh/hetzner-platform(output completo, comprese le righe-----BEGINe-----END) - Salva
Step B: aggiungi il server
- Servers → + New → Remote Server
- Compila:
- Name:
platform-1(o un nome qualunque) - IP Address/Hostname: l'IP del tuo server
- Advanced Connection Settings → Port:
2222, User:root
- Name:
- Private Key: seleziona la chiave che hai aggiunto
- Click su Validate Connection
Se la validazione va a buon fine, il server è pronto per i deploy.
Perché root? Coolify fa SSH all'host per scrivere config di deploy (.env, docker-compose.yaml) sotto /data/coolify/applications/. Internamente usa sudo echo '...' | tee /path, ma tee gira con i permessi dell'utente che invoca. Con un utente non-root, la scrittura fallisce anche con sudo. Usare root lo evita. L'utente deploy resta disponibile per le tue sessioni SSH quotidiane.
Primo deploy
Dopo il setup del server hai:
- Dashboard Coolify su
http://YOUR_SERVER_IP:8000 - Coolify connesso al server come
root(porta SSH 2222) - Utente
deployper le tue sessioni SSH quotidiane
Prepara il codebase
Monorepo e pnpm
Questa repo usa pnpm workspaces. Alla root ti serve:
pnpm-workspace.yamlconpackages: ["apps/*", "packages/*"]package.jsondi root con"packageManager": "[email protected]"onlyBuiltDependencies: [sharp]inpnpm-workspace.yaml(pnpm 10 blocca i lifecycle script di default; sharp serve per le immagini Next.js)
Scaffolda l'app blog
mkdir -p apps
pnpm create next-app@latest apps/blog
Rispondi: TypeScript, ESLint/Biome, Tailwind, directory src/, App Router.
Poi:
pnpm install
Aggiungi a pnpm-workspace.yaml:
onlyBuiltDependencies:
- sharp
Esegui di nuovo pnpm install. Rimuovi apps/blog/pnpm-lock.yaml se presente (in monorepo si usa solo il lockfile di root). Esegui sempre pnpm install dopo aver aggiunto dipendenze e committa pnpm-lock.yaml; Coolify usa --frozen-lockfile e fallisce se il lockfile non è in sync. Vale per qualunque modifica a apps/*/package.json: aggiungere @radix-ui/react-scroll-area, rehype-slug o qualunque altra dep senza eseguire pnpm install alla root romperà il prossimo deploy.
Configurazione Next.js
Non usare output: "standalone" con Nixpacks. Il build pack fa una COPY finale che sovrascrive l'output di build, quindi server.js manca a runtime. Usa il next start di default.
Verifica in locale
pnpm --filter blog dev
Apri http://localhost:3000. Testa la produzione: pnpm --filter blog build poi pnpm --filter blog start.
Connetti Coolify a GitHub
Per una repo privata, Coolify ha bisogno di una GitHub App.
Aggiungi il source GitHub App
- Sources → + Add → GitHub App
- Name: es.
platform-coolify(evita il nome riservatocoolify) - Organization: lascialo vuoto per account personali
- Click su Continue
Coolify ti reindirizza a GitHub per creare l'app. Completa il flow e installa l'app sul tuo account. Scegli Only select repositories e seleziona platform.
Registra il webhook
Tornato in Coolify, apri Sources → la tua GitHub App. Vedrai:
- Webhook Endpoint:
http://YOUR_SERVER_IP:8000 - Bottone Register Now
- Un warning: "You must complete this step before you can use this source!"
Click su Register Now. Questo dice a GitHub dove mandare gli eventi di push e PR. Lascia Mandatory e Preview Deployments spuntati.
Credenziali GitHub App (se usi setup manuale): riempi Client Secret e Webhook Secret da GitHub → Developer settings → GitHub Apps → la tua app. Organization resta vuoto per account personali.
Permessi GitHub App (su GitHub): Repository permissions → Contents (Read-only), Metadata (Read-only). Subscribe to events → abilita Push (richiesto per auto deploy). Opzionalmente Pull requests per i preview deployment.
Crea e fai deploy dell'applicazione
Crea un progetto
Projects → + Add (o New Project). Chiamalo platform, poi Continue.
Aggiungi l'applicazione
Nel progetto, + Add Resource → Application.
Step 1: repository e build pack
| Campo | Valore |
|---|---|
| Repository | platform (selezionalo dal dropdown, poi Load Repository) |
| Branch | master (o main) |
| Build Pack | Nixpacks |
| Base Directory | / |
| Port | 3000 |
| Is it a static site? | No |
Click su Continue.
Step 2: comandi di build e start
| Campo | Valore |
|---|---|
| Install Command | (lascia vuoto: auto-detected via packageManager) |
| Build Command | pnpm --filter blog build |
| Start Command | pnpm --filter blog start |
| Watch Paths | ** (oppure apps/blog/** se vuoi i deploy solo quando cambia il codice del blog) |
Base Directory / è richiesto: Nixpacks ha bisogno di pnpm-lock.yaml e pnpm-workspace.yaml alla root della repo. Build e start puntano al pacchetto blog via --filter blog. Usa Watch Paths ** così i push che cambiano pnpm-lock.yaml o config di root scatenano comunque un deploy.
Abilita Auto Deploy
Configuration → Advanced → General → abilita Auto Deploy. Senza, i push non scateneranno deploy.
Deploy
Click su Save poi Deploy. Coolify builda l'immagine, avvia il container e assegna un URL (es. random-name.YOUR_IP.sslip.io). Aprilo per verificare che l'app sia live.
L'URL lo trovi in Configuration → Domains o nel tab Links.
Troubleshooting
Se l'app è in stato Exited, controlla Logs per errori.
| Errore | Fix |
|---|---|
Permission denied scrivendo .env o docker-compose.yaml | Coolify deve fare SSH come root. Controlla: (1) Server User è root in Coolify, (2) PermitRootLogin prohibit-password nella config SSH. Lo script di setup gestisce entrambi. |
ERR_PNPM_NO_LOCKFILE | Base Directory deve essere /. Con apps/blog, il lockfile manca dal contesto di build. |
ERR_PNPM_OUTDATED_LOCKFILE | pnpm-lock.yaml non è in sync con package.json (es. dep aggiunte a apps/blog/package.json senza eseguire pnpm install). Esegui pnpm install alla root della repo, committa e pusha pnpm-lock.yaml. |
pnpm: command not found | Aggiungi "packageManager": "[email protected]" al package.json di root. Nixpacks usa Corepack per installare pnpm. |
onlyBuiltDependencies mancante per sharp | Aggiungi onlyBuiltDependencies: [sharp] a pnpm-workspace.yaml, poi pnpm install di nuovo. |
Errore cache Docker BuildKit (parent snapshot does not exist) | Sul server: sudo docker builder prune -af, poi riprova il deploy. |
| Push che non scatena deploy | Controlla: (1) GitHub → Developer settings → GitHub Apps → la tua app → Advanced → Recent Deliveries (verde = webhook OK); (2) Auto Deploy abilitato in Coolify → Advanced; (3) Watch Paths: apps/blog/** scatena solo sui cambi del blog; usa ** per qualunque push; (4) Se il webhook è OK ma niente deploy, click su Redeploy a mano. |
| Caricamento infinito su Brave (o altri browser privacy) | Brave Shields blocca script di terze parti (es. Clerk da clerk.com). Se ClerkProvider avvolge tutta l'app, la pagina si pianta. Avvolgi ClerkProvider solo intorno alle route admin/auth, non nel root layout. Vedi ClerkProvider e browser privacy nella sezione Autenticazione. |
Appendice: tool installati
| Tool | Scopo |
|---|---|
| htop | Monitor processi interattivo |
| ncdu | Uso disco visuale |
| fzf | Fuzzy finder |
| ripgrep | Ricerca testo veloce |
| eza | ls moderno |
| bat | cat con syntax highlighting |
| fd | File finder veloce |
| jq | Parsing JSON |
| tree | Albero di directory |
Alias: dc → docker compose, dps → stato dei container, fd → fdfind (rename Ubuntu).
Vedi server-setup-quick.md per riferimento rapido e troubleshooting.
Cursor rules e skill (opzionali)
Se usi Cursor (o Claude Code), imposta rules e skill prima o dopo lo scaffolding.
Rules (.cursor/rules/*.mdc): convenzioni a livello progetto, sempre disponibili. Usa description, alwaysApply o globs nel frontmatter.
Skill (.cursor/skills/ o ~/.cursor/skills/): istruzioni specifiche per task, caricate quando rilevanti. Installale via:
npx skills add https://github.com/vercel-labs/next-skills --skill next-best-practices

Sfoglia skills.sh per altro. Le skill specifiche del progetto vanno in .cursor/skills/ o .claude/skills/ con un file SKILL.md.
Gitignore e env var (opzionale)
.gitignore di root: node_modules/, .next/, out/, .agents/, .env, .env.local, .env*.local.
Sviluppo locale: cp apps/blog/.env.example apps/blog/.env.local, edita coi valori reali. Mai committare.
Coolify: aggiungi le env var nella UI Coolify per ogni app. Vengono iniettate a runtime. Per app con auth (es. Better Auth), imposta NEXT_PUBLIC_SITE_URL e BETTER_AUTH_URL al tuo URL di produzione (es. https://your-domain.com). Le var NEXT_PUBLIC_* sono embeddate al build time, quindi rifai il deploy dopo averle aggiunte.