DISABLE_REGISTRATION = true. È lei a creare gli account; nessuno si serve da solo. Se ha bisogno di iscrizioni occasionali, usi il flusso di invito invece di aprire la porta.

Un server Git tutto suo.
Il repository è l'organigramma.
Un repository privato è un registro datato di cosa sta costruendo, con chi lo sta costruendo, e a che punto è arrivato — e su una piattaforma, quel registro è in mano a un'azienda nella giurisdizione di qualcun altro. Sei passaggi per spostarlo su un server che affitta in modo anonimo: Forgejo dietro Caddy, PostgreSQL sotto, push SSH su una porta tutta sua, un runner Actions che non vede mai i suoi secret di produzione, e un backup che ha davvero ripristinato. Il server costa $3.90 al mese. Testato su Debian 12.
- 01
Scelta
Repo bare, Forgejo, GitLab
- 02
Provisioning
Porte decise prima del boot
- 03
Distribuisci
Forgejo, Postgres, Caddy
- 04
Push
SSH, deploy key, firme
- 05
Build
Un runner, tenuto a distanza
- 06
Keep
Mirror, backup, aggiornamenti
Il codice è la parte meno interessante. I metadati sono l'asset.
Quasi ogni discussione sul self-hosting di Git è in realtà una discussione sul codice sorgente, ed è proprio il codice sorgente la parte che conta meno. Gran parte di esso è poco notevole, buona parte è un sottile strato sopra librerie open source, e se trapelasse domani il danno pratico sarebbe di solito imbarazzo più che un vero danno. Questo è il motivo per cui "il mio codice non è segreto" sembra una risposta completa, e il motivo per cui non lo è.
Ciò che circonda il codice è la parte interessante. Un repository è un registro del lavoro datato, attribuito e ad alta risoluzione: i timestamp dei commit descrivono quando dorme, le righe di co-autore descrivono con chi lavora, i nomi dei branch descrivono prodotti che nessuno ha ancora annunciato, un issue tracker descrive cosa è rotto e quanto gravemente, e un primo commit data l'intero progetto. Legga cento repository in questo modo e non starà più leggendo software — starà leggendo un'azienda, la sua roadmap, il suo organico e i suoi guai.
Su una piattaforma, quel registro non è suo in alcun senso significativo. Risiede su un'infrastruttura che non controlla, sotto un account legato a un'identità che ha verificato, in una giurisdizione il cui iter legale può raggiungerlo, scansionato da sistemi il cui ambito è stabilito da un documento che può essere modificato. Niente di tutto ciò richiede malafede da parte di qualcuno: è semplicemente cosa significa affittare il tracker. Quattro configurazioni, e cosa rivela realmente ciascuna:
| Dove vive il repository | Chi detiene la cronologia | Legato alla sua identità legale | Come va storto |
|---|---|---|---|
| Una piattaforma, account personale | La piattaforma, nella propria giurisdizione | Sì — email, fatturazione, spesso un numero di telefono | Una sospensione si porta via ogni remote |
| Una piattaforma, account organizzazione | La piattaforma, più chi ne amministra l'organizzazione | Sì, e anche al suo datore di lavoro | L'offboarding le toglie l'accesso al suo stesso lavoro |
| Self-hosted, su un hyperscaler | Lei, a livello applicativo | Sì — l'account di fatturazione è l'identità | Lo snapshot vive nell'account che può essere chiuso |
| Self-hosted, su un VPS senza KYC | Lei, e soltanto lei | No — nessun documento, nessuna carta, pagamento in crypto | Ora il sysadmin è lei. Un backup fatto male è definitivo |
La terza riga è quella che coglie in fallo i team convinti di aver già risolto il problema. Una forge installata da sé, su una macchina affittata con una carta aziendale sotto un account aziendale verificato, ha spostato l'applicazione fuori dalla piattaforma lasciando l'identità esattamente dov'era. È un guadagno reale in termini di controllo e nessun guadagno in termini di esposizione.
E la quarta riga dichiara apertamente il proprio costo, perché il resto di questa guida riguarda soprattutto il pagarlo. La modalità di fallimento del self-hosting non è la sorveglianza, è la perdita. Una piattaforma mantiene backup a cui lei non pensa mai. Il suo server mantiene i backup che ha configurato lei. Il passo 06 non è la rifinitura opzionale alla fine — è il passo che rende sicuri gli altri cinque.
Di quanta forge ha bisogno? La risposta onesta è spesso "nessuna".
Inizi dalla cosa più piccola che risolve il suo problema, perché ogni livello sopra di essa è un servizio che deve gestire lei. Un repository bare via SSH è un solo comando su un server che ha già, e le dà un vero remote Git con autenticazione, cronologia e accesso in push. Nessuna applicazione web, nessun database, nessuna corsa continua agli aggiornamenti, nulla da aggiornare oltre a sshd. Se è una persona sola che vuole portare i propri commit fuori da un portatile e fuori da una piattaforma, questa è una risposta completa e può smettere di leggere dopo il passo 02.
# On the server, as an unprivileged user:
mkdir -p ~/git/project.git && cd ~/git/project.git
git init --bare
# On your laptop:
git remote add origin ssh://user@git.example.com/~/git/project.git
git push -u origin main
Ha bisogno di una forge quando entra in gioco una seconda persona, o quando desidera una delle quattro cose che una directory di repository bare non può fare: sfogliare la cronologia nel browser, rivedere le modifiche come pull request, tracciare le issue accanto al codice, e concedere a una persona l'accesso a un repository. È un autentico salto di qualità in termini di capacità e un autentico salto in termini di peso operativo. Scelga con consapevolezza:
| Proprietà | Repo bare via SSH | Forgejo | Gitea | GitLab CE |
|---|---|---|---|---|
| UI web, pull request, issue | Nessuno | Sì | Sì | Sì, e molto di più |
| Memoria a riposo | Praticamente zero | Poche centinaia di MB | Poche centinaia di MB | Diversi GB, prima della CI |
| CI integrata | git hook, se li scrive lei | Forgejo Actions | Gitea Actions | GitLab CI, quella matura |
| Chi guida il progetto | Il progetto Git | Codeberg e.V., una no-profit | Un'azienda a scopo di lucro, open core | Un'azienda quotata in borsa, open core |
| Licenza | GPLv2 | GPLv3+ dalla v9.0 (MIT prima) | MIT, con componenti aggiuntivi proprietari | Nucleo MIT, piani proprietari |
| Lo scelga quando | È da solo e vuole manutenzione zero | Vuole una forge vera su un server piccolo | La usa già e funziona | Le serve il set di funzionalità enterprise |
Forgejo e Gitea condividono un antenato e nell'uso quotidiano appaiono ancora quasi identici. Forgejo è diventato un hard fork all'inizio del 2024 ed è gestito da Codeberg e.V., un'associazione no-profit registrata a Berlin; Gitea è sviluppato da un'azienda a scopo di lucro che possiede il relativo marchio e chiede ai collaboratori la cessione del copyright. Sono entrambi software validi. Solo uno dei due ha un custode i cui incentivi corrispondono al motivo per cui sta leggendo questa pagina.
Il resto di questa guida usa Forgejo. Tutto ciò che è nei passi da 02 a 06 si applica quasi alla lettera anche a Gitea — cambiano il nome del container e alcune variabili d'ambiente, non la struttura.
Decida le porte prima che sia la forge a deciderle al posto suo. Due di esse sono SSH.
Prima di tutto ordini il server. Nel pannello: Order → VPS → Sentinel, immagine Debian 12, nel bastione in cui vuole che viva la cronologia — Helsinki, Stockholm, Oslo o Reykjavík. Non è richiesto alcun indirizzo email per aprire l'account e la fattura si salda in Monero, Bitcoin, Lightning o qualsiasi altro asset supportato. Qui questo conta più che per la maggior parte dei carichi di lavoro: una forge è l'unico servizio la cui intera proposta di valore consiste nel fatto che il registro del suo lavoro non è legato a un nome.
Poi esegua la checklist di hardening della prima ora — SSH solo a chiave su una porta spostata, aggiornamenti automatici, un firewall a blocco predefinito — perché tutto ciò che segue presuppone un server che non sia già di qualcun altro.
Una forge non è un sito statico con un login. È un'applicazione web pubblica con un motore di esecuzione del codice imbullonato sopra. Due impostazioni predefinite, se lasciate come sono, la trasformano in un servizio gratuito per sconosciuti: la registrazione aperta, che riempie l'istanza di account spam entro pochi giorni dalla comparsa del certificato nei log di trasparenza, e una CI che esegue workflow provenienti da chiunque possa aprire una pull request, il che è come un runner self-hosted finisce per minare criptovalute per qualcun altro. Nessuno dei due è un attacco sottile e nessuno dei due è raro. Entrambi si chiudono con un'impostazione ciascuno, nel passo 03 e nel passo 05 — ma decida fin da ora che li chiuderà, perché la finestra tra "funziona" e "viene usato" si misura in giorni.
Punti un hostname sul server — un record A, e un record AAAA se vuole che sia raggiungibile via IPv6 — e lo lasci propagare prima di iniziare, perché il reverse proxy proverà a ottenere un certificato per quel nome al primo avvio, e una challenge fallita è un modo confuso di cominciare. Poi scriva le regole del firewall mentre ancora nulla è in ascolto.
Il problema dei due SSH. Una forge parla SSH per Git, e il suo server parla già SSH per l'amministrazione. Non possono possedere entrambi la porta 22, e mescolarli è il modo in cui si finisce con un demone amministrativo che deve anche capire Git. Li tenga separati: l'amministrazione sulla porta spostata secondo la guida di hardening, Git su una porta pubblica dedicata che si mappa nel container. 2222 è la scelta convenzionale e non ha nulla di sacro.
ufw default deny incoming
ufw allow 47222/tcp # administrative SSH, moved
ufw allow 80/tcp # ACME HTTP-01 challenge
ufw allow 443/tcp # the web interface
ufw allow 2222/tcp # git over SSH, into the container
ufw enable
ufw status numbered
# Nothing else is open. In particular: PostgreSQL stays on the Docker
# network and is never published to the host, let alone the internet.
Installi Docker dal repository ufficiale anziché dal pacchetto della distribuzione, in modo che Compose v2 sia presente e la versione sia aggiornata. Poi crei una directory per lo stack e la renda l'unico posto sul server che conserva stato — è la directory di cui il passo 06 fa il backup.
curl -fsSL https://get.docker.com | sh
docker compose version
install -d -m 0750 /srv/forgejo
cd /srv/forgejo
Tre container. Uno di essi non deve mai essere raggiungibile.
Forgejo, PostgreSQL e Caddy. Forgejo conserva i repository e serve l'interfaccia, PostgreSQL conserva tutto ciò che non è un oggetto Git, e Caddy termina il TLS e ottiene il certificato senza doverglielo chiedere due volte. PostgreSQL non viene deliberatamente pubblicato sull'host: è raggiungibile solo tramite la rete Compose, per nome, dall'unico container che ne ha bisogno.
/srv/forgejo/.env
FORGEJO_DOMAIN=git.example.com
FORGEJO_VERSION=15 # the LTS line; 16 is current stable
POSTGRES_PASSWORD=$(openssl rand -base64 32) # paste the value, do not leave the $( )
/srv/forgejo/docker-compose.yml
services:
forgejo:
image: codeberg.org/forgejo/forgejo:${FORGEJO_VERSION}
container_name: forgejo
environment:
USER_UID: 1000
USER_GID: 1000
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${POSTGRES_PASSWORD}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__SSH_PORT: "2222" # what clones will say
FORGEJO__server__SSH_LISTEN_PORT: "22" # what the container listens on
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__repository__DEFAULT_PRIVATE: private
FORGEJO__security__INSTALL_LOCK: "true"
restart: unless-stopped
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000" # web, reached only by Caddy
- "2222:22" # git over SSH, public
depends_on:
- db
db:
image: postgres:16-alpine
container_name: forgejo-db
environment:
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: forgejo
restart: unless-stopped
volumes:
- ./postgres:/var/lib/postgresql/data
# No ports: block. This is the point.
caddy:
image: caddy:2-alpine
container_name: forgejo-caddy
restart: unless-stopped
network_mode: host
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
/srv/forgejo/Caddyfile
git.example.com {
encode zstd gzip
request_body {
max_size 512MB # raise it if you push large repositories
}
reverse_proxy 127.0.0.1:3000
}
Le due righe SSH sono quelle su cui più spesso si sbaglia, quindi vale la pena leggerle due volte. SSH_LISTEN_PORT è la porta dentro il container, dove si lega il server SSH integrato di Forgejo. SSH_PORT è la porta che Forgejo stampa negli URL di clone che le mostra — deve corrispondere al lato pubblico del mapping della porta. Le disallinei e funzionerà tutto tranne l'unica cosa che gli utenti copiano e incollano.
Lo avvii, poi crei l'amministratore da riga di comando. Non usi l'installer web. Su un'istanza appena creata è raggiungibile da chiunque trovi l'hostname, e il primo account creato tramite di esso diventa l'amministratore — una corsa a cui non ha bisogno di partecipare. INSTALL_LOCK nel file sopra lo chiude ancor prima che il container si avvii.
docker compose up -d
docker compose logs -f forgejo # wait for "Starting new Web server"
docker compose exec -u git forgejo forgejo admin user create \
--admin --username you --email you@example.com --random-password
Apra l'hostname in un browser, effettui l'accesso con la password stampata e la cambi. Poi ripassi le impostazioni che trasformano un'istanza funzionante in una chiusa — la maggior parte sono già impostate dal blocco di variabili d'ambiente sopra, e vale la pena verificarle una per una invece di darle per scontate:
DEFAULT_PRIVATE = private. I nuovi repository partono chiusi. Pubblicare diventa allora un atto deliberato invece che la conseguenza di un'impostazione predefinita.
REQUIRE_SIGNIN_VIEW = true nasconde anche i repository pubblici ai visitatori non autenticati. Giusto per una forge privata di team; sbagliato se vuole pubblicare qualcosa.
La attivi sull'account amministratore prima di fare qualsiasi altra cosa. La forge è ora la macchina che può riscrivere ogni branch che lei possiede.
Un percorso che vale la pena memorizzare: la configurazione live si trova in ./data/gitea/conf/app.ini sull'host. Forgejo ha mantenuto il nome della directory gitea per compatibilità con le installazioni esistenti, cosa che sorprende tutti esattamente una volta. Le variabili d'ambiente nella forma FORGEJO__section__KEY vengono scritte in quel file all'avvio, quindi può gestire l'intera configurazione dal file Compose — ma se modifica app.ini a mano, una variabile d'ambiente per la stessa chiave sovrascriverà la sua modifica al riavvio successivo. Ne scelga uno dei due e sia coerente.
Push via SSH. Poi lo firmi, così la cronologia non dipende dal server.
Incolli la sua chiave pubblica nelle impostazioni del profilo, poi insegni al suo client dove si trova la forge. Poiché Git è su una porta non standard, una voce nella sua configurazione SSH vale i trenta secondi che richiede — tiene la porta fuori da ogni URL che dovrà mai digitare e rende i remote portabili.
# ~/.ssh/config
Host git.example.com
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
# Then, in any repository:
git remote set-url origin git@git.example.com:you/project.git
git push -u origin main
ssh -T git@git.example.com # expect a greeting, not a shell
Quell'ultimo comando è quello che le dice se il mapping della porta, il server SSH del container e la sua chiave sono tutti d'accordo tra loro. Un saluto da parte di Forgejo significa che l'intera catena funziona. Una richiesta di password significa che ha invece raggiunto lo sshd amministrativo dell'host, e il mapping della porta è sbagliato.
Ora firmi i suoi commit. Questo è il passo che la maggior parte delle guide al self-hosting salta, e su una forge self-hosted conta più che su una piattaforma. Il campo autore di un commit è testo libero — chiunque possa fare push può scriverci il suo nome. Una firma è la prova che una chiave specifica ha creato un commit specifico, e resta una prova anche dopo che il server su cui è stato inviato con il push è stato compromesso, migrato o gettato via. Git può firmare con la chiave SSH che ha già, quindi non c'è più bisogno di alcuna deviazione tramite PGP:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git commit --allow-empty -m "signed"
git log --show-signature -1
Poi aggiunga la stessa chiave pubblica una seconda volta nella forge, come chiave di firma invece che come chiave di autenticazione. Forgejo verifica la firma lato server e mostra il commit come verificato; senza quella seconda voce non ha modo di collegare la firma al suo account, e ogni commit risulta non verificato anche se è firmato correttamente.
Deploy key, per le macchine che si limitano a leggere. Un server di produzione, una macchina di build o un mirror non hanno bisogno di un account utente sulla forge. Le dia invece una deploy key: una coppia di chiavi aggiunta a un repository, di sola lettura a meno che non conceda esplicitamente la scrittura, che non dà accesso a nient'altro sull'istanza. Revocarla significa eliminare una sola voce. Questa è l'abitudine singolarmente più efficace per mantenere ordinata una forge self-hosted, perché gli account utente accumulano permessi, mentre le deploy key non possono.
Migrare da una piattaforma è un buon momento per fare tutto questo per bene. Il flusso New Migration di Forgejo prende un URL di repository e un access token e importa branch, tag, issue, commenti, etichette, milestone, release e pull request. Esegua prima un repository per vedere che forma ha il risultato, si aspetti che i rate limit dell'API rallentino i lotti grandi, e mantenga l'origine in sola lettura per qualche settimana invece di eliminarla lo stesso giorno.
Un runner CI è una macchina che esegue codice di sconosciuti. Lo collochi di conseguenza.
Forgejo Actions legge i file di workflow nello stesso formato di GitHub Actions, da .forgejo/workflows/, e li passa a un programma separato chiamato Forgejo Runner. Il runner non fa parte della forge e non deve necessariamente vivere sulla stessa macchina — è proprio questa l'idea di fondo del design, ed è il motivo per cui questo passo arriva con un avvertimento invece che con un'alzata di spalle.
Attivi Actions sull'istanza, poi ottenga un registration token dal pannello di amministrazione — a livello di intera istanza, oppure limitato a un'organizzazione o a un repository, il che è la scelta predefinita migliore quando non è sicuro. Il runner si collega verso l'esterno alla forge, quindi non ha bisogno di alcuna porta in entrata e può stare dietro qualsiasi firewall preferisca, anche su una macchina senza indirizzo pubblico.
# In the Compose environment of the forge, or app.ini:
# FORGEJO__actions__ENABLED: "true"
# Then: Site administration -> Actions -> Runners -> Create new runner
# On the runner machine:
forgejo-runner generate-config > config.yml
forgejo-runner register --no-interactive \
--instance https://git.example.com \
--token <REGISTRATION_TOKEN> \
--name runner-01 \
--labels docker:docker://node:22-bookworm,debian:docker://debian:12
forgejo-runner --config config.yml daemon
Le etichette sono il contratto tra il workflow e il runner: un job che richiede runs-on: docker ottiene un container costruito a partire dall'immagine sul lato destro. Dichiari soltanto le etichette che è realmente pronto a servire, perché un job che richiede un'etichetta che nessuno offre resta in attesa per sempre invece di fallire, il che è una cosa confusa da debuggare alla fine di una lunga giornata.
.forgejo/workflows/ci.yml
on: [push, pull_request]
jobs:
test:
runs-on: docker
container:
image: node:22-bookworm
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
Un runner esegue tutto ciò che dice il file di workflow, e il file di workflow arriva dentro una pull request. Se chiunque al di fuori del suo team può aprirne una, il runner appartiene a una macchina diversa dalla forge — un secondo Sentinel a $3.90 al mese, senza accesso alla directory dati della forge, senza credenziali del database e senza nulla nel suo ambiente che le dispiacerebbe veder comparire in un log di build. Sulla forge stessa, richieda un'approvazione prima di eseguire i workflow per i contributor alla prima partecipazione. I due fallimenti standard sono entrambi noiosi ed entrambi costosi: un carico di mining nascosto in una pull request, e un secret stampato da un passaggio che qualcuno ha aggiunto di proposito.
Limiti i secret allo stesso modo. Forgejo li conserva a livello di repository, organizzazione e istanza; una chiave di deployment di cui ha bisogno un solo repository dovrebbe esistere soltanto su quel repository, in modo che un workflow compromesso in un progetto non correlato non possa leggerla. Preferisca token di breve durata rispetto a quelli di lunga durata ovunque la destinazione lo supporti, e non metta mai un secret in un file di workflow — il file di workflow sta nel repository, e il repository ha una cronologia.
Se le sue build scaricano le stesse immagini di base a ogni esecuzione, le metta in cache. Un registry mirror locale o una cache persistente del runner trasformano un pull di immagine di due minuti in uno di due secondi, ed è la cosa educata da fare verso i registry a monte che sta martellando di richieste. E noti lo schema che l'intero design invita ad adottare, motivo per cui questa guida sta accanto alla guida al runtime per agenti: il runner è un worker general-purpose che preleva job dalla sua forge, quindi è anche il posto giusto per la manutenzione pianificata, le pipeline di dati e i carichi di lavoro degli agenti.
Ogni clone è un backup del codice. Niente è un backup della conversazione.
Git qui è insolitamente indulgente, e vale la pena essere precisi sul perché. Ogni clone è una copia completa della cronologia, quindi finché uno sviluppatore ha una copia di lavoro aggiornata, il codice sopravvive al server. Questo è genuinamente rassicurante, e copre esattamente una delle due cose che la sua forge conserva. L'altra — issue, discussioni delle pull request, thread di revisione, release, pagine wiki, utenti, team, permessi, cronologia della CI — esiste solo nel database, nessuno ne ha un clone, ed è di solito la parte che ha richiesto tre anni per accumularsi.
Prima di tutto, un push mirror. È gratuito e si gestisce da solo. In un repository: Settings → Mirror settings → aggiunga un push mirror con l'URL e un access token per la destinazione. Forgejo replica poi ogni branch e tag dopo ciascun push, verso un secondo Forgejo, verso Codeberg, o verso una piattaforma che ha deciso di mantenere come vetrina anziché come casa. Un avvertimento che vale la pena dichiarare apertamente: un push mirror ripubblica la cronologia fedelmente, incluso un secret che intendeva rimuovere.
Poi un backup vero, che non è l'archivio integrato. Forgejo include un comando forgejo dump che impacchetta configurazione, repository, dati e un dump del database in un unico zip. È una cosa genuinamente utile da consegnare a un maintainer quando si segnala un bug. Non è ciò su cui dovrebbe fare affidamento per tornare in vita dopo un disastro: l'SQL che produce ha problemi noti di lunga data in fase di re-importazione, e l'archivio riscrive l'intero set di repository a ogni esecuzione, il che su un'istanza reale significa un file di grandi dimensioni scritto da zero ogni notte. Usi invece il percorso noioso e verificabile — faccia il dump del database con lo strumento nativo del database, e faccia il backup della directory dei dati come file:
#!/bin/sh
# /usr/local/bin/forgejo-backup — run from a systemd timer, nightly
set -eu
cd /srv/forgejo
# 1. Consistent database dump, written next to the data.
docker compose exec -T db pg_dump -U forgejo -Fc forgejo > ./db.dump
# 2. Everything that matters, to an encrypted offsite repository.
restic backup ./data ./db.dump ./docker-compose.yml ./.env ./Caddyfile
# 3. Retention, then prove the repository is still readable.
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
restic check
Lo invii a una macchina in una giurisdizione diversa da quella della forge, tramite un repository append-only, in modo che una compromissione della forge non possa cancellare la propria stessa cronologia — la guida ai backup cifrati è la versione estesa delle tre righe sopra, incluso il timer systemd e la restrizione della chiave append-only.
Poi lo ripristini una volta, di proposito. Ordini un VPS usa e getta, ripristini lo snapshot al suo interno, avvii lo stesso file Compose puntato sulla directory dati ripristinata, carichi il dump del database, punti su di esso una voce del file hosts ed effettui l'accesso. Cronometri l'intera operazione. Quel numero — non l'esistenza del job di backup — è la sua reale posizione di recupero, e chiunque abbia fatto questo esercizio ha trovato almeno una cosa mancante dall'elenco degli elementi inclusi la prima volta.
# restic keeps absolute paths, so the stack reappears under the target.
restic restore latest --target /srv/restore
cd /srv/restore/srv/forgejo
docker compose up -d db # empty volume: the entrypoint creates the database
docker compose exec -T db pg_restore -U forgejo -d forgejo \
--clean --if-exists --no-owner < db.dump
docker compose up -d # then sign in and walk the issue tracker
Aggiornamenti, in breve. Blocchi l'immagine su una versione principale invece che su :latest, così un pull non presidiato non può farla passare attraverso un breaking change mentre dorme. Legga le release notes prima di attraversare un confine di versione principale, prenda prima uno snapshot, e ricordi che una migrazione del database viene eseguita al primo avvio della nuova versione — il che è una porta a senso unico. L'auto-aggiornamento in stile Watchtower è una buona idea per un sito statico e una pessima idea per la macchina che conserva ogni branch che lei possiede.
Quattro controlli prima di dichiararsi soddisfatti. Esca dall'account e verifichi che l'istanza non offra alcun modo per creare un account. Da un browser senza sessione, verifichi che un repository privato risulti invisibile e non semplicemente vietato. Verifichi che il runner non possa leggere la directory dati della forge. E verifichi che l'ultimo restic check sia passato — il job di backup che fallisce in silenzio è quello che hanno tutti, fino al giorno in cui ne hanno bisogno.
Ha rimosso una parte in causa. Non ha rimosso l'edificio.
Fare self-hosting di una forge è un cambiamento reale di chi detiene il suo lavoro, e non è invisibilità. Quattro cose restano vere dopo che tutto quanto sopra è stato fatto, e conoscerle è la differenza tra una difesa vera e una storia che si racconta da solo.
Il disco si trova nell'edificio di qualcun altro. Un server affittato gira su hardware in un data center con porte e personale. NordBastion non ispeziona i carichi di lavoro dei clienti, e il transparency report e il warrant canary descrivono cosa accadrebbe se ci venisse chiesto — ma è l'affermazione architetturale quella onesta: ciò che cambia è che non esiste alcun indice a livello di prodotto dei suoi repository, nessuna pipeline di analisi sui suoi commit e nessuna privacy policy che possa essere modificata. Ciò che non cambia è che la macchina è fisica e non si trova a casa sua.
L'hostname diventa pubblico nel momento in cui il certificato viene emesso. Ogni certificato pubblicamente attendibile viene pubblicato nei log di Certificate Transparency che chiunque può consultare, quindi git.yourproject.com è individuabile anche se non lo collega mai da nessuna parte. Questo non è una fuga di notizie sul suo codice, ma è una fuga di notizie sul fatto che il progetto esiste e su più o meno quando è iniziato. Usi un sottodominio di un nome che pubblica già, oppure rinunci del tutto al certificato pubblico e raggiunga la forge via WireGuard o tramite un servizio onion.
Ora sta applicando patch a un'applicazione web. La forge è una pagina di login, una pipeline di rendering per markup non attendibile, un server SSH e un motore di CI, tutti raggiungibili da internet. È una superficie considerevolmente più ampia rispetto al repository bare del passo 01, ed è una superficie che deve tenere aggiornata. Si iscriva agli annunci di release, e tratti una release di sicurezza come un lavoro da fare nella stessa settimana, non nello stesso trimestre.
La federazione non c'è ancora. Forgejo sta facendo un lavoro reale sulla federazione tra forge e ne pubblica i progressi, ma a oggi non può aprire una pull request sull'istanza di qualcun altro a partire dalla propria, come può fare su una piattaforma condivisa. In pratica, questo significa che un contributor esterno ha comunque bisogno di un account sulla sua forge, oppure lei accetta patch via email, oppure mantiene un mirror pubblico per i contributi occasionali. Pianifichi in base a quale delle tre opzioni corrisponde al suo progetto, invece di esserne colto di sorpresa.
Cosa ha davvero comprato, con precisione: il registro datato di cosa costruisce, con chi, e quando, non è più in mano a un'azienda il cui modello di business include l'analizzarlo. È un'affermazione circoscritta, ed è una vera. Vale quello che costa solo se il passo 06 è stato completato — una forge self-hosted senza un ripristino testato non è un miglioramento della privacy, è un singolo punto di fallimento con un branding migliore.
La forge è piccola. La CI non lo è.
| Livello | Mensile | Risorse | Cosa fa girare comodamente |
|---|---|---|---|
| Sentinel | $3.90 | 2 vCPU · 4 GB · 120 GB | Forgejo + PostgreSQL per una persona sola o un piccolo team. Il punto di partenza giusto. |
| Un secondo Sentinel | $3.90 | Il runner, su un server a sé | L'isolamento che conta davvero, per il prezzo di un caffè |
| Garrison | $7.90 | 4 vCPU · 8 GB · 240 GB | Una forge per dieci persone, un container registry e artefatti |
| Ravelin | $16.90 | 8 vCPU · 16 GB · 480 GB | CI intensa, repository grandi, diversi job in parallelo |
Niente in questa tabella è dimensionato per Git. Forgejo e PostgreSQL insieme, a riposo, occupano poche centinaia di megabyte; ciò che riempie un server è la CI — layer dei container, cache di build, artefatti — e il contenuto dei repository che non è codice sorgente. Dimensioni prima per il disco, poi per i core, e per ultima la memoria. Qui due server piccoli battono uno grande, perché il secondo è un confine di sicurezza, non capacità.
Sei modi in cui questo va storto. Due di essi costano denaro.
Registrazione aperta, e una forge piena di sconosciuti
Il certificato compare nei log di trasparenza, gli scanner arrivano entro pochi giorni, e un'istanza con le iscrizioni abilitate si riempie di account spam e repository spam. Imposti DISABLE_REGISTRATION prima del primo avvio, non dopo il primo incidente.
Il runner che mina criptovalute per qualcun altro
Una CI che esegue workflow provenienti da qualsiasi pull request è calcolo gratuito per chiunque la trovi. Richieda l'approvazione per i contributor alla prima partecipazione, tenga il runner su un server a sé, e controlli ogni tanto la durata dei job.
ROOT_URL sbagliato, e tutto funziona a metà
Gli URL di clone puntano all'host sbagliato, i webhook scattano verso l'indirizzo sbagliato, i callback OAuth falliscono e gli avatar si rompono. L'interfaccia sembra a posto, quindi nessuno sospetta di una singola riga. Imposti insieme DOMAIN, ROOT_URL, SSH_DOMAIN e SSH_PORT e controlli un URL di clone nell'interfaccia.
Due server SSH, una porta
Il container vuole la porta 22 e l'host la sta già usando, quindi il mapping atterra silenziosamente da qualche parte di inatteso e git push chiede una password. Sposti prima l'SSH amministrativo, pubblichi il container su una porta tutta sua, e verifichi con ssh -T.
Un dump che non è mai stato ripristinato
L'archivio integrato è un artefatto di supporto, non un percorso di ripristino testato, e il suo SQL ha problemi noti in fase di re-importazione. Faccia il dump del database con pg_dump, faccia il backup della directory dei dati come file, e ripristini l'intera cosa su un server usa e getta una volta.
Il disco si riempie di layer di build, non di codice
Il codice sorgente è minuscolo; i layer dei container, gli artefatti e le cache della CI non lo sono. Un server da 120 GB con una pipeline entusiasta si riempie del tutto in poche settimane. Faccia il prune delle immagini con un timer, imponga un limite alla conservazione degli artefatti, e tenga d'occhio il disco piuttosto che la CPU.
Domande, con risposta.
Undici domande che emergono mentre si costruisce tutto questo — a partire dalle due che decidono se valga la pena costruirlo affatto.
Forgejo o Gitea — quale dovrei installare davvero?
Forgejo, per la maggior parte delle persone, e il motivo è la governance, non le funzionalità. I due progetti condividono un antenato comune e nell'uso quotidiano sembrano quasi identici: stesso binario Go, stesso ingombro leggero, stessa interfaccia web, CI paragonabile. Ciò che cambia è chi li dirige. Forgejo è gestito da Codeberg e.V., un'associazione no-profit registrata a Berlin, ed è diventato un hard fork all'inizio del 2024; pubblica tutto come software libero e non chiede ai collaboratori di cedere il copyright. Gitea è sviluppato da un'azienda a scopo di lucro che possiede il marchio e il dominio, richiede la cessione del copyright da parte dei collaboratori e adotta un modello open-core con funzionalità a pagamento intorno alla base gratuita. Nessuno dei due fatti rende Gitea un cattivo software. Ma il punto centrale dello spostare i propri repository fuori da una piattaforma è smettere di dipendere dalla roadmap di un'azienda, quindi scegliere il fork il cui custode è un'organizzazione no-profit è la decisione coerente con il motivo per cui è qui. Una nota pratica se in seguito volesse cambiare: Forgejo può essere installato sopra una directory dati di Gitea già esistente, mentre il percorso inverso non è supportato.
Ho davvero bisogno di una forge? Un repository bare via SSH non basta?
Per una persona sola con un portatile e l'abitudine di scrivere buoni messaggi di commit, un repository bare è davvero sufficiente, ed è la cosa più solida di questa guida: git init --bare sul server, un normale account SSH, e non c'è nessuna applicazione web da aggiornare, nessun database da salvare e nessuna superficie d'attacco oltre a sshd. Quella configurazione non arriverà mai a fine vita. Ha bisogno di una forge nel momento in cui entra in gioco una seconda persona, o nel momento in cui desidera una delle quattro cose che un repository bare non conosce affatto — una cronologia consultabile nel browser, pull request con revisione, un issue tracker accanto al codice, e un controllo degli accessi per singolo repository. Non è un piccolo passo in più; è la differenza tra archiviazione e collaborazione. Sia onesto su quale dei due problemi sta davvero risolvendo, perché una forge è un servizio che ora gestisce lei, mentre un repository bare è soltanto una directory.
Posso migrare i miei repository GitHub, comprese issue e pull request?
Sì, ed è una delle migliori esperienze di migrazione che si trovano nel self-hosting. Lo strumento di migrazione integrato in Forgejo prende un URL di repository e un access token personale e importa molto più del semplice codice: branch e tag, issue con i relativi commenti, pull request, etichette, milestone e release. Gli fornisca un token con permesso di sola lettura, esegua prima un repository per vedere che aspetto ha il risultato, poi proceda con gli altri. Tre cose da aspettarsi. Le migrazioni grandi o numerose incontreranno i rate limit dell'API della piattaforma di origine e richiederanno semplicemente più tempo — le avvii e le lasci fare. Gli allegati delle issue vengono scaricati dalla rete, quindi un'istanza dietro un firewall in uscita restrittivo produrrà issue con immagini rotte. E le discussioni di revisione non finiscono sempre esattamente dove si trovavano, perché i due modelli di dati non sono identici. Preveda di mantenere i repository di origine in sola lettura per qualche settimana, invece di eliminarli lo stesso pomeriggio.
Una forge self-hosted è davvero più privata, o soltanto diversa?
È davvero diversa, in un modo specifico e importante: nessuno tranne lei detiene l'indice. Su una piattaforma, il repository privato è privato rispetto agli altri utenti — non è privato rispetto alla piattaforma, che lo archivia, lo scansiona, ne calcola il grafo delle dipendenze e può essere costretta a produrlo da un tribunale della propria giurisdizione. Quell'indice è insolitamente rivelatore anche quando il codice è noioso: i timestamp dei commit descrivono il suo orario di lavoro, i co-autori descrivono il suo team, i nomi dei branch descrivono prodotti non ancora annunciati, e il primo commit data l'inizio di un progetto che nessuno ha ancora annunciato. Spostarlo sul proprio server elimina del tutto la piattaforma da questo quadro. Quello che non fa è far sparire la macchina. Sta comunque affittando hardware in un data center, il disco è comunque fisicamente raggiungibile da chi possiede l'edificio, e l'hostname che ha scelto è comunque nei log di Certificate Transparency. Il riassunto onesto: ha sostituito un'azienda con una privacy policy contrattuale con un server la cui unica policy è la sua configurazione, in una giurisdizione scelta deliberatamente — e l'account che la paga non deve necessariamente portare il suo nome.
Quanto deve essere grande il VPS?
Più piccola di quanto la documentazione di GitLab abbia abituato tutti ad aspettarsi. Forgejo e Gitea sono singoli binari Go che a riposo occupano un paio di centinaia di megabyte di RAM; aggiunga PostgreSQL e un reverse proxy e la forge di uno sviluppatore singolo sta comodamente in un Sentinel — 2 vCPU, 4 GB, 120 GB NVMe, $3.90 al mese — con margine di riserva. La pressione arriva da altri tre fronti, e vale la pena dimensionare ciascuno di essi con attenzione. La CI è il più grande: un runner che compila container userà ogni core che gli dà e riempirà il disco di layer. La dimensione dei repository è il secondo: asset di gioco, notebook e dipendenze vendored crescono molto più in fretta del codice sorgente. Artefatti e un registry di container sono il terzo. Una regola che nella pratica ha retto bene — metta la forge su un Sentinel, metta il runner su un secondo Sentinel a sé stante, e salga di piano solo quando è il disco a richiederlo, non la CPU.
Forgejo Actions è davvero compatibile con i miei workflow di GitHub Actions?
Abbastanza compatibile perché la maggior parte dei workflow semplici gira senza modifiche, ma non così compatibile da poter migrare una pipeline complessa senza leggerla con attenzione. Il formato del file di workflow è lo stesso, si trova in .forgejo/workflows/ (e viene letto anche .github/workflows/), e i blocchi di base comuni — checkout, configurazione del linguaggio, cache, build a matrice — si comportano come ci si aspetta, perché il runner li esegue in container allo stesso modo. Quello che si rompe è tutto ciò che presuppone GitHub come piattaforma anziché Actions come formato: action del marketplace ancorate a URL di github.com che la sua istanza deve poter raggiungere, action che chiamano la REST API di GitHub per i metadati di release o PR, immagini dei runner ospitati da GitHub con una toolchain preinstallata molto specifica, e autenticazione cloud basata su OIDC. Lo schema di migrazione che funziona: inizi con un workflow che si limita a compilare e testare, lo porti al verde, poi riaggiunga i passaggi di deployment uno alla volta.
Cosa succede al mio codice se il VPS si guasta?
Il codice in sé è l'unica parte che sta bene, ed è questo il vantaggio silenzioso di Git rispetto a quasi ogni altro servizio self-hosted: ogni clone è una copia completa della cronologia, quindi finché uno sviluppatore ha una copia di lavoro aggiornata, il repository non è perduto. Ciò che muore con il server è tutto quello che circonda il codice — issue, discussioni delle pull request, commenti di revisione, release, wiki, utenti, regole di accesso, cronologia della CI. Quel materiale vive solo nel database, nessuno ne ha un clone, ed è di solito la parte che ha richiesto anni per accumularsi. Quindi tratti le due cose in modo diverso. Per il codice, configuri un push mirror verso una seconda destinazione e ottiene una protezione offsite continua e automatica senza il minimo sforzo. Per i metadati, serve un backup vero: un dump di PostgreSQL più la directory dei dati, cifrati e inviati con un timer a una macchina in un'altra giurisdizione. Il passo 06 copre entrambi, e l'esercitazione di ripristino alla fine è ciò che li trasforma da un'intenzione in una garanzia.
Posso mantenere GitHub come mirror invece di abbandonarlo del tutto?
Sì, e per molti progetti è la risposta giusta, non un compromesso. Forgejo supporta i push mirror, quindi la sua istanza è l'origin su cui lavora il team e ogni push viene replicato automaticamente su un repository su GitHub, Codeberg o un secondo Forgejo. Visibilità pubblica, contributor occasionali, registry di pacchetti che si aspettano un URL di GitHub e il link all'issue tracker in un vecchio README continuano tutti a funzionare, mentre la fonte di verità — e i repository privati, e i log della CI, e la cronologia delle issue di tutto ciò che non pubblica — resta sul suo server. Funziona anche nella direzione opposta: i pull mirror mantengono una copia locale e sempre aggiornata dei progetti a monte da cui dipende, il che vale la pena farlo per tutto ciò da cui resterebbe bloccato se venisse cancellato domani. L'unica cosa su cui essere consapevoli è cosa sta mettendo in mirror: un push mirror di un repository la cui cronologia contiene un secret che intendeva rimuovere ripubblica quel secret, immediatamente e automaticamente.
Come posso dare a un collaboratore esterno accesso a un solo repository senza dargli accesso al resto?
Usi il livello che corrisponde a quanto deve durare l'accesso. Per una persona che leggerà, farà commit e aprirà pull request, crei un account utente e lo aggiunga come collaboratore esattamente sui repository di cui ha bisogno, con un livello di permesso esplicito — lettura, scrittura o admin — invece di inserirlo in un team dell'organizzazione che concede tacitamente di più. Per un sistema automatizzato che deve soltanto fare fetch — una destinazione di deployment, una macchina di build, un mirror — usi una deploy key di sola lettura invece di un account utente: è limitata a un repository, non concede nient'altro, e revocarla significa eliminare una riga. Per gli script che dialogano con l'API, emetta un access token con permessi limitati invece di consegnare una password. Due abitudini tengono in piedi tutto quanto: mantenga la registrazione chiusa, così gli unici account esistenti sono quelli che ha creato lei, e segni una data in calendario quando concede un accesso temporaneo, perché nessuna forge le ricorderà mai che un contratto è terminato.
La forge ha bisogno di un nome di dominio, e questo espone la mia identità?
Ha bisogno di un hostname per l'interfaccia web, perché i certificati TLS vengono rilasciati per dei nomi e qui nessun browser dovrebbe fidarsi di un indirizzo IP. Non ha bisogno che quel nome sia riconducibile a lei. Tre cose su cui essere consapevoli. Registri il dominio in un posto che non richieda l'identità a cui ha fatto attenzione altrove, e mantenga attiva la privacy WHOIS — il registrar è una decisione di fiducia separata rispetto all'host, ed è quella che si dimentica più spesso. Si aspetti che l'hostname diventi pubblico nel momento in cui il certificato viene emesso, perché ogni certificato pubblicamente attendibile viene pubblicato nei log di Certificate Transparency che chiunque può consultare; un wildcard su un dominio che usa già evita di aggiungere un nuovo nome ricercabile. E se preferisse non avere alcun nome pubblico, l'alternativa è reale: metta la forge su un servizio onion di Tor o raggiungibile solo via WireGuard, rinunci del tutto al certificato pubblico, e accetti che la comodità di aprire un browser da qualunque posto se ne va insieme ad esso.
A NordBastion dà fastidio se gestisco una forge Git e dei runner CI su un VPS?
Nient'affatto — una forge con i suoi runner è un carico di lavoro ordinario, ed è una delle cose che i nostri clienti costruiscono più spesso. La policy di utilizzo accettabile non dice nulla riguardo all'ospitare codice sorgente o al compilarlo. Due note operative, non di policy. Primo, un runner CI che accetta workflow da sconosciuti è un servizio di calcolo gratuito per chiunque lo trovi: l'abuso standard è il mining di criptovalute nascosto in una pull request, e verrà scoperto entro pochi giorni dall'apertura. Mantenga la registrazione chiusa, e richieda un'approvazione prima che i workflow vengano eseguiti per chiunque non sia già un collaboratore. Secondo, se le sue build scaricano immagini container di grandi dimensioni a ogni esecuzione, si tratta di traffico normale ed è incluso — ma vale comunque la pena metterle in cache localmente, perché rende la sua pipeline più veloce ed è la cosa educata da fare verso i registry a monte da cui dipende.
Una forge tutta sua, su un account che non porta il suo nome. KYC-free, pagamento in crypto.
Sentinel — 2 vCPU e 4 GB per $3.90 al mese, a Helsinki, Stockholm, Oslo o Reykjavík. Sufficiente per Forgejo, PostgreSQL e il reverse proxy, con margine per i repository che ha sempre avuto intenzione di spostare. Ne aggiunga un secondo per il runner. Nessuna email alla registrazione, nessun documento d'identità, e nessun indice a livello di prodotto di qualunque cosa invii con il push.
Ultima revisione · 2026-10-12 · Fonti · Documentazione per amministratori di Forgejo (installazione, Actions, riga di comando), il confronto di Forgejo con Gitea, le pagine sulla governance di Codeberg e.V., la documentazione di PostgreSQL e Caddy, git-config(1) · Cadenza · annuale
Anonymous VPS hosting in 2026 — the cluster.
This guide is one spoke of a larger series. The pillar walks the three privacy layers end to end — the sibling spokes below dive into the specifics.
Three independent layers — signup, payment, network — explained, legal context included, common mistakes flagged.
Bitwarden-compatible password vault under your own control.
Files, calendar, contacts, photos — owned, not rented.
Postfix, Dovecot, SPF/DKIM/DMARC — deliverability included.
A meta search engine that does not log you — because you own it.