← Blog

Von Fly.io zu deinem eigenen Server umziehen

Railyard

22. Mai 2026 · 8 Min. Lesezeit

Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig

Fly rechnet nach Nutzung ab, was schwer vorhersehbar ist, und “Fly Postgres” ist immer noch ein Postgres, das du selbst betreibst, nur auf Flys Maschinen statt deinen eigenen. Wenn du schon ein Dockerfile hast, das auf Fly baut und läuft, hast du fast alles, was du brauchst, um es auch woanders zu betreiben. Diese Anleitung zieht eine bestehende Fly-App, ihre Umgebung, ihre Secrets und ihre Postgres-Daten auf einen einzigen Server um, den du selbst besitzt: Docker Compose, Caddy für HTTPS, ein echter Backup-Job, dann ein DNS-Cutover ohne Downtime-Fenster. Die Abkürzung am Ende nutzt Railyards eigenen Fly-Importer für die Teile, die keinen manuellen Dump und Restore brauchen.

Was du brauchst

  • Einen Server. Ein DigitalOcean Basic Droplet mit 2 vCPUs und 2 GB RAM betreibt die meisten kleinen Apps samt einer Postgres-Instanz zusammen für 18 $/Monat; geh auf 4 GB für 24 $/Monat, falls deine App nennenswerte Background-Arbeit macht. Die äquivalente Ubuntu-Box jedes anderen Anbieters funktioniert genauso.
  • Einen Domainnamen, den du umziehen kannst, idealerweise den, der schon an der Fly-App hängt.
  • Die flyctl-CLI, bereits gegen den Account authentifiziert, dem die App gehört (fly auth whoami sollte deinen Account ausgeben, keinen Fehler).

Schritt 1: Inventar, was die Fly-App tatsächlich braucht

Zieh die aktuelle App-Konfiguration, damit du weißt, was du nachbaust, statt zu raten:

fly config show -a your-fly-app

Du solltest dasselbe JSON zurückbekommen, aus dem Fly deployed, inklusive des [http_service]- oder [[services]]-Blocks mit internal_port, des [env]-Blocks mit deinen nicht-geheimen Variablen, und jeglicher [[mounts]] für Volumes. Notiere besonders den internen Port, das ist der eine Wert, auf den sich jeder andere Service in dieser Anleitung einigen muss.

Liste die Secrets nach Namen, da Fly dir nie ihre Werte zeigt:

fly secrets list -a your-fly-app

Du solltest eine Tabelle mit Secret-Namen, Digests und Deployment-Status sehen, nicht die Werte selbst. Zieh die tatsächlichen Werte von dort, wo du sie ursprünglich gesetzt hast (ein Passwort-Manager, deine CI-Secrets, ein Teammitglied), denn das ist das eine, was Fly dir wirklich nicht zurückgeben kann.

Schritt 2: Den Server provisionieren und Docker installieren

curl -fsSL https://get.docker.com | sh
docker compose version

Du solltest einen Compose-v2.x-Versionsstring sehen. Zeig den A-Record deiner Domain jetzt schon auf den neuen Server, da der spätere HTTPS-Schritt davon abhängt:

dig +short app.example.com

Schritt 3: Die Docker-Compose-Datei schreiben

Die meisten Fly-Apps bauen bereits aus einem Dockerfile im Repo, Fly verlangt eines (oder ein Buildpack, das eines erzeugt), um überhaupt zu deployen, also läuft dasselbe Image, das auf Flys Maschinen läuft, auch hier. Postgres und Caddy drumherum bauen:

services:
  app:
    build: .
    restart: unless-stopped
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app_net

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      start_period: 10s
      retries: 20
    networks:
      - app_net

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      - app
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - app_net

networks:
  app_net:

volumes:
  postgres_data:
  caddy_data:
  caddy_config:

Bau die .env aus dem [env]-Block, den fly config show vorher ausgegeben hat, plus den Secret-Werten, die du besorgt hast:

POSTGRES_PASSWORD=a-long-random-string
DATABASE_URL=postgres://app:a-long-random-string@db:5432/app
PORT=8080
# everything from fly config show's [env] block goes here too
# everything fly secrets list named goes here, with its real value

PORT sollte zum internal_port aus Schritt 1 passen, denn das ist der Port, auf dem der Code deiner App selbst lauscht, egal welche Plattform ihn betreibt.

Schritt 4: Die Postgres-Daten umziehen

Nutzt die App Flys unmanaged Postgres (eine Postgres-App, die du selbst deployed und angehängt hast, der häufige Fall bei Apps, die schon eine Weile laufen), öffne einen Proxy dazu:

fly proxy 15432:5432 -a your-postgres-app

In einem zweiten Terminal die Datenbank durch diesen Proxy dumpen:

pg_dump -Fc -h localhost -p 15432 -U postgres your_database -f fly-dump.pgdump

Du solltest am Ende eine nicht-leere fly-dump.pgdump-Datei haben; ls -lh fly-dump.pgdump sollte eine Größe zeigen, die grob zu der Datenmenge passt, die du erwartest, nicht ein paar Bytes.

Kopier sie auf den neuen Server und stell sie wieder her in den db-Container, sobald der Stack läuft:

scp fly-dump.pgdump root@YOUR_SERVER_IP:/root/
docker compose cp /root/fly-dump.pgdump db:/tmp/fly-dump.pgdump
docker compose exec db pg_restore -U app -d app --no-owner --no-acl /tmp/fly-dump.pgdump

--no-owner --no-acl verwerfen Rollen- und Berechtigungsinformationen, die nur auf Flys Postgres Sinn ergaben, damit das Restore nicht fehlschlägt, weil es nach einer Rolle sucht, die es hier nicht gibt. Du solltest sehen, dass pg_restore höchstens mit ein paar Warnungen zu übersprungenen Objekten aus demselben Grund fertig wird, nicht mit einem harten Fehler.

Bist du stattdessen auf Flys neuerem Managed Postgres, ist der Proxy-Befehl fly mpg proxy YOUR_CLUSTER_ID --local-port 15432, und die Verbindungsdetails (Nutzer, Datenbankname) kommen aus fly mpg status statt standardmäßig postgres/dem Namen deiner App zu sein; die pg_dump/pg_restore-Befehle selbst sind identisch, sobald du durch den Proxy verbunden bist.

Schritt 5: Den Stack hochfahren und verifizieren

docker compose up -d --build
docker compose logs -f app

Du solltest die gewohnten Startup-Logzeilen deiner eigenen App sehen, dieselben, die du in fly logs sehen würdest, endend damit, dass sie auf dem Port aus .env lauscht. Bestätige, dass die Daten mitgekommen sind:

docker compose exec db psql -U app -d app -c "SELECT count(*) FROM your_main_table;"

Diese Zahl sollte, abgesehen von ein paar zwischenzeitlich geschriebenen Zeilen, zu dem passen, was du vor dem Dump auf Fly gesehen hast.

Schritt 6: Das Caddyfile schreiben und DNS umschalten

app.example.com {
	reverse_proxy app:8080
}

Caddy hochfahren und bestätigen, dass das Zertifikat ausgestellt wird (docker compose logs caddy), bevor du DNS anfasst. Sobald https://app.example.com gegen den neuen Server korrekt antwortet, auf einer temporären Subdomain oder indem du zum Testen deine lokale /etc/hosts bearbeitest, aktualisiere den echten A-Record auf die IP des neuen Servers. Fly bedient die eigene Subdomain der App die ganze Zeit weiter, da du dort nichts angefasst hast, also gibt es keine Lücke, in der keiner der beiden Server antwortet. Sobald du bestätigt hast, dass der Traffic zum neuen Server fließt, skaliere die Fly-App auf null Maschinen und lösche sie, sobald du dir sicher bist, dass du sie nicht mehr brauchst.

Schritt 7: Backups und Upgrades

Sichere Postgres genauso, wie du es von Fly gedumpt hast, dieses Mal nach einem Zeitplan:

#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db pg_dump -U app -Fc app > "backups/app-db-$STAMP.pgdump"
find backups -name '*.pgdump' -mtime +14 -delete

Kopier backups/ im selben Cron-Job vom Server weg, auf eine andere Maschine oder in Object Storage. Um die App selbst zu aktualisieren, bau nur diesen Container neu und erstelle ihn neu:

docker compose build app
docker compose up -d app

Die Abkürzung: mit Railyard

Railyards Fly-Importer übernimmt die Teile davon, die nicht deine eigenen Daten betreffen: unter Applications → New app → Import from Heroku / Render / Fly, Fly wählen, einen Token aus fly tokens create org einfügen, und die App auswählen. Er zieht exakt das Image, das Fly gerade betreibt (nutzt deinen Token als Registry-Passwort, sodass nichts aus dem Quellcode neu gebaut wird), die Umgebungsvariablen und den internen Port aus der Machine-Konfiguration, und die Namen deiner Secrets – du gibst ihre Werte selbst neu ein, da Fly sie niemandem offenlegt, auch diesem Importer nicht. Andere Prozessgruppen jenseits der Haupt-App werden noch nicht kopiert, füge sie danach als Worker hinzu, und jegliche Volumes starten leer, also ist Schritt 4 oben (oder seine Managed-Postgres-Variante) weiterhin der Weg, auf dem die eigentlichen Daten umziehen; der Importer selbst fasst Postgres überhaupt nicht an.

https://app.railyard.run/deploy?repo=https://github.com/you/your-app

funktioniert ebenfalls, für den schlichten Git-Push-Weg, sobald der Importer die Fly-spezifischen Teile erledigt hat, oder falls du ihn lieber ganz überspringen willst. Siehe Import from another platform für das vollständige Bild, was jede Quellplattform mitbringt, Migration von Fly.io für das Feld-für-Feld-Mapping zwischen Flys Konzepten und denen von Railyard, und Hosting-Kosten, falls du auch herausfinden willst, was der neue Server dich tatsächlich kosten sollte im Vergleich zu dem, was Fly abgerechnet hat.

Troubleshooting

pg_restore schlägt bei einer Rolle oder Extension fehl, die es nicht finden kann. Das ist fast immer eine Fly-spezifische Extension oder eine Rolle, auf die der Dump verweist, die am Ziel nicht existiert. --no-owner --no-acl behebt das meiste davon; für eine tatsächlich fehlende Extension installiere sie im Ziel-Postgres-Image oder entferne die CREATE EXTENSION-Zeile aus dem Dump vor dem Restore.

Die App bootet, aber wirft hinter Caddy sofort 502. Prüfe, dass PORT in .env sowohl zu dem passt, auf das deine App tatsächlich bindet, als auch zu dem, worauf die reverse_proxy-Zeile des Caddyfiles zeigt. Eine Abweichung hier ist die mit Abstand häufigste Ursache, da Fly und ein generisches Compose-Setup sich nicht immer einig sind, wie der Port injiziert wird.

Zertifikat wird nie ausgestellt. dig +short app.example.com muss zuerst auf den neuen Server auflösen. Testest du vor dem echten Cutover, nutze eine Staging-Subdomain, die schon hierhin zeigt, statt auf den Produktions-Record zu warten.

Secrets fehlen nach dem Import, oder die App crasht bei einem Konfigurationswert. Flys eigene API gibt nie Secret-Werte zurück, nur Namen, das ist also erwartet: gib jeden einzeln wieder ein, von dort, wo du ihn ursprünglich gespeichert hast, nicht aus dem Import.

Out of Memory während pg_restore auf einem kleinen Server. Ein 2-GB-Droplet kann sich schwertun, einen großen Dump neben einer laufenden App wiederherzustellen. Stoppe den app-Container während des Restores (docker compose stop app), um Speicher freizugeben, und starte ihn wieder, sobald pg_restore fertig ist.

FAQ

Muss ich alles auf einmal migrieren? Nein. Bring die Datenbank und den Hauptprozess zuerst rüber, verifiziere es, und füge dann jegliche sekundären Prozessgruppen als separate Worker-Services hinzu, sobald du bestätigt hast, dass die Haupt-App funktioniert.

Was passiert mit der Subdomain, die Fly jeder App automatisch zuweist? Nichts, bis du die Fly-App löschst. Sie antwortet weiter, genau bis du die App auf null skalierst, was den Zero-Downtime-Cutover überhaupt erst möglich macht: Beide Adressen, die alte und die neue, funktionieren gleichzeitig, während DNS propagiert.

Funktioniert das für Apps, die Flys Multi-Region-Deployment nutzen? Diese Anleitung zieht dich auf einen Server in einer Region um. Brauchst du wirklich mehrere Regionen, führe dieses Setup einmal pro Region hinter deinem eigenen Load Balancing aus, es gibt hier kein Ein-Befehl-Äquivalent zu Flys automatischem Region-Fanout.

Ist Flys Managed Postgres anders genug, um eigene Schritte zu brauchen? Nur der Proxy-Befehl und der Standard-Benutzername, fly mpg proxy statt fly proxy, und Credentials aus fly mpg status statt aus dem App-Namen. pg_dump und pg_restore funktionieren exakt gleich, sobald du verbunden bist.

Beide Wege holen deine App und ihre Daten aus Flys Abrechnungsmodell heraus und auf einen Server, bei dem du die echte Spec und die echte Rechnung siehst – die manuellen Schritte, wenn du die volle Kontrolle über den Cutover willst, der Importer, wenn er dir lieber die einfachen 80 Prozent abnehmen soll.

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.

Die App direkt aus Fly importieren

Railyards Fly-Importer zieht dasselbe Image, denselben Port und dieselben Env-Vars mit deinem Token, sodass du nur Secrets neu eingibst statt alles neu zu tippen.

Kostenloser Plan, keine Kreditkarte, und 14 Tage Pro für jedes neue Team. Ein fester Preis pro Team.