Gitea is a full GitHub-shaped forge — repos, issues, pull requests, a package registry, and now its own CI — in one small Go binary. The web half installs in five minutes. The part that actually costs an evening is git-over-SSH, because your VPS's SSH port is already spoken for.Gitea ist eine vollwertige Forge im GitHub-Stil – Repos, Issues, Pull Requests, eine Paketregistry und inzwischen eine eigene CI – in einer kleinen Go-Binary. Die Weboberfläche ist in fünf Minuten installiert. Was wirklich einen Abend kostet, ist Git-over-SSH, weil der SSH-Port deines VPS schon vergeben ist.
Gitea, unlike some self-hosted apps that only pretend SQLite is an option, genuinely supports it in production for small teams. A single maintainer or a handful of collaborators pushing to a dozen private repos will not notice the database at all — it is one file, backed up by copying it, and there is nothing else to run.Anders als manche selbst gehosteten Apps, die SQLite nur zum Schein anbieten, unterstützt Gitea es für kleine Teams tatsächlich produktiv im Einsatz. Ein einzelner Maintainer oder eine Handvoll Mitarbeiter, die in ein Dutzend private Repos pushen, merken von der Datenbank überhaupt nichts — sie ist eine einzelne Datei, die du durch Kopieren sicherst, und es läuft sonst nichts, worum du dich kümmern müsstest.
Move to Postgres once either becomes true: several people are pushing and opening pull requests at the same time and you want real concurrent writes, or you are running Gitea Actions on the same box, where the runner and the web UI can both hit the database while a job is in flight. Postgres also makes backups cleaner later — a pg_dump is a text stream you can diff and restore selectively, where a SQLite file backup is all-or-nothing.Wechsle zu Postgres, sobald eines von beidem zutrifft: Mehrere Leute pushen und öffnen gleichzeitig Pull Requests und du willst echte gleichzeitige Schreibzugriffe, oder du betreibst Gitea Actions auf derselben Maschine, wo Runner und Web-UI gleichzeitig auf die Datenbank zugreifen können, während ein Job läuft. Postgres macht Backups später auch sauberer — ein pg_dump ist ein Textstrom, den du diffen und selektiv wiederherstellen kannst, während ein SQLite-Datei-Backup alles oder nichts ist.
There is no in-place conversion between the two: outgrowing SQLite means exporting what you can, or scripting a migration, rather than flipping a config flag — decide early if you already know you'll have more than one or two regular committers.Es gibt keine Konvertierung zwischen beiden im laufenden Betrieb: SQLite zu entwachsen bedeutet, zu exportieren, was geht, oder eine Migration zu skripten, statt einfach ein Konfigurations-Flag umzulegen — entscheide dich früh, wenn du schon weißt, dass du mehr als ein oder zwei regelmäßige Committer haben wirst.
Gitea itself is light. The Go binary, a handful of goroutines, and either SQLite or a thin Postgres connection: 1 GiB of RAM is genuinely comfortable for a personal instance, Caddy included.Gitea selbst ist leichtgewichtig. Die Go-Binary, eine Handvoll Goroutinen und entweder SQLite oder eine schlanke Postgres-Verbindung: 1 GiB RAM ist für eine persönliche Instanz wirklich komfortabel, Caddy inklusive.
The number that changes this is Actions. A CI job is a short-lived container doing real work — compiling, running a test suite, building an image — and while it runs it competes for the same RAM as Gitea and Postgres. Turn Actions on and point a runner at the same VPS, and plan for 2 GiB as the realistic floor, more if your builds pull large base images or compile anything non-trivial. Disk follows the same pattern: repos are usually small, but the Docker layer cache a runner accumulates is not, so keep an eye on it once jobs run regularly.Was das ändert, ist Actions. Ein CI-Job ist ein kurzlebiger Container, der echte Arbeit erledigt — kompilieren, eine Testsuite ausführen, ein Image bauen — und während er läuft, konkurriert er mit Gitea und Postgres um denselben RAM. Schaltest du Actions ein und richtest einen Runner auf demselben VPS ein, plane 2 GiB als realistische Untergrenze, mehr, wenn deine Builds große Base-Images ziehen oder etwas nicht Triviales kompilieren. Beim Speicherplatz gilt dasselbe Muster: Repos sind meist klein, aber der Docker-Layer-Cache, den ein Runner ansammelt, ist es nicht — behalte ihn im Auge, sobald regelmäßig Jobs laufen.
Pin the image tag rather than tracking latest, so an upgrade is something you choose, not something that happens on a restart — the comment on the image line below is where to check for the current minor.Pinne den Image-Tag, statt latest zu verfolgen, damit ein Upgrade etwas ist, das du entscheidest, und nicht etwas, das bei einem Neustart einfach passiert — der Kommentar in der Image-Zeile unten sagt dir, wo du die aktuelle Minor-Version nachschaust.
services:
gitea:
image: gitea/gitea:1.22 # check hub.docker.com/r/gitea/gitea/tags for the current minor
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
- GITEA__server__SSH_DOMAIN=git.example.com
- GITEA__server__SSH_PORT=2222
- GITEA__server__SSH_LISTEN_PORT=22
- GITEA__security__INSTALL_LOCK=true
- GITEA__service__DISABLE_REGISTRATION=true
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
volumes:
gitea_data:
The web port is bound to 127.0.0.1 on purpose, so only the reverse proxy on the same host can reach it. The SSH port is deliberately not loopback-bound, because it has to be reachable from wherever you push from; more on the number 2222 below.Der Web-Port ist absichtlich an 127.0.0.1 gebunden, sodass ihn nur der Reverse-Proxy auf demselben Host erreicht. Der SSH-Port ist bewusst nicht an Loopback gebunden, weil er von überall erreichbar sein muss, von wo aus du pushst; mehr zur Zahl 2222 weiter unten.
To move the database to Postgres, add a second service and point Gitea at it:Um die Datenbank auf Postgres umzustellen, füge einen zweiten Service hinzu und richte Gitea darauf aus:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=gitea
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
Add db_data: next to gitea_data: under the top-level volumes: block — Compose won't start a service that references an undeclared volume.Füge db_data: neben gitea_data: im obersten volumes:-Block hinzu — Compose startet keinen Service, der auf ein nicht deklariertes Volume verweist.
and add to the gitea service's environment, plus depends_on: [db]:und ergänze im environment des gitea-Services sowie depends_on: [db]:
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${DB_PASSWORD}
Put the password in a .env file next to the compose file, not inline in the YAML:Leg das Passwort in einer .env-Datei neben der Compose-Datei ab, nicht direkt im YAML:
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .env
Bring the stack up, then create the admin account from the CLI:Fahre den Stack hoch und lege danach den Admin-Account über die CLI an:
docker compose up -d
docker compose exec -u git gitea gitea admin user create --username <you> --password '<strong-password>' --email [email protected] --admin
GITEA__security__INSTALL_LOCK=true above skips Gitea's /install wizard — without it, that page sits open and unauthenticated until you finish it by hand. GITEA__service__DISABLE_REGISTRATION=true matters too: Gitea's default is open self-registration.GITEA__security__INSTALL_LOCK=true oben überspringt Giteas /install-Assistenten — ohne diese Einstellung bleibt die Seite offen und unauthentifiziert stehen, bis du sie von Hand abschließt. GITEA__service__DISABLE_REGISTRATION=true ist ebenso wichtig: Giteas Standardeinstellung ist offene Selbstregistrierung.
git.example.com {
reverse_proxy 127.0.0.1:3000
}
That is the whole file. Caddy requests and renews the certificate for this hostname automatically as soon as it starts or reloads with this config, provided the A record already resolves to the machine — point DNS first, let it settle, and only then start Caddy.Das ist die ganze Datei. Caddy fordert das Zertifikat für diesen Hostnamen automatisch an und erneuert es, sobald es mit dieser Konfiguration startet oder neu lädt — vorausgesetzt, der A-Eintrag zeigt bereits auf die Maschine. Richte zuerst das DNS ein, lass es sich setzen, und starte erst danach Caddy.
Ports 80 and 443 aren't guaranteed forwarded either — some plans hand them to you by default, some don't, and Caddy needs at least one reachable to get a certificate. Check what your plan forwards before pointing DNS at the box; NAT IPv4, ports and forwarding is the same page the SSH story below sends you to.Auch die Ports 80 und 443 sind nicht garantiert weitergeleitet — manche Pläne geben sie dir standardmäßig, manche nicht, und Caddy braucht mindestens einen erreichbaren, um ein Zertifikat zu bekommen. Prüfe, was dein Plan weiterleitet, bevor du DNS auf die Maschine zeigen lässt; NAT IPv4, Ports und Weiterleitung ist dieselbe Seite, auf die dich die SSH-Geschichte weiter unten verweist.
This is the part every other Gitea write-up glosses over. Your VPS almost certainly reaches SSH on a dedicated, non-standard port already — that is how sshd is exposed under NAT IPv4, and it is not something you can also hand to a container. Gitea's own SSH server needs a second forwarded port pointed at it, mapped to the container's internal port 22.Das ist der Teil, über den jede andere Gitea-Anleitung hinweggeht. Dein VPS erreicht SSH so gut wie sicher schon über einen dedizierten, nicht standardmäßigen Port — so wird sshd unter NAT-IPv4 freigegeben, und den kannst du nicht auch noch an einen Container weitergeben. Giteas eigener SSH-Server braucht einen zweiten weitergeleiteten Port, der auf ihn zeigt, gemappt auf den internen Port 22 des Containers.
Concretely: your provider gives you a small number of forwarded ports besides the one used for host SSH. Say the next is 41023. You map it in compose as "41023:22" and set GITEA__server__SSH_PORT=41023 to match — but keep GITEA__server__SSH_LISTEN_PORT=22 right next to it, the way the compose file above already does. SSH_PORT only controls what Gitea advertises in clone URLs; SSH_LISTEN_PORT controls what it actually listens on inside the container, and it quietly defaults to whatever SSH_PORT is set to. Change one without pinning the other to 22, and Gitea's internal listener moves off port 22 too — nothing answers your "41023:22" mapping anymore, so git-over-SSH fails outright, connection refused, not just a wrong URL in the UI.Konkret: Dein Provider gibt dir eine kleine Anzahl weitergeleiteter Ports zusätzlich zu dem, der für Host-SSH genutzt wird. Sagen wir, der nächste ist 41023. Du mappst ihn in Compose als "41023:22" und setzt passend GITEA__server__SSH_PORT=41023 — aber lässt GITEA__server__SSH_LISTEN_PORT=22 direkt daneben stehen, so wie es die Compose-Datei oben bereits tut. SSH_PORT bestimmt nur, was Gitea in Clone-URLs ankündigt; SSH_LISTEN_PORT bestimmt, worauf es innerhalb des Containers tatsächlich lauscht, und es übernimmt still den Wert von SSH_PORT, wenn nichts anderes gesetzt ist. Änderst du eines, ohne das andere auf 22 festzunageln, wandert auch Giteas interner Listener von Port 22 weg — auf dein "41023:22"-Mapping antwortet dann nichts mehr, sodass Git-over-SSH komplett fehlschlägt, Connection refused, nicht bloß eine falsche URL in der Oberfläche.
If you would rather not deal with a second forwarded port at all, git push/git clone over HTTPS with a personal access token works identically and needs nothing beyond the port Caddy already uses. Generate the token under Settings → Applications in Gitea's UI, and use it as the password when the CLI or credential helper asks. This is the honest fallback, not a downgrade — plenty of people run Gitea for years on HTTPS-only clones and never touch the SSH port.Willst du dich gar nicht erst mit einem zweiten weitergeleiteten Port befassen, funktioniert git push/git clone über HTTPS mit einem Personal Access Token identisch und braucht nichts außer dem Port, den Caddy ohnehin schon nutzt. Erzeuge das Token unter Einstellungen → Anwendungen in Giteas Oberfläche und verwende es als Passwort, wenn die CLI oder der Credential-Helper danach fragt. Das ist die ehrliche Alternative, keine Notlösung — jede Menge Leute betreiben Gitea jahrelang nur mit HTTPS-Clones und fassen den SSH-Port nie an.
Worth reading first: NAT IPv4 vs a dedicated IP and NAT IPv4, ports and forwarding.Lesenswert vorab: NAT IPv4 vs. dedizierte IP und NAT IPv4, Ports und Weiterleitung.
Gitea Actions speaks most of the GitHub Actions workflow syntax, so .gitea/workflows/*.yml files using common actions mostly just work. It ships disabled; turn it on with GITEA__actions__ENABLED=true and restart.Gitea Actions spricht den Großteil der GitHub-Actions-Workflow-Syntax, sodass .gitea/workflows/*.yml-Dateien mit gängigen Actions meistens einfach funktionieren. Es ist standardmäßig deaktiviert; schalte es mit GITEA__actions__ENABLED=true ein und starte neu.
Actions itself does no building — it dispatches jobs to act_runner, a separate process that registers against your instance and polls for work. Generate a registration token from inside the running container:Actions selbst baut nichts — es verteilt Jobs an act_runner, einen separaten Prozess, der sich bei deiner Instanz registriert und nach Arbeit pollt. Erzeuge ein Registrierungstoken aus dem laufenden Container heraus:
docker compose exec -u git gitea gitea actions generate-runner-token
Copy the printed token into .env as RUNNER_TOKEN=<token> — the runner block below reads it from there, and an empty value means a runner that never registers.Kopiere das ausgegebene Token in die .env als RUNNER_TOKEN=<token> — der Runner-Block unten liest es von dort, und ein leerer Wert bedeutet einen Runner, der sich nie registriert.
then point a runner at it:dann richte einen Runner darauf aus:
runner:
image: gitea/act_runner:0.2.11 # check gitea.com/gitea/act_runner's tags for the current release
restart: unless-stopped
environment:
- GITEA_INSTANCE_URL=https://git.example.com
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
- GITEA_RUNNER_NAME=vps-runner
volumes:
- runner_data:/data
- /var/run/docker.sock:/var/run/docker.sock
Add runner_data: to that same volumes: block too (and db_data:, if you added Postgres).Füge runner_data: ebenfalls in denselben volumes:-Block hinzu (und db_data:, falls du Postgres ergänzt hast).
That last mount is the thing to be honest about: giving the runner the Docker socket gives it effective root on the host, since it can start a container with the host filesystem mounted in. On a box that only runs Gitea and its own private repos, that's a reasonable trade. a self-hosted CI runner on a VPS covers sizing and isolation if your builds get heavier, or you want the runner on its own machine.Bei diesem letzten Mount solltest du ehrlich sein: Gibst du dem Runner den Docker-Socket, gibst du ihm effektiv Root auf dem Host, weil er einen Container mit eingehängtem Host-Dateisystem starten kann. Auf einer Maschine, die nur Gitea und seine eigenen privaten Repos betreibt, ist das ein vertretbarer Kompromiss. Ein selbst gehosteter CI-Runner auf einem VPS behandelt Dimensionierung und Isolation, falls deine Builds schwerer werden oder du den Runner auf einer eigenen Maschine haben willst.
Gitea ships its own backup command, and it's the one to use — it snapshots the database, repos, hooks, and configuration together, instead of leaving you to reconstruct which copies were consistent with each other.Gitea bringt seinen eigenen Backup-Befehl mit, und das ist der, den du benutzen solltest — er erstellt einen Snapshot von Datenbank, Repos, Hooks und Konfiguration zusammen, statt dich rekonstruieren zu lassen, welche Kopien zueinander konsistent waren.
docker compose exec -u git gitea gitea dump -c /data/gitea/conf/app.ini
Run without a Postgres service attached, this also captures the SQLite file. The resulting zip lands inside the container's data directory; copy it off before it does anyone any good:Ohne angeschlossenen Postgres-Service erfasst das auch die SQLite-Datei. Das entstehende Zip landet im Datenverzeichnis des Containers; kopiere es herunter, bevor es irgendjemandem nützt:
DUMP=$(docker compose exec -T gitea sh -c 'ls -t /data/gitea/gitea-dump-*.zip | head -n1')
docker cp "$(docker compose ps -q gitea):$DUMP" "./gitea-dump-$(date +%F).zip"
Copy that file off the VPS entirely — a dump on the same disk as the instance it came from is a file, not a backup. back up your VPS covers what off-machine backup means in practice. If you moved to Postgres, the dump still includes a database export, so a separate pg_dump is only needed for a faster point-in-time restore of a large database on its own.Kopiere diese Datei vollständig vom VPS herunter — ein Dump auf derselben Platte wie die Instanz, von der er stammt, ist eine Datei, kein Backup. Backup deines VPS beschreibt, was Backup abseits der Maschine in der Praxis bedeutet. Bist du zu Postgres gewechselt, enthält der Dump trotzdem einen Datenbank-Export, sodass ein separater pg_dump nur für eine schnellere Point-in-Time-Wiederherstellung einer großen Datenbank für sich allein nötig ist.
docker compose pull
docker compose up -d
Bump the pinned tag deliberately, read the release notes for the version you're jumping to, and take a gitea dump immediately beforehand — Gitea runs migrations automatically on first start after an upgrade, and a migration you can roll back from is one where the backup came ten minutes earlier, not one you're improvising after the fact.Erhöhe den gepinnten Tag bewusst, lies die Release Notes zu der Version, auf die du springst, und mach unmittelbar vorher ein gitea dump — Gitea führt Migrationen automatisch beim ersten Start nach einem Upgrade aus, und eine Migration, von der du zurückrollen kannst, ist eine, bei der das Backup zehn Minuten vorher entstanden ist, nicht eine, die du hinterher improvisierst.
Full disclosure: this is what we sell. If you want the repos without the sysadmin, the managed Gitea container comes with its own hostname and certificate — you get the app and a URL, not a root shell.Zur vollen Transparenz: Das ist, was wir verkaufen. Willst du die Repos ohne die Systemadministration, kommt der gemanagte Gitea-Container mit eigenem Hostnamen und Zertifikat — du bekommst die App und eine URL, keine Root-Shell.
One-click apps — EUR 4 to EUR 12 a month, hosted in Germany (EU). Eight apps: n8n, Uptime Kuma, Vaultwarden, Gitea, Nextcloud, Ghost, Managed WordPress, Private AI Chat. Each customer gets an isolated Docker network and volume, plus a hostname under apps.overnight.host on a real wildcard certificate. Memory and CPU are capped per plan by the container runtime.One-Click-Apps — 4 bis 12 EUR im Monat, gehostet in Deutschland (EU). Acht Apps: n8n, Uptime Kuma, Vaultwarden, Gitea, Nextcloud, Ghost, Managed WordPress, Private AI Chat. Jeder Kunde bekommt ein isoliertes Docker-Netzwerk und -Volume sowie einen Hostnamen unter apps.overnight.host mit einem echten Wildcard-Zertifikat. Arbeitsspeicher und CPU sind je Plan durch die Container-Runtime gedeckelt.
You order in the shop, pay by card (Stripe) or SEPA bank transfer, and your login details are e-mailed to you once the service is set up. Support is e-mail, run by one person, with no guaranteed response time. All prices are final totals under the German small-business rule (§19 UStG); no VAT is added or shown.Du bestellst im Shop, zahlst per Karte (Stripe) oder SEPA-Überweisung, und deine Zugangsdaten werden dir per E-Mail zugeschickt, sobald der Dienst eingerichtet ist. Support läuft per E-Mail, von einer einzelnen Person betrieben, ohne garantierte Reaktionszeit. Alle Preise sind Endpreise. Gemäß § 19 UStG wird keine Umsatzsteuer ausgewiesen.
Order one-click-gitea → · One-click apps overviewOne-Click-Gitea bestellen → · Übersicht One-Click-Apps
Written by the person who runs overnight.host: a small, honest hosting company on dedicated bare metal — Linux VPS, game servers, web hosting. Live status at up.overnight.host.Geschrieben von der Person, die overnight.host betreibt: ein kleines, ehrliches Hosting-Unternehmen auf dedizierter Bare-Metal-Hardware — Linux-VPS, Gameserver, Webhosting. Live-Status unter up.overnight.host.
For a small team it is a real, supported option, not a toy — Gitea's SQLite backend handles a handful of concurrent users comfortably. Move to Postgres once several people are pushing at once, or you're running Actions on the same box — both add write concurrency SQLite wasn't designed for.Für ein kleines Team ist es eine echte, unterstützte Option, kein Spielzeug — Giteas SQLite-Backend kommt mit einer Handvoll gleichzeitiger Nutzer problemlos zurecht. Wechsle zu Postgres, sobald mehrere Leute gleichzeitig pushen oder du Actions auf derselben Maschine betreibst — beides bringt Schreibnebenläufigkeit mit sich, für die SQLite nicht ausgelegt ist.
Because it is already in use by sshd for logging into the machine itself, and one port cannot be forwarded to two different listeners. Gitea's own SSH server needs its own forwarded port, mapped to the container, with SSH_PORT set to match for correct clone URLs and SSH_LISTEN_PORT pinned to 22 so the container's own listener still matches that mapping.Weil er bereits von sshd genutzt wird, um sich bei der Maschine selbst anzumelden, und ein Port nicht an zwei verschiedene Listener weitergeleitet werden kann. Giteas eigener SSH-Server braucht seinen eigenen weitergeleiteten Port, gemappt auf den Container, wobei SSH_PORT passend gesetzt sein muss für korrekte Clone-URLs und SSH_LISTEN_PORT auf 22 festgenagelt bleibt, damit der eigene Listener des Containers weiterhin zu diesem Mapping passt.
No. A second forwarded port for Gitea's SSH server is enough, and if your provider doesn't hand you a spare one, HTTPS with a personal access token works identically with no extra port at all. A dedicated IPv4 only matters if you want a standard port number instead; on our plans that's arranged by e-mail.Nein. Ein zweiter weitergeleiteter Port für Giteas SSH-Server reicht, und falls dein Provider dir keinen übrigen gibt, funktioniert HTTPS mit einem Personal Access Token identisch, ganz ohne zusätzlichen Port. Eine dedizierte IPv4 ist nur relevant, wenn du stattdessen eine Standard-Portnummer willst; bei unseren Plänen wird das per E-Mail geregelt.
On the same VPS is fine at small scale, as long as you budget the RAM for it and accept the runner's Docker socket access as host-level power. Once builds get heavy, or you want a boundary between the git server and whatever your CI jobs execute, move the runner to its own box — a self-hosted CI runner on a VPS walks through sizing and isolation for that case.Auf demselben VPS ist im kleinen Maßstab in Ordnung, solange du den RAM dafür einplanst und den Docker-Socket-Zugriff des Runners als Macht auf Host-Ebene akzeptierst. Werden die Builds aufwendiger, oder willst du eine Grenze zwischen dem Git-Server und dem, was deine CI-Jobs ausführen, zieh den Runner auf eine eigene Maschine um — Ein selbst gehosteter CI-Runner auf einem VPS geht Dimensionierung und Isolation für diesen Fall durch.
gitea dump actually back up?Was sichert gitea dump eigentlich?The database (including a SQLite file, if that's what you're running), the repository data, custom configuration, hooks, and logs, bundled into one zip with -c pointing at your app.ini — the one command that guarantees the pieces are consistent with each other, which copying files by hand doesn't.Die Datenbank (einschließlich SQLite-Datei, falls du die betreibst), die Repository-Daten, benutzerdefinierte Konfiguration, Hooks und Logs, gebündelt in einem Zip, wobei -c auf deine app.ini zeigt — der eine Befehl, der garantiert, dass die Teile zueinander konsistent sind, was manuelles Kopieren von Dateien nicht leistet.
Prices are final totals; no VAT is shown (§19 UStG). Need something the shop does not list? Email us for a written offer.Alle Preise sind Endpreise ohne ausgewiesene USt. (§19 UStG). Du brauchst etwas, das nicht im Shop steht? Schreib uns für ein schriftliches Angebot.
Order now →Jetzt bestellen → Request a custom configIndividuelle Konfiguration anfragen