HTTPS für viele Apps auf einem Server
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
Eine VPS ist reichlich Maschine für mehrere kleine Apps: ein Blog, eine interne API, eine Statusseite, von denen keine einen eigenen Server braucht. Was sie brauchen, ist, dass jede HTTPS auf ihrer eigenen Subdomain beantwortet, mit einem Zertifikat, das tatsächlich gültig ist und sich tatsächlich erneuert, ohne dass du alle paar Monate certbot renew per Hand ausführst oder pro Site eine separate nginx-Konfiguration und einen Cron-Job schreibst. Eine einzige Caddy-Instanz vor allem erledigt das mit weniger Konfiguration, als man erwarten würde, indem sie pro Domain aus einer Datei ein Zertifikat ausstellt und erneuert.
Diese Anleitung richtet drei Apps hinter einer Caddy-Instanz auf einem Server ein, jede erreichbar auf ihrer eigenen Subdomain mit automatisch gehandhabtem HTTPS, und behandelt dann, was Railyard an demselben Problem automatisiert, sodass überhaupt kein Caddyfile gepflegt werden muss.
Was du brauchst
- Eine VPS mit mindestens 2 GB RAM. Ein DigitalOcean-2-GB-Basic-Droplet (12 $/Monat) oder ein Hetzner CX23 (2 vCPU, 4 GB RAM, 5,99 €/Monat exkl. MwSt.) hat Platz für mehrere kleine Container plus Caddy selbst.
- Eine Domain, die du kontrollierst, mit Zugriff, um DNS-Records hinzuzufügen.
- Docker und das Compose-Plugin auf dem Server installiert.
- Die Ports 80 und 443 offen und nicht bereits von einem anderen Webserver belegt.
Schritt 1: DNS auf den Server zeigen, bevor irgendetwas anderes passiert
Caddy fordert ein Zertifikat an, sobald jede Domain das erste Mal über HTTPS angefragt wird, und Let’s Encrypt validiert diese Anfrage, indem geprüft wird, dass die Domain tatsächlich auf diesen Server aufgelöst wird. Erstelle jetzt pro Subdomain einen A-Record, damit er Zeit hat, sich zu verbreiten, bis du Caddy startest:
blog.example.com. A 203.0.113.10
api.example.com. A 203.0.113.10
status.example.com. A 203.0.113.10
Alle drei können auf dieselbe IP zeigen; Caddy unterscheidet sie über den Host-Header im Request, genau wie jeder andere Reverse-Proxy auch.
Schritt 2: Die Apps als separate Container anlegen
Dieses Beispiel betreibt drei Dinge: eine Blog-App und eine API, jeweils ein eigener kleiner Container, und eine Statusseite, die als reine statische Dateien ohne jedes Backend ausgeliefert wird. Jede App lebt in ihrem eigenen Verzeichnis mit eigenem Dockerfile:
.
├── blog/
│ └── Dockerfile
├── api/
│ └── Dockerfile
├── status/
│ └── index.html
├── docker-compose.yml
└── Caddyfile
Nichts davon hängt davon ab, was tatsächlich in blog oder api steckt; eine Django-App aus einer früheren Anleitung, eine Node-API, oder alles andere, das in seinem Container auf einem Port lauscht, funktioniert genauso. Die einzige Anforderung, die Caddy stellt, ist, dass jeder Container einen Port im Docker-Netzwerk exponiert.
Schritt 3: Das Caddyfile schreiben
{
email ops@example.com
}
blog.example.com {
reverse_proxy blog:8000
}
api.example.com {
reverse_proxy api:8001
}
status.example.com {
root * /srv/status
file_server
}
Jede Domain ist ihr eigener Block. blog.example.com und api.example.com leiten per Docker-Compose-Servicename an ihre Container weiter; Caddy löst diesen Namen im internen Netzwerk genauso auf, wie es überall sonst einen Hostnamen auflösen würde. status.example.com hat überhaupt kein Backend, Caddy liefert einfach Dateien direkt von der Disk mit file_server. Der oberste email-Block ist das, was Let’s Encrypt nutzt, um dich zu warnen, falls ein Zertifikat kurz davor steht, nicht erneuert zu werden; er ist optional, aber es lohnt sich, ihn zu setzen.
Validier ein Caddyfile, bevor du ihm in Produktion traust:
docker run --rm -v "$(pwd)/Caddyfile:/etc/caddy/Caddyfile" caddy:2 caddy validate --config /etc/caddy/Caddyfile
Du solltest Valid configuration ausgegeben sehen, keinen Parse-Fehler.
Schritt 4: docker-compose.yml schreiben
services:
blog:
build: ./blog
restart: unless-stopped
expose:
- "8000"
api:
build: ./api
restart: unless-stopped
expose:
- "8001"
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./status:/srv/status:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- blog
- api
volumes:
caddy_data:
caddy_config:
Nur caddy veröffentlicht die Ports 80 und 443 zum Host; blog und api nutzen expose, was ihre Ports für andere Container im selben Compose-Netzwerk erreichbar macht, aber überhaupt nicht von außerhalb des Servers. Das ist der Punkt, alles hinter einem Proxy zu betreiben: Die App-Container sind nie direkt erreichbar, nur über Caddy, und nur auf der Domain, auf der sie antworten sollen. Das caddy_data-Volume ist das, was Zertifikate einen Restart überleben lässt; es zu verlieren bedeutet, dass Caddy beim nächsten Start neue von Let’s Encrypt anfordert, was sicher ist, aber gegen Let’s Encrypts Rate-Limits zählt, falls es oft passiert.
Schritt 5: Starten und die Zertifikate prüfen
docker compose up -d --build
docker compose logs -f caddy
Du solltest für jede Domain eine Zeile sehen, die besagt, dass ein Zertifikat bezogen wurde, etwa certificate obtained successfully mit dem Domainnamen daneben. Bestätige von außerhalb des Servers:
curl -I https://blog.example.com
curl -I https://api.example.com
curl -I https://status.example.com
Jede sollte eine Antwort ohne Zertifikatswarnung liefern, und curl -v würde ein von Let’s Encrypt für genau diesen Hostnamen ausgestelltes Zertifikat zeigen.
Schritt 6: Später eine vierte App ohne Downtime hinzufügen
Füge einen neuen Block zum Caddyfile hinzu, einen neuen Service zu docker-compose.yml, erstelle den DNS-Record, und sag dem laufenden Caddy-Container, die Änderung zu übernehmen, ohne sonst etwas neu zu starten:
docker compose up -d newapp
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
caddy reload liest die Konfiguration neu ein und berührt nur, was sich geändert hat; die Zertifikate und Verbindungen der anderen drei Domains bleiben ungestört.
Backups und Upgrades
Der einzige Zustand, der sich hier zu sichern lohnt, ist das caddy_data-Volume (Zertifikate, die sich sowieso neu generieren) und was jede App selbst speichert, wie ein Datenbank-Volume, falls eine davon eines hat. Sicher die genauso, wie du es für jede andere App tun würdest: ein geplanter Dump oder ein vom Server kopierter Volume-Snapshot.
Um Caddy selbst zu aktualisieren, zieh den neuen Image-Tag und erstelle nur diesen Container neu:
docker compose pull caddy
docker compose up -d caddy
Bestehende Zertifikate in caddy_data werden übernommen; Caddy muss nichts neu ausstellen, nur weil sich die Binary-Version geändert hat.
Die Abkürzung: mit Railyard
Alles oben (das Zertifikat pro Domain, der Reverse-Proxy, der Ownership-Check, der verhindert, dass jemand anderes eine Domain auf deinen Server zeigt, die er nicht kontrolliert) ist das, was Railyard pro App automatisiert, statt über ein geteiltes Caddyfile. Jede App bekommt https://<name>.<server-ip>.sslip.io mit einem Zertifikat ab dem Moment, in dem sie deployt wird, ohne dass bereits ein DNS-Record nötig ist. Deine eigene Domain hinzuzufügen bedeutet, den Domains-Tab der App zu öffnen, den Hostnamen hinzuzufügen und den A- oder CNAME-Record zu erstellen, den es dir zeigt; ein TXT-Record (_railyard-verify.<domain>) beweist, dass du die Domain kontrollierst, bevor das Zertifikat oder Routing startet, automatisch alle 30 Sekunden geprüft. Mehrere Apps auf demselben Server zu betreiben, ändert nichts davon: Jede wird unabhängig geroutet und zertifiziert, genauso wie das Caddyfile oben drei Domains in einer Konfiguration handhabt, nur ohne dass du diese Konfiguration selbst schreibst oder neu lädst.
Für eine Wildcard-Domain (*.example.com, die jede Subdomain an eine App routet) oder viele Subdomains, die sonst ein Zertifikat pro Besuch bedeuten würden, behandelt Networking die zwei Optionen: On-Demand-TLS (ein Zertifikat, ausgestellt beim ersten Besuch jeder Subdomain, der Standard) oder ein Zertifikat für die ganze Wildcard über eine DNS-01-Challenge, mit einem unter Team settings → DNS credentials hinzugefügten Cloudflare-API-Token oder Route-53-Access-Key.
Troubleshooting
Port 80 oder 443 bereits in Benutzung. Etwas anderes (oft ein vom Server-Image installiertes nginx oder Apache, oder ein Überbleibsel eines früheren Setups) ist bereits daran gebunden. Finde es mit ss -tlnp | grep -E ':80|:443' und stopp es, bevor du Caddy startest.
Für eine Domain wird nie ein Zertifikat ausgestellt, aber die anderen funktionieren. Prüf das DNS dieser spezifischen Domain separat: dig blog.example.com. Ein Tippfehler in einem A-Record ist die häufigste Ursache, und er bricht nur diese eine Domain, da Caddy Zertifikate unabhängig pro Site-Block anfordert.
„too many certificates already issued” von Let’s Encrypt. Das ist ein Rate-Limit (aktuell 5 doppelte Zertifikate pro exakter Domain pro Woche), meist getroffen durch wiederholtes Abbauen und Neuerstellen des caddy_data-Volumes während des Testens. Behalte das Volume zwischen Restarts, und nutze Caddys interne CA (tls internal im Caddyfile-Block), während du an der Konfiguration iterierst, und wechsle zu echten Zertifikaten, sobald es funktioniert.
Dateiberechtigungsfehler auf der Statusseite oder einem gemounteten Volume. file_server braucht Lesezugriff auf alles, was in seinem Root gemountet ist; ein von einem anderen Container unter einem anderen User erstelltes Volume ist die übliche Ursache. Behebe den Besitz auf dem Host, oder mounte es :ro wie im obigen Beispiel, falls nichts darauf schreiben muss.
Dem Server geht bei mehreren Containern plus Caddy der Speicher aus. Jeder kleine App-Container ist meist in Ordnung, aber prüf die tatsächliche Nutzung mit docker stats, bevor du annimmst, es sei Caddy; ein Datenbank-Container ist ein wahrscheinlicherer Übeltäter als der Proxy selbst. Skalier hoch, oder zieh die ressourcenhungrigste App auf ihren eigenen Server und zeig ihre Subdomain weiterhin dorthin, wo sie tatsächlich läuft.
FAQ
Brauche ich Docker dafür, oder funktioniert es mit einem systemweit installierten Caddy? Das Muster ist in beiden Fällen identisch: ein Caddyfile mit einem Block pro Domain, jeweils auf einen lokalen Port statt einen Docker-Servicenamen zeigend. systemd verwaltet den Caddy-Prozess statt Compose.
Müssen alle Apps auf demselben Server sein? Für diese Anleitung ja, da der Punkt eine geteilte Caddy-Instanz ist. Nichts hindert dich daran, Caddy auf einem Server zu betreiben und über ein privates Netzwerk an Apps auf anderen weiterzuleiten, aber an dem Punkt baust du die Art von Routing, die eine Plattform wie Railyard schon handhabt.
Was passiert, wenn ich hundert Subdomains hinzufüge? On-Demand-TLS stellt pro Subdomain ein Zertifikat beim ersten Besuch aus, was funktioniert, aber bedeutet, hundert separate Let’s-Encrypt-Zertifikate nachzuverfolgen. Ein Wildcard-Zertifikat über DNS-01 deckt alle mit einem ab, was die bessere Wahl ist, sobald man über eine Handvoll Subdomains hinausgeht, egal ob in einem handgeschriebenen Caddyfile oder über Railyards eigene DNS-Credential-Option.
Leitet das automatisch HTTP zu HTTPS um? Ja, Caddy macht das standardmäßig für jede Domain im Caddyfile, ohne dass zusätzliche Konfiguration nötig ist.
Drei Domains hinter einer Caddy-Instanz und dreißig hinter einer sind keine bedeutend unterschiedlichen Setups, das Caddyfile wird nur länger. Was sich zu verinnerlichen lohnt, ist, dass ein Reverse-Proxy, der seine eigenen Zertifikate pro Domain ausstellt, einfacher zu betreiben ist als ein certbot-Cron-Job und eine nginx-Konfiguration pro Site, egal ob du dieses Caddyfile selbst schreibst oder etwas anderes es für dich schreiben lässt.
Warum Leute sich dafür Railyard entscheiden
- Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
- Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
- Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
- Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
- Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.