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.
# 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_keysDer ö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:
Host coderconda
HostName 203.0.113.10
User damjan
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesDanach ist der ganze Zugang ein Wort:
ssh codercondaPhase 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
sudo nano /etc/ssh/sshd_config.d/99-hardening.confPermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers damjanRoot-Anmeldung aus, Passwortanmeldung vollständig aus, ein einziger Benutzer zugelassen. Vor dem Neustart die Syntax prüfen:
sudo sshd -t
sudo systemctl restart sshDieses 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.
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 enableDrei Ports offen, alles andere zu.
fail2ban
sudo apt install fail2ban
sudo systemctl enable --now fail2banKonfiguriert 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
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgradesDDoS-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.
# nach der Installation sicherstellen, dass claude im PATH liegt
which claude
claude --versionAuf 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:
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:
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:
sudo certbot --nginxHier 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:
git remote set-url origin https://git.damjan-savic.com/damjan/portfolio.git
git pull --rebase
git pushDer 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:
cd ~ && git clone https://git.damjan-savic.com/damjan/portfolio.git
cd portfolio
corepack prepare pnpm@latest --activate
pnpm install --frozen-lockfile
pnpm buildZu 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:
pm2 start .next/standalone/server.js --name portfolio
pm2 save
pm2 logs portfolioUnd wieder nginx, wieder über Claude Code:
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:
- Den A-Record löschen, der auf Vercel zeigt.
- Den
www-CNAME aufcname.vercel-dns.comlöschen. - Einen A-Record auf die Server-IP anlegen.
- Einen zweiten A-Record für
wwwauf dieselbe IP.
Dann warten und prüfen, statt zu raten:
dig damjan-savic.com +shortBei 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
- 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.
- certbot scheitert mit einem Challenge-Fehler. Dein DNS ist noch nicht propagiert. Mit
digprüfen, warten, noch einmal laufen lassen. Sonst ist nichts kaputt. - 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 mit127.0.0.1:beginnen. - 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:
- Dienst auf einem lokalen Port laufen lassen
- an
127.0.0.1binden - per nginx als Reverse Proxy davorhängen
- Subdomain eintragen
- 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.

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.



