Eine Go-Web-App auf Hetzner deployen
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
Eine Go-Binary ist ungewöhnlich einfach auf einem Server zu platzieren: Kein Interpreter zu installieren, kein Paketmanager auszuführen, überhaupt keine Sprach-Runtime-Version auf dem Host zu pinnen. Einmal kompilieren, hinüberkopieren, und es läuft. Diese Einfachheit nimmt den Rest der Arbeit aber nicht weg. Du brauchst immer noch einen Server, eine darauf zeigende Domain, eine Datenbank, HTTPS vor allem, ein Backup, das das Verschwinden des Servers überlebt, und einen kurzen Weg, deine nächste Änderung auszuliefern. Diese Anleitung baut all das von Hand, dann zeigt sie die Abkürzung.
Die App selbst ist bewusst klein: ein net/http-Service, der nur die Standardbibliothek verwendet, mit einer Route, die über pgx/v5 von und zu Postgres liest und schreibt, sodass es einen echten Grund gibt, über eine Datenbankverbindung zu sprechen und nicht nur über ein statisches „Hello World”.
Was du brauchst
- Einen Hetzner-Cloud-Account und einen Server. Der kosteneffiziente CX23 (2 vCPU, 4 GB Speicher, 40 GB NVMe) listet für 5,99 €/Monat exkl. MwSt. Prüfe aktuelle Preise und Verfügbarkeit in der Hetzner-Cloud-Konsole vor der Bestellung, da sich Cloud-Angebote und Preise durchaus ändern.
- Einen Domainnamen, zu dem du einen DNS-Eintrag hinzufügen kannst.
- Ein SSH-Schlüsselpaar auf deiner Maschine (
ssh-keygen -t ed25519, falls du noch keines hast). - Lokal installiertes Go 1.27 oder neuer, um die App zu schreiben und zu bauen, bevor sie jemals den Server berührt.
- Etwa fünfzehn Minuten, bis Server und DNS sich einrichten, meist mit Warten statt Tippen verbracht.
Schritt 1: Den Server erstellen und eine Domain darauf richten
Öffne in der Hetzner-Cloud-Konsole ein (oder erstelle ein) Projekt, dann wähle Add Server. Wähle einen Standort: Nürnberg, Falkenstein oder Helsinki halten deine Daten in der EU, während Ashburn, Hillsboro oder Singapur ebenfalls angeboten werden, falls deine Nutzer anderswo sind. Wähle Ubuntu 24.04 als Image, dann unter Servertyp den Shared-vCPU-CX23 (2 vCPU, 4 GB Speicher, 40 GB NVMe). Füge unter SSH-Keys den öffentlichen Schlüssel von deiner Maschine hinzu, statt sich auf ein generiertes Passwort zu verlassen, und erstelle den Server dann.
Sobald er hochgefahren ist, zeigt die Konsole seine öffentliche IPv4-Adresse. Verbinde dich, um Zugriff zu bestätigen:
ssh root@YOUR_SERVER_IP
Du solltest das Ubuntu-Willkommensbanner sehen und in einer Root-Shell landen. Falls die Verbindung abgelehnt wird, prüfe nochmal, ob der am Server hinterlegte SSH-Key mit dem privaten Schlüssel auf deiner Maschine übereinstimmt.
Füge bei deinem DNS-Provider einen A-Record für den gewünschten Hostnamen hinzu, der auf diese IP zeigt:
Type: A
Name: myapp (oder @ für die nackte Domain)
Value: YOUR_SERVER_IP
TTL: 300
Prüfe, dass es aufgelöst wird, bevor du weitermachst, da Caddy das in einem späteren Schritt bereits funktionierend braucht:
dig +short myapp.example.com
Du solltest die IP deines Servers zurückbekommen. Falls du nichts bekommst, warte ein paar Minuten und versuch es erneut.
Hetzner stellt neuen Servern standardmäßig keine Firewall vor, also sperre den Host auf genau das ab, was du brauchst, mit ufw:
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Schritt 2: Docker auf dem Server installieren
Dockers eigenes Installationsskript richtet sowohl die Engine als auch das Compose-Plugin auf einer frischen Ubuntu-Box ein:
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker
Prüfe die Installation:
docker --version
docker compose version
Du solltest eine Docker-Version und eine Compose-Version ausgegeben sehen, keinen „command not found”-Fehler.
Schritt 3: Die Go-App schreiben
Richte auf deiner eigenen Maschine das Modul ein:
mkdir myapp && cd myapp
go mod init myapp
go get github.com/jackc/pgx/v5
go mod init und go get schreiben eine go.mod wie diese (deine genauen Abhängigkeitsversionen können leicht abweichen, je nachdem, wann du es ausführst, da go mod tidy sie zum Anfragezeitpunkt auflöst):
module myapp
go 1.27
require github.com/jackc/pgx/v5 v5.7.6
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
Jetzt main.go, nur mit net/http aus der Standardbibliothek plus pgxpool für die Datenbank:
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
var pool *pgxpool.Pool
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
log.Fatal("DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
pool, err = pgxpool.New(ctx, dbURL)
if err != nil {
log.Fatalf("unable to create connection pool: %v", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
log.Fatalf("unable to reach postgres: %v", err)
}
if _, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS visits (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`); err != nil {
log.Fatalf("unable to create visits table: %v", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", handleHealthz)
mux.HandleFunc("/visits", handleVisits)
addr := "0.0.0.0:" + port
log.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func handleHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
type visitsResponse struct {
Count int64 `json:"count"`
}
func handleVisits(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if _, err := pool.Exec(ctx, "INSERT INTO visits DEFAULT VALUES"); err != nil {
http.Error(w, "database error", http.StatusInternalServerError)
return
}
var count int64
if err := pool.QueryRow(ctx, "SELECT count(*) FROM visits").Scan(&count); err != nil {
http.Error(w, "database error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(visitsResponse{Count: count})
}
Zwei Dinge sind hier über die Handler selbst hinaus wichtig. Erstens liest die App PORT aus der Umgebung und fällt auf 8080 zurück, und sie bindet an 0.0.0.0:$PORT, nicht an localhost:$PORT. Caddy läuft in seinem eigenen Container und erreicht diese App über das Docker-Netzwerk anhand ihres Containernamens, nicht über das Loopback-Interface, also würde eine Bindung an localhost bedeuten, dass Caddy sie überhaupt nie erreichen könnte. Zweitens wird DATABASE_URL aus der Umgebung gelesen statt hartkodiert, sodass genau dieselbe Binary unverändert gegen eine lokale, eine Staging- oder die Produktionsdatenbank auf diesem Server läuft.
Es lohnt sich, zu benennen, was im Vergleich zu einer interpretierten Sprache fehlt. Ein Rails- oder Django-Deploy muss sicherstellen, dass die richtige Ruby- oder Python-Version auf dem Server installiert ist und dass die passenden Gems oder Pakete an den vom Interpreter erwarteten Pfaden gefunden werden. Nichts davon gilt hier. go build erzeugt eine Binary, in die schon alles eingelinkt ist; der Server braucht gar kein installiertes Go, nur die kompilierte Datei. Deshalb hat das Dockerfile im nächsten Schritt auch zwei Stufen: eine zum Kompilieren, eine, um nur das Ergebnis auszuführen.
Schritt 4: Das Dockerfile schreiben
FROM golang:1.27.3-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server .
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY --from=build /out/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]
Die erste Stufe verwendet golang:1.27.3-alpine, um die Binary zu kompilieren. CGO_ENABLED=0 schaltet cgo ab, sodass die resultierende Binary nicht gegen die C-Bibliothek des Systems linkt, und GOOS=linux garantiert eine Linux-Binary, selbst wenn du auf einem Mac baust. Die zweite Stufe startet von einem reinen alpine:3.21, weit kleiner als das Build-Image, und kopiert nur die kompilierte server-Binary hinein. ca-certificates wird installiert, damit die Binary TLS-Zertifikate verifizieren kann, falls DATABASE_URL jemals auf eine Postgres-Instanz zeigt, die SSL erfordert; pgx hat keine C-Abhängigkeiten, sobald CGO_ENABLED=0 gesetzt ist, also muss in dieser Runtime-Stufe nichts weiter installiert werden.
Schritt 5: Die Docker-Compose-Datei schreiben
Kopiere deine App auf den Server (scp, rsync, oder ein git clone deines Repositorys nach /opt/myapp), erstelle eine .env-Datei mit einem Datenbankpasswort, und füge dann compose.yml daneben hinzu:
POSTGRES_PASSWORD=a-long-random-password
services:
app:
build: .
restart: unless-stopped
environment:
DATABASE_URL: postgres://myapp:${POSTGRES_PASSWORD}@db:5432/myapp?sslmode=disable
PORT: "8080"
depends_on:
db:
condition: service_healthy
expose:
- "8080"
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 10
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
volumes:
db_data:
caddy_data:
caddy_config:
Der app-Service erreicht Postgres unter dem Hostnamen db, Composes Name für diesen Service, nicht localhost. depends_on: condition: service_healthy hält die App davon ab, zu starten, bevor Postgres tatsächlich Verbindungen akzeptieren kann, nicht nur bevor sein Container existiert. Nur caddy veröffentlicht Ports zum Host, sodass Postgres und die App selbst von außerhalb dieses Servers nie erreichbar sind.
Prüfe, dass die Datei sauber parst, bevor du weitermachst:
docker compose config
Du solltest die vollständig aufgelöste Konfiguration ohne Fehlerzeilen sehen.
Schritt 6: Die Caddyfile schreiben
myapp.example.com {
reverse_proxy app:8080
}
Ersetze myapp.example.com durch die Domain, die du in Schritt 1 auf den Server gerichtet hast, und 8080 durch das, was du in compose.yml als PORT gesetzt hast, falls du es änderst. Caddy leitet jeden Request an den app-Container auf diesem Port weiter und fordert ein kostenloses TLS-Zertifikat von Let’s Encrypt für genau diesen Hostnamen an, das automatisch erneuert wird. Das funktioniert nur, wenn die Domain bereits auf diesen Server aufgelöst wird, weshalb DNS in dieser Anleitung vor Caddy kam. Du kannst prüfen, ob die Caddyfile wohlgeformt ist, mit:
docker run --rm -v "$(pwd)/Caddyfile:/etc/caddy/Caddyfile" caddy:2 caddy validate --config /etc/caddy/Caddyfile
Du solltest Valid configuration in der Ausgabe sehen.
Schritt 7: Den Stack hochfahren
Aus /opt/myapp:
docker compose up -d --build
Das baut das Image, startet Postgres, wartet darauf, dass es sich gesund meldet, dann startet es die App, die sich mit Postgres verbindet und bei erstem Start ihre eine Tabelle erstellt.
Prüfe den Stack:
docker compose ps
curl -I https://myapp.example.com/healthz
Du solltest alle drei Services als Up sehen (und db als healthy), und der curl sollte HTTP/2 200 ohne Zertifikatswarnung zurückgeben. Mehrmaliges Aufrufen von /visits sollte eine steigende Zahl zurückgeben:
curl https://myapp.example.com/visits
Schritt 8: Postgres sichern
Ein Dump, der außerhalb des Daten-Volumes genommen wird, ist das, was dich tatsächlich schützt, falls der Server verschwindet. Füge ein Backup-Skript hinzu:
cat > /opt/myapp/backup.sh <<'EOF'
#!/bin/sh
cd /opt/myapp
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U myapp myapp | gzip > /opt/myapp/backups/$TIMESTAMP.sql.gz
find /opt/myapp/backups -name '*.sql.gz' -mtime +14 -delete
EOF
mkdir -p /opt/myapp/backups
chmod +x /opt/myapp/backup.sh
Plane es per Cron, täglich um 3 Uhr:
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/myapp/backup.sh") | crontab -
Führe es einmal manuell aus und prüfe, ob es funktioniert hat:
/opt/myapp/backup.sh
ls -lh /opt/myapp/backups
Du solltest eine .sql.gz-Datei mit dem heutigen Zeitstempel und einer Größe über null sehen. Für alles jenseits eines persönlichen Projekts kopiere diese Dateien auch vom Server weg, auf eine andere Maschine oder in einen Object Store, damit der Verlust des Servers nicht auch den Verlust des Backups bedeutet.
Schritt 9: Ein Upgrade ausliefern
Jede spätere Änderung folgt derselben Form: den neuen Code auf den Server bringen, das Image neu bauen, den laufenden Container ersetzen.
git pull
docker compose build app
docker compose up -d app
docker compose up -d app erstellt nur den app-Container neu, nicht db oder caddy, sodass Postgres weiterläuft und Caddys Zertifikatsstatus unberührt bleibt. Es gibt eine kurze Lücke, ein, zwei Sekunden, während der alte Container stoppt und der neue startet, da dieses Setup einen einzelnen App-Container betreibt.
Es ist wert, eines explizit zu benennen, was Rails und Django automatisch handhaben und Go nicht: Es gibt keine einzige dominante Konvention, um Schema-Migrationen als Teil eines Deploys auszuführen. Die eine Tabelle dieser Anleitung wird mit CREATE TABLE IF NOT EXISTS direkt in main.go erstellt, in Ordnung für etwas so Kleines, aber eine echte App mit sich entwickelndem Schema greift meist zu einem dedizierten Tool. goose und golang-migrate sind die beiden gängigsten, ausgeführt als expliziter Schritt, entweder in den Startbefehl des Containers eingebacken oder von Hand ausgeführt, bevor docker compose up -d app läuft. Entscheide dich für eins und bleib konsistent, da nichts es implizit für dich macht.
Prüfe, dass es angekommen ist:
docker compose logs --tail=50 app
curl -I https://myapp.example.com/healthz
Du solltest sehen, dass deine App in den Logs saubermen startet und der curl bei der neuen Version weiter 200 zurückgibt.
Die Abkürzung: mit Railyard
Alles oben (der Server, Docker, Postgres, Caddy, Backups, ein wiederholbares Release) ist auch das, was Railyard für eine Go-App auf Hetzner automatisiert, ohne dass du eine der obigen Dateien von Hand schreiben musst.
Railyard erkennt Go anhand von go.mod, einschließlich eines Moduls, das in einem Unterordner statt im Repository-Root liegt, und baut es automatisch vom richtigen Ort aus. Ein bekannter Datenbanktreiber-Import in deinem Code, github.com/jackc/pgx, github.com/lib/pq, github.com/go-sql-driver/mysql oder github.com/redis/go-redis, stellt die passende verwaltete Datenbank für dich bereit und fügt DATABASE_URL oder REDIS_URL als Umgebungsvariable ein, ohne ein Addon von Hand hinzuzufügen, dieselbe Verbindung, die diese Anleitung manuell in Schritt 5 verdrahtet hat. Die eine Anforderung bleibt unverändert aus dem manuellen Dockerfile oben: Die Binary muss auf $PORT lauschen, auf allen Interfaces, nicht nur auf localhost.
Go hat kein einzelnes dominantes Job-Queue-Framework, so wie Rails Sidekiq oder Solid Queue hat, also gibt es dort auch nichts für Railyard, automatisch zu erkennen. Statt dessen ist ein Worker-Prozess eine Zeile in einer Procfile-artigen Prozessdeklaration:
web: ./server
worker: ./server -mode=worker
Beide Zeilen führen dieselbe gebaute Binary aus; deine eigene main() liest ein Flag oder eine Umgebungsvariable, um zu entscheiden, welche Schleife läuft. Jeder Prozesstyp skaliert unabhängig von den anderen.
Füge die URL deines Repositorys in https://app.railyard.run/deploy?repo= ein und wähle Hetzner als Provider, oder geh direkt über den Go-auf-Hetzner-Deploy-Pfad. Siehe die Go-Docs für die genauen Erkennungsregeln. Railyards Free-Plan deckt einen Server plus einen Datenbankserver und drei Apps ohne Karte ab, Pro kostet 19 $/Monat pro Team, und Team kostet 49 $/Monat pro Team.
Troubleshooting
Lokal gebaute Binary läuft nicht auf dem Server. Ein Fehler wie exec format error beim Containerstart bedeutet meist, dass die Binary für die falsche Architektur cross-compiliert wurde, auf einem Apple-Silicon-Mac ohne GOARCH=amd64 gebaut und dann direkt auf einen x86-Server kopiert, oder GOOS=linux wurde weggelassen, sodass die Binary nur auf der Maschine läuft, die sie gebaut hat. Innerhalb des Dockerfiles mit CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build zu bauen, wie in Schritt 4 gezeigt, statt eine lokal gebaute Binary hineinzukopieren, vermeidet das, da der Build immer auf die richtige Plattform zielt.
Postgres-Verbindung abgelehnt. Falls die App-Logs could not connect to server: Connection refused oder Ähnliches zeigen, prüfe, dass DATABASE_URL db als Hostnamen verwendet, nicht localhost oder 127.0.0.1. Innerhalb von Composes Netzwerk ist db die einzige Adresse, die zum Postgres-Container aufgelöst wird; localhost bedeutet den app-Container selbst, in dem keine Datenbank läuft.
Port bereits in Verwendung. Falls docker compose up mit etwas fehlschlägt, das an Port 80 oder 443 gebunden ist, sind Ubuntus Standard-Apache oder -Nginx möglicherweise bereits installiert und laufend. Prüfe mit ss -tlnp | grep -E ':80|:443' und stoppe oder entferne, welcher Dienst sie auch besitzt, bevor Caddy versucht zu binden.
HTTPS wird nicht ausgestellt. Falls docker compose logs caddy wiederholte Zertifikatsfehler oder NXDOMAIN zeigt, hat dein DNS-Eintrag aus Schritt 1 sich entweder noch nicht propagiert oder zeigt auf die falsche IP. Führe dig +short myapp.example.com erneut aus, bestätige, dass sie mit dem Server übereinstimmt, und starte Caddy dann mit docker compose restart caddy neu, sobald es stimmt.
Out of Memory unter Last. Die 4 GB Speicher eines CX23 betreiben diesen Stack im Ruhezustand bequem, aber ein Goroutine-Leck, zum Beispiel eine pro Request, die nie beendet wird, kann den Speicherverbrauch stetig wachsen lassen, bis der Kernel beginnt, Prozesse zu killen. docker stats auf dem Server zeigt, ob der Speicher des app-Containers über die Zeit steigt statt sich nach einem Traffic-Burst zu stabilisieren; pprofs Heap-Profil ist der nächste Schritt, falls das der Fall ist.
FAQ
Brauche ich ein Framework wie gin oder echo für eine kleine App? Nein. Die Standardbibliothek net/http und http.ServeMux handhaben Routing, und http.Handler- und Middleware-Muster sind reines Go, kein Framework erforderlich, für alles, was nicht Dutzende Routen hat oder eingebaute Request-Validierung und -Gruppierung will.
Wie füge ich einen Worker-Prozess hinzu? Füge einen zweiten Service zu compose.yml hinzu, gebaut aus demselben Image, der dieselbe Binary mit anderem Flag oder Befehl ausführt, zum Beispiel ./server -mode=worker statt ./server. Er liest dieselbe DATABASE_URL, nur ohne auf einen Port zu binden.
Macht der Static-Binary-Ansatz tatsächlich einen Sicherheitsunterschied? Er entfernt eine ganze Problemkategorie: keine Interpreter-Version mit eigenen CVEs, die auf dem Server gepatcht werden muss, und kein Paketmanager, der transitive Abhängigkeiten zur Deployzeit auflöst. Schwachstellen in deinen eigenen Go-Abhängigkeiten müssen trotzdem verfolgt werden, wie bei jeder Sprache, aber die Angriffsfläche von „was ist außer meiner App noch installiert” ist nahezu null.
Was, wenn die App überhaupt keine Datenbank hat? Entferne den db-Service komplett aus compose.yml, entferne DATABASE_URL aus der Umgebung von app, und der Rest der Anleitung bleibt unverändert: die Binary genauso bauen, sie genauso hinter Caddy ausführen, nichts sichern, weil es nichts Zustandsbehaftetes zu sichern gibt.
Wie funktionieren Migrationen, da Go keine einzige Konvention hat? Anders als Rails’ db:prepare oder Djangos migrate läuft nichts implizit. Wähl ein Tool, goose und golang-migrate sind die beiden am meisten verwendeten, und führe es als expliziten Schritt in deinem Deploy-Prozess aus, entweder als Befehl im Entrypoint des Containers oder als manuellen Schritt vor docker compose up -d app. Für ein so einfaches Schema, CREATE TABLE IF NOT EXISTS beim Start, wie es die main.go dieser Anleitung macht, ist eine vernünftige Übergangslösung, aber sie handhabt nicht das Ändern einer bestehenden Tabelle, wo ein echtes Migrationstool sich seinen Platz verdient.
Das ist ein komplettes Produktionssetup für einen Go-Service: ein Server, Docker Compose, das die App und Postgres betreibt, Caddy, das HTTPS übernimmt, ein nächtliches Backup und ein Zwei-Befehle-Release. Halte compose.yml, Caddyfile und das Dockerfile in deinem Repository neben der App, und jeder zukünftige Deploy ist derselbe git pull, Build, Neustart, den du gerade in Schritt 9 ausgeführt hast.
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.