---
title: "Von Vercel und GitHub auf einen 30-Euro-Rootserver: sieben Phasen, unter einer Stunde"
date: 2026-08-02
author: "Damjan Savić"
canonical: https://damjan-savic.com/de/wissen/vercel-to-root-server
language: de
keywords: "Self-Hosting, Infrastruktur, Docker, Claude Code, Next.js, Gitea"
video: https://www.youtube.com/watch?v=HAgdRl5VfOc
---
# Von Vercel und GitHub auf einen 30-Euro-Rootserver: sieben Phasen, unter einer Stunde

**Kurz gesagt** — Ein eigener Server mit acht dedizierten Kernen und 16 GB ECC-RAM kostet 29,61 Euro im Monat ohne Vertragsbindung und ist in unter einer Stunde aufgesetzt. Sieben Phasen, und danach gehört die gesamte Deployment-Kette mir statt zwei Unternehmen, über die ich keinerlei Kontrolle habe.

---

Ein Server, acht dedizierte CPU-Kerne, 16 Gigabyte ECC-RAM, eigenes Git-Hosting, eigenes Website-Hosting und Claude Code direkt auf der Maschine. 29,61 Euro im Monat, keine Vertragsbindung, aufgesetzt in unter einer Stunde.

Vorher lief mein Portfolio auf Vercel, der Code lag auf GitHub. Beides funktioniert gut. Nur war meine Deployment-Kette damit über zwei Unternehmen verteilt, über die ich keinerlei Kontrolle habe. Vercel Pro kostet 20 Dollar im Monat, sobald man über die Hobby-Grenzen hinaus ist, GitHub je nach Plan weitere 4 bis 21 Dollar. Und wenn eines der beiden seine Preise, seine Bedingungen oder seine Build-Limits ändert, erfahre ich das per E-Mail.

Dazu kommt ein zweiter Punkt, der weniger nach Prinzip klingt und im Alltag mehr wiegt: Wenn ich etwas auf einem echten Server testen will, brauche ich einen echten Server.

## Die Maschine

NetCup RS 2000 G12:

| | |
| --- | --- |
| CPU | AMD EPYC 9645, 8 dedizierte Kerne — nicht geteilt |
| RAM | 16 GB DDR5 ECC |
| Speicher | 512 GB NVMe an einem Hardware-RAID-Controller |
| Traffic | Flatrate |
| Standort | Nürnberg, Deutschland |
| Preis | 29,61 Euro im Monat inklusive 19 Prozent Mehrwertsteuer |

Zwei ehrliche Anmerkungen zum Preis. Erstens nehme ich die Laufzeit von einem Monat: einen Monat Mindestlaufzeit, monatliche Abrechnung, jederzeit kündbar. Wer sich auf zwölf Monate festlegt, bekommt 17 Prozent Rabatt — also etwa 25 Euro im Monat, aber im Voraus für ein Jahr abgerechnet, rund 300 Euro auf einmal. Die fünf Euro Aufschlag im Monat zahle ich für die Beweglichkeit.

Zweitens gibt es bei Erstbestellungen eine 30-tägige Zufriedenheitsgarantie. Wer mitmacht und es hasst, bekommt sein Geld zurück.

Beim Standort lohnt der genaue Blick: „Europa ohne Präferenz" ist etwa vier Euro günstiger, dann landet die Maschine aber in Österreich, Deutschland oder den Niederlanden und man hat keine Wahl. Ich wollte Nürnberg, also zahle ich für Nürnberg.

## Phase 1 · Bestellen

Nach der Bestellung stolpern die meisten über dasselbe: Bei NetCup gibt es **zwei** Oberflächen mit getrennten Zugangsdaten, die beide per E-Mail kommen.

- **CCP**, das Customer Control Panel: Verträge, Rechnungen, Upgrades.
- **SCP**, das Server Control Panel: die eigentliche Maschine.

Gebraucht wird das SCP. Dort unter „Server" die Maschine wählen, „Medien / Images", Ubuntu 22.04.5 auswählen, in den Einstellungen einen zusätzlichen Benutzer anlegen lassen und die Installation starten. Am Ende zeigt NetCup das Root-Passwort. Kopieren — man braucht es in genau einer Minute und danach nie wieder.

Zwei Dinge noch, bevor es weitergeht, und beide sparen später Zeit:

**Ein Snapshot auf der sauberen Installation.** In Phase 3 wird die SSH-Konfiguration verändert, und wenn das schiefgeht, will ich einen Rückweg von dreißig Sekunden statt einer Neuinstallation.

**Der DNS-Eintrag, jetzt.** Ein A-Record für die Subdomain — bei mir `git.damjan-savic.com` — auf die neue Server-IP, mit der kleinstmöglichen TTL, also einer Minute. Das Portfolio läuft zu diesem Zeitpunkt noch auf Vercel und soll keine Ausfallzeit haben. DNS-Propagation dauert, also stoße ich sie an und lasse sie im Hintergrund laufen, während ich alles andere mache.

## Phase 2 · Benutzer

Erste Anmeldung als `root` mit dem Passwort aus Phase 1. Als root zu arbeiten ist eine schlechte Angewohnheit, also entsteht sofort ein richtiger Benutzer mit Passwort und `sudo`-Rechten.

Dann der SSH-Schlüssel. Anders als bei manchen Anbietern legt NetCup den öffentlichen Schlüssel nicht bei der Bestellung ab — man startet mit einem Passwort. Das wird als Nächstes behoben.

```bash
# Auf dem eigenen Rechner, falls noch kein Schlüssel existiert
ssh-keygen -t ed25519 -C "damjan@coderconda"

# Auf dem Server
mkdir -p ~/.ssh && chmod 700 ~/.ssh
sudo apt update && sudo apt install nano
nano ~/.ssh/authorized_keys        # den öffentlichen Schlüssel als eine Zeile einfügen
chmod 600 ~/.ssh/authorized_keys
```

Der öffentliche Schlüssel liegt danach auf dem Server, der private auf meinem Rechner.

Und dann der Teil, der den Alltag wirklich verändert: Ich will keine IP-Adresse mehr tippen. Auf dem eigenen Rechner — unter Windows in `C:\Users\<Name>\.ssh\config` — kommt ein Block dazu:

```text
Host coderconda
  HostName 203.0.113.10
  User damjan
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
```

Danach ist der ganze Zugang ein Wort:

```bash
ssh coderconda
```

## Phase 3 · Härten

Diese Maschine hat eine öffentliche IP und eine funktionierende Passwortanmeldung. Etwa zehn Minuten nach dem Livegang fangen Bots an, an Port 22 zu klopfen. Das ist keine Paranoia, das ist einfach das Internet.

### SSH

```bash
sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
```

```text
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers damjan
```

Root-Anmeldung aus, Passwortanmeldung vollständig aus, ein einziger Benutzer zugelassen. Vor dem Neustart die Syntax prüfen:

```bash
sudo sshd -t
sudo systemctl restart ssh
```

**Dieses Terminal nicht schließen.** Ein zweites Fenster öffnen und dort die Anmeldung testen. Wenn sie klappt, ist alles gut. Wenn nicht, hat man noch die offene Sitzung, um es zurückzunehmen. Und wer beide Fenster trotzdem zumacht, hat den Snapshot aus Phase 1 und die Remote-Konsole im SCP — die funktioniert auch dann, wenn SSH nicht mehr funktioniert.

Bei mir ist genau das passiert: Ich hatte im `AllowUsers` zunächst den falschen Namen stehen und bin ausgesperrt worden. Genau dafür ist das zweite Fenster da.

### Die Firewall

Hier liegt ein echter Unterschied zu manchen anderen Anbietern: NetCup gibt dir **keine** getrennte Firewall auf Netzwerkebene, die man zusammenklickt. Deine Firewall ist die auf der Maschine. Dieser Schritt ist hier also nicht optional.

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```

Drei Ports offen, alles andere zu.

### fail2ban

```bash
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
```

Konfiguriert auf eine Stunde Sperre nach drei fehlgeschlagenen Anmeldeversuchen. Was im Video passiert ist: fail2ban hat die IP-Adresse meines eigenen Rechners gesperrt, und ich musste über die SCP-Konsole hinein, um sie wieder freizugeben. Auch das ist ein Argument für die Remote-Konsole.

### Auto-Updates

```bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
```

### DDoS-Schutz

DDoS-Schutz ist bei NetCup auf jedem Rootserver enthalten, ohne Aufpreis, mit Filterkapazität bis in den Bereich von zwei Terabit pro Sekunde. Das ist die Netzwerkebene. Die HTTP-Ebene kommt später in Phase 7.

## Phase 4 · Claude Code

Das ist der Teil, der den Rest schnell macht. Statt nginx-Konfigurationen von Hand zu schreiben, installiere ich Claude Code auf dem Server und lasse es die Arbeit machen.

```bash
# nach der Installation sicherstellen, dass claude im PATH liegt
which claude
claude --version
```

Auf einem Server ohne Oberfläche gibt es keinen Browser. Die Anmeldung gibt deshalb eine URL aus, die man auf dem eigenen Rechner öffnet, wo man ohnehin angemeldet ist; den zurückgegebenen Code fügt man im Terminal ein.

Ein Detail, das ins Bild gehört: Mitten in dieser Phase kam ein `529 overloaded` zurück. Der Statusseite nach gab es zu dem Zeitpunkt erhöhte Fehlerraten über alle Modelle, seit etwa zwei Stunden bekannt. Also Pause, danach lief derselbe Prompt durch. Wer eine Migration plant, sollte einkalkulieren, dass die Werkzeuge in der Kette auch mal nicht da sind.

## Phase 5 · Gitea

Gitea ist ein selbst gehosteter Git-Dienst. GitHub, aber deiner, und er läuft in etwa 200 Megabyte RAM. Ich nehme Docker, weil das der schnellste Weg hin und der sauberste Weg wieder weg ist.

In der Compose-Datei steht die wichtigste Zeile des ganzen Videos:

```yaml
ports:
  - '127.0.0.1:3000:3000'
```

Nicht `3000:3000`. Der Grund: **Docker schreibt seine eigenen iptables-Regeln und geht damit an UFW vorbei.** Wer einen Port auf dem gewohnten Weg veröffentlicht, dessen Firewall hält ihn nicht auf. Und auf diesem Server ist UFW die einzige Firewall, die es gibt — diese eine Zeile wiegt hier also mehr als anderswo. Die Bindung an localhost bedeutet, dass nur nginx auf derselben Maschine herankommt.

Danach nginx und certbot, und hier lasse ich Claude Code arbeiten:

```text
Installiere nginx und certbot. Erstelle eine nginx-Site für
git.damjan-savic.com, die als Reverse Proxy auf 127.0.0.1:3000 zeigt,
mit client_max_body_size 512M und den üblichen Proxy-Headern.
Aktiviere sie, prüfe die Konfiguration, lade nginx neu.
```

Eine Einschränkung, die dabei auffiel: `sudo` kann in dieser Umgebung nicht nach einem Passwort fragen. Die Installationsbefehle mussten in einem echten Terminalfenster laufen, danach konnte Claude Code weitermachen, die Installation prüfen und die Konfiguration schreiben. Ich habe an der Stelle nebenbei ChatGPT benutzt, um die Befehle in eine Zeile zu formatieren — auch das gehört zur ehrlichen Fassung.

Dann das Zertifikat:

```bash
sudo certbot --nginx
```

Hier zahlt sich Phase 1 aus: Der DNS-Eintrag von vorhin ist inzwischen propagiert, also läuft die Challenge sofort durch. Anschließend `git.damjan-savic.com` im Browser öffnen, SQLite als Datenbank bestätigen, Pfade lassen wie sie sind, Seitentitel setzen und das Administratorkonto anlegen. Eigener Git-Server, fertig.

## Phase 6 · Umzug

Gitea bringt ein Migrationswerkzeug mit, das ist einfacher als es klingt. Man braucht ein GitHub-Token (Settings → Developer Settings → Personal Access Tokens → Tokens (classic)), dann in Gitea „New Migration", GitHub wählen, Token eintragen, Besitzer und Repository-Namen setzen, Issues, Labels und Releases anhaken.

Was danach dasteht, ist nicht eine Kopie des letzten Stands, sondern das Repository: die vollständige Commit-Historie, alle Branches, alle Issues.

Eine Entscheidung gibt es dabei: das Häkchen „This repository will be a mirror". Gesetzt, bleibt GitHub die Quelle der Wahrheit und Gitea folgt nur. Ich habe es weggelassen, weil Gitea ab jetzt primär sein soll.

Auf dem eigenen Rechner also:

```bash
git remote set-url origin https://git.damjan-savic.com/damjan/portfolio.git
git pull --rebase
git push
```

Der erste Push wurde abgelehnt — `fetch first`. Also erst ziehen, dann schieben. Danach schiebt mein Rechner auf meinen eigenen Server.

## Phase 7 · Live

Auf dem Server klonen, Abhängigkeiten installieren, bauen:

```bash
cd ~ && git clone https://git.damjan-savic.com/damjan/portfolio.git
cd portfolio
corepack prepare pnpm@latest --activate
pnpm install --frozen-lockfile
pnpm build
```

Zu reparieren gab es dabei die Konfiguration, die auf Vercel gezeigt hatte — ein Projekt, das dort gebaut wurde, hat Annahmen darin stehen, die auf einer nackten Maschine nicht mehr gelten.

Dann läuft es als Node-Prozess hinter nginx, verwaltet von PM2, auf Port 3001 — 3000 ist von Gitea belegt:

```bash
pm2 start .next/standalone/server.js --name portfolio
pm2 save
pm2 logs portfolio
```

Und wieder nginx, wieder über Claude Code:

```text
Erstelle eine nginx-Site für damjan-savic.com und www.damjan-savic.com,
die auf 127.0.0.1:3001 proxyt. Lege im http-Block eine limit_req_zone mit
20 Requests pro Sekunde an und wende sie im location-Block mit burst=40
nodelay an. Ergänze gzip und die üblichen Security-Header.
```

Claude hat die Konfiguration angelegt und dabei drei Punkte ausdrücklich als offene Entscheidungen zurückgegeben statt sie stillschweigend zu treffen: kein TLS-Block, weil nicht gefragt; die Ratenbegrenzung deckt auch die statischen Dateien unter `/_next/static` ab; und die Weiterleitung zwischen `www` und Apex fehlt noch, beide Hostnamen liefern gerade denselben Inhalt.

Das ist die Ratenbegrenzung auf Anwendungsebene. NetCup fängt die Flut auf Netzwerkebene ab, das hier fängt die HTTP-Flut ab. Ein ernsthaftes Botnetz hält es nicht auf, ein einzelnes Skript, das den Server hämmert, schon.

### Der Umschalter

Das ist der einzige Moment, in dem die Seite wirklich auf dem Spiel steht, also geht es schnell. Beim DNS-Anbieter:

1. Den A-Record löschen, der auf Vercel zeigt.
2. Den `www`-CNAME auf `cname.vercel-dns.com` löschen.
3. Einen A-Record auf die Server-IP anlegen.
4. Einen zweiten A-Record für `www` auf dieselbe IP.

Dann warten und prüfen, statt zu raten:

```bash
dig damjan-savic.com +short
```

Bei mir war `www` bereits propagiert, während die Domain ohne `www` noch auf Vercel zeigte — ein einzelner alter Eintrag, den ich übersehen hatte. Ein paar Minuten später stimmte auch der. Danach certbot noch einmal laufen lassen, und die Seite steht: eigener Server, eigenes Zertifikat, eigene Git-Historie. Das Projekt bei Vercel kann weg.

## Vier Fehlerbilder

1. **Du hast dich per SSH ausgesperrt.** SCP öffnen und die Remote-Konsole nutzen — sie funktioniert auch, wenn SSH es nicht tut. Wenn auch das nichts hilft: den Snapshot aus Phase 1 zurückspielen.
2. **certbot scheitert mit einem Challenge-Fehler.** Dein DNS ist noch nicht propagiert. Mit `dig` prüfen, warten, noch einmal laufen lassen. Sonst ist nichts kaputt.
3. **Ein Docker-Container ist aus dem Internet erreichbar, obwohl UFW den Port geschlossen meldet.** Das ist die iptables-Sache von oben. Jeden `ports`-Eintrag in jeder Compose-Datei prüfen: Er muss mit `127.0.0.1:` beginnen.
4. **Traffic-Drosselung.** Die Flatrate ist echt, aber jenseits von etwa 3 TB in 24 Stunden wird auf 300 Mbit gedrosselt. Für eine Portfolio-Seite ist das kein Thema.

## Das Muster

Sobald man es einmal gesehen hat, kann alles Weitere auf denselben Server. Die Struktur ist immer dieselbe:

1. Dienst auf einem lokalen Port laufen lassen
2. an `127.0.0.1` binden
3. per nginx als Reverse Proxy davorhängen
4. Subdomain eintragen
5. certbot laufen lassen

Gitea auf 3000, Portfolio auf 3001, das Nächste auf 3002.

Dieselben fünf Schritte, jedes Mal. Mit 16 Gigabyte RAM ist reichlich Platz. Und jeder einzelne dieser Schritte ist etwas, das Claude Code für dich schreiben kann, sobald du Port und Domain nennst.

![Diagramm · Fünf Schritte, die jeder weitere Dienst auf derselben Maschine wiederholt](/media/website/article/figures/vercel-to-root-server-pattern.avif)

## Was noch fehlt

Ehrlich gesagt: das automatische Deployment. Im Moment muss ich noch von Hand ziehen und neu bauen. Ein Gitea-Webhook und ein Shell-Skript mit fünf Zeilen erledigen das. Danach wären Uptime Kuma für die Überwachung, automatische Snapshots, n8n, eine Postgres-Instanz oder Cloudflare davor die naheliegenden nächsten Schritte.

## Fazit

Selbst hosten ist nicht mehr schwer. Es ist nur ungewohnt. Sieben Phasen, ein Server, 30 Euro im Monat, keine Vertragsbindung — und die gesamte Kette gehört mir.

## Kapitel der Aufnahme

- [0:48 — Die Maschine und der Preis](https://youtu.be/HAgdRl5VfOc?t=48)
- [2:23 — Phase 1: Bestellung, Ubuntu, Snapshot, DNS](https://youtu.be/HAgdRl5VfOc?t=143)
- [4:44 — Phase 2: Benutzer und SSH-Schlüssel](https://youtu.be/HAgdRl5VfOc?t=284)
- [7:57 — Phase 3: Härten, Firewall, fail2ban](https://youtu.be/HAgdRl5VfOc?t=477)
- [11:11 — Phase 4: Claude Code auf dem Server](https://youtu.be/HAgdRl5VfOc?t=671)
- [12:49 — Phase 5: Gitea, Docker und die 127.0.0.1-Zeile](https://youtu.be/HAgdRl5VfOc?t=769)
- [18:28 — Phase 6: Das Repository umziehen](https://youtu.be/HAgdRl5VfOc?t=1108)
- [20:53 — Phase 7: Build, PM2, nginx, DNS-Umschaltung](https://youtu.be/HAgdRl5VfOc?t=1253)
- [27:15 — Vier Fehlerbilder](https://youtu.be/HAgdRl5VfOc?t=1635)

## Häufige Fragen

### Warum muss in der Compose-Datei 127.0.0.1:3000:3000 stehen?

Weil Docker seine eigenen iptables-Regeln schreibt und damit an UFW vorbeigeht. Wer einen Port auf dem gewohnten Weg veröffentlicht, dessen Firewall hält ihn nicht auf — und auf diesem Server ist UFW die einzige Firewall, die es gibt. Die Bindung an localhost bedeutet, dass nur nginx auf derselben Maschine herankommt.

### Wie sperrt man sich beim Härten von SSH nicht selbst aus?

Das Terminal nicht schließen. Nach dem Neustart von SSH ein zweites Fenster öffnen und dort die Anmeldung testen; klappt sie nicht, nimmt man es in der noch offenen Sitzung zurück. Dahinter liegen der Snapshot auf der sauberen Installation und die Remote-Konsole im SCP, die auch dann funktioniert, wenn SSH es nicht tut. Mir ist genau das passiert — ein falscher Name in AllowUsers.

### Was ist das Muster für jeden weiteren Dienst auf derselben Maschine?

Fünf Schritte, jedes Mal dieselben: Dienst auf einem lokalen Port laufen lassen, an 127.0.0.1 binden, nginx als Reverse Proxy davorhängen, Subdomain eintragen, certbot laufen lassen. Gitea auf 3000, Portfolio auf 3001, das Nächste auf 3002. Jeder einzelne Schritt ist etwas, das Claude Code schreiben kann, sobald Port und Domain feststehen.

---

Von Vercel und GitHub auf einen 30-Euro-Rootserver: sieben Phasen, unter einer Stunde — https://damjan-savic.com/de/wissen/vercel-to-root-server
KI-generierter Inhalt: https://damjan-savic.com/de/ki-transparenz
© 2026 Damjan Savić. https://damjan-savic.com
