Kurz gesagt
Ein kleiner Server mit 1 vCPU und 1 bis 2 GB RAM reicht für eine kleine App. Docker Compose startet die App und den Webserver Caddy davor. Caddy holt das HTTPS-Zertifikat selbst und erneuert es automatisch, du brauchst keinen certbot-Cronjob. Wichtig: Nur Caddy veröffentlicht Ports, die App nicht.
Diese Anleitung bringt eine App, die schon in Docker läuft, auf einen frischen Ubuntu-Server (24.04 oder 26.04 LTS) und unter deine eigene Domain, mit gültigem HTTPS-Zertifikat.
Was du brauchst
- Einen Server (VPS) mit 1 vCPU und 1 bis 2 GB RAM für eine kleine App. Jeder Anbieter funktioniert; welcher zu dir passt, steht im VPS-Anbieter-Vergleich.
- Eine Domain, deren DNS-Einträge du bearbeiten kannst.
- Eine App, die in Docker läuft und auf einem Port lauscht (hier
3000).
Schritt für Schritt
Domain auf den Server zeigen lassen
Lege einen
A-Eintrag fürapp.deine-firma.demit der IPv4-Adresse des Servers an, bei IPv6 zusätzlich einenAAAA-Eintrag. Mach das zuerst: Caddy bekommt erst ein Zertifikat, wenn die Domain auf den Server zeigt. Prüfen mitdig +short app.deine-firma.de.Server absichern
Eigenen Benutzer anlegen, SSH-Login nur noch mit Schlüssel, Firewall auf die nötigen Ports beschränken.Docker installieren
Über das offizielle Paket-Repository von Docker, danachdocker compose versionprüfen.Compose-Datei und Caddyfile anlegen
Die App ohne veröffentlichte Ports, Caddy davor auf Port 80 und 443.Starten
docker compose up -dund im Caddy-Log verfolgen, wie das Zertifikat ausgestellt wird.
Server absichern
Melde dich einmal als root an, lege einen normalen Benutzer an und gib ihm deinen SSH-Schlüssel:
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployDann Passwort- und root-Login abschalten. Ubuntu liest zuerst die Dateien in /etc/ssh/sshd_config.d/, und bei den meisten Einstellungen gilt der erste gefundene Wert. Eine Datei mit 00- vorne gewinnt deshalb auch gegen Vorgaben, die manche Anbieter-Images dort ablegen:
printf "PasswordAuthentication no\nPermitRootLogin no\n" | sudo tee /etc/ssh/sshd_config.d/00-hardening.conf
sudo sshd -t
sudo systemctl restart ssh.serviceLass die aktuelle Sitzung offen und teste den Login als deploy in einem zweiten Terminal, bevor du dich abmeldest. Sonst sperrst du dich bei einem Tippfehler aus.
Nur die nötigen Ports öffnen:
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enableDocker umgeht ufw
Jeder Port, den du in Compose mit ports: veröffentlichst, ist aus dem Internet erreichbar, auch wenn ufw ihn blockiert. Docker leitet den Verkehr an den
ufw-Regeln vorbei (Docker-Dokumentation). Deshalb veröffentlicht unten nur Caddy
Ports, die App nicht.
Docker installieren
Installiere Docker über das offizielle Paket-Repository, wie in der Docker-Anleitung für Ubuntu beschrieben. Das bequeme Skript von get.docker.com empfiehlt Docker selbst nur für Test- und Entwicklungsumgebungen, nicht für Produktion. Danach:
sudo usermod -aG docker deploy # danach ab- und wieder anmelden
docker compose versionDie Gruppe docker gibt dem Benutzer faktisch root-Rechte. Füge nur Benutzer hinzu, denen du das zutraust.
Die Compose-Datei
Lege ~/app/compose.yaml an:
services:
app:
image: ghcr.io/deine-firma/deine-app:1.0.0 # oder build: .
restart: unless-stopped
environment:
- NODE_ENV=production
# absichtlich kein "ports:": nur Caddy spricht mit der App
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:Und ~/app/conf/Caddyfile:
app.deine-firma.de {
encode
reverse_proxy app:3000
}app ist der Name des Compose-Dienstes; Dockers internes DNS löst ihn auf. encode ohne Angaben komprimiert mit zstd und gzip. Caddy bekommt den Ordner conf statt der einzelnen Datei, weil das Caddy-Image davon abrät, das Caddyfile direkt einzubinden: Manche Editoren ersetzen beim Speichern die Datei, und der Container sieht die Änderung dann nicht.
Starten
cd ~/app
docker compose up -d
docker compose logs -f caddyKurz darauf meldet das Caddy-Log, dass das Zertifikat ausgestellt wurde. Öffne https://app.deine-firma.de, und die App ist online.
Das Volume caddy_data enthält deine Zertifikate und Schlüssel. Lösch es nicht: Sonst muss Caddy alles neu anfordern, und du kannst an die Rate Limits von Let's Encrypt stoßen.
Updates ausrollen
docker compose pull app && docker compose up -d app
docker image prune -fFür ein Deployment bei jedem Push kann ein GitHub-Actions-Job per SSH auf den Server gehen und dieselben zwei Befehle ausführen.
Backups
Der Server ist jetzt die einzige Kopie von allem, was deine App schreibt. Mindestens:
- Daten in ein benanntes Volume oder einen Bind-Mount legen, nie nur in den Container.
- Die Datenbank regelmäßig sichern, bei Postgres zum Beispiel mit
docker compose exec -T db pg_dump -U postgres app > backup.sql. - Sicherungen vom Server wegkopieren, etwa mit restic auf S3-kompatiblen Speicher. Die meisten Anbieter verkaufen außerdem automatische Server-Backups oder Snapshots gegen Aufpreis; schalte sie ein.
Fehlersuche
| Symptom | Wahrscheinliche Ursache |
|---|---|
Caddy-Log: no such host oder die ACME-Prüfung schlägt fehl | DNS zeigt noch nicht auf den Server, oder Port 80 ist in der Firewall des Anbieters gesperrt |
502 Bad Gateway | Die App lauscht im Container nur auf localhost statt auf 0.0.0.0:3000 |
| HTTP geht, HTTPS nicht | Port 443 ist in der Cloud-Firewall des Anbieters nicht offen (die ist unabhängig von ufw) |
Wie es weitergeht
Wenn du lieber auf „Deploy“ klickst, als Compose-Dateien zu schreiben, bringt dir Coolify eine Oberfläche wie bei Heroku oder Vercel auf denselben Server. Und wie du nach demselben Muster eine Automatisierungsplattform betreibst, zeigt n8n selbst hosten.
[ Anolilab — Blog ]
Mehr Praxis zu KI, Webentwicklung und Infrastruktur.
