← Blog

Eine Rails-App auf DigitalOcean deployen

Railyard

14. April 2026 · 10 Min. Lesezeit

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

Du hast eine Rails-App, die auf deinem Laptop läuft, und du brauchst sie auf einem echten Server, mit einer echten Domain, einer echten Datenbank und HTTPS, das sich selbst erneuert. Die meisten Anleitungen hören entweder bei “docker build” auf und lassen dich den Rest selbst herausfinden, oder sie reichen dir eine gehostete Plattform mit einem Kreditkartenformular. Diese hier bringt dich den ganzen Weg von einem leeren DigitalOcean-Konto bis zu einer Rails-App in Produktion: ein Droplet, Docker Compose, das deine App und Postgres ausführt, Caddy, das HTTPS terminiert, ein Backup-Job und ein wiederholbarer Weg, dein nächstes Release zu verschicken.

Was du brauchst

  • Ein DigitalOcean-Konto und ein Basic Droplet in der Größe s-1vcpu-2gb (1 vCPU, 2 GB Speicher, 50 GB SSD), aktuell 12,00 $/Monat.
  • Einen Domainnamen, bei dem du einen DNS-Eintrag hinzufügen kannst.
  • Ein SSH-Schlüsselpaar auf deinem Rechner (ssh-keygen -t ed25519, falls du noch keins hast).
  • Eine Rails-App, oder eine neue: Diese Anleitung baut eine von Grund auf mit rails new, sodass du auch ohne bestehende App mitmachen kannst.
  • Fünfzehn Minuten, bis sich Droplet und DNS eingependelt haben, hauptsächlich Warten statt Tippen.

Schritt 1: Das Droplet erstellen

Wähle im DigitalOcean-Control-Panel Create, dann Droplets. Wähle Ubuntu 24.04 (LTS) x64 als Image, Basic als Plan, Regular SSD und die Größe s-1vcpu-2gb (12,00 $/Monat, 2 GB Speicher, 1 vCPU, 50 GB SSD). Wähle eine Region nahe deinen Nutzern, Frankfurt oder Amsterdam, falls du Daten in der EU halten musst. Wähle unter Authentication SSH key und füge den öffentlichen Schlüssel von deinem Rechner hinzu statt eines Passworts, dann erstelle das Droplet.

Sobald es gebootet ist, zeigt dir DigitalOcean seine öffentliche IP. Verbinde dich, um den Zugriff zu bestätigen:

ssh root@YOUR_DROPLET_IP

Du solltest das Ubuntu-Willkommensbanner sehen und in einer Root-Shell landen. Falls die Verbindung abgelehnt wird, prüfe, ob der hinzugefügte SSH-Schlüssel zum privaten Schlüssel auf deinem Rechner passt.

Schritt 2: Deine Domain auf das Droplet zeigen lassen

Füge bei deinem DNS-Anbieter einen A-Record für den gewünschten Hostnamen hinzu, der auf die IP des Droplets zeigt:

Type: A
Name: myapp (oder @ für die nackte Domain)
Value: YOUR_DROPLET_IP
TTL: 300

Die DNS-Propagierung kann einige Minuten dauern. Prüfe sie von deinem Rechner, bevor du weitermachst, da Caddy das in einem späteren Schritt bereits korrekt auflösen muss:

dig +short myapp.example.com

Du solltest die IP deines Droplets zurückgemeldet sehen. Falls nichts kommt, warte ein paar Minuten und versuch es erneut, bevor du weitermachst.

Schritt 3: Docker auf dem Droplet installieren

Das von Rails generierte Dockerfile braucht Docker Engine und das Compose-Plugin auf dem Server. Dockers eigenes Installationsskript erledigt beides auf einer frischen Ubuntu-Box:

apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker

Öffne außerdem die Firewall für SSH, HTTP und HTTPS, und nichts sonst:

ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable

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 4: Die Rails-App mit Dockerfile und Postgres generieren

Generiere auf deinem eigenen Rechner eine neue App, die für Postgres statt für die SQLite-Voreinstellung konfiguriert ist:

gem install rails -v 8.1.4
rails new myapp --database=postgresql
cd myapp

rails new erstellt standardmäßig ein Dockerfile (eine der Dateien, die beim Lauf des Generators aufgelistet werden, zusammen mit .dockerignore), abgestimmt auf Produktion: Es baut deine Gems und Assets in einer Stage und kopiert danach nur das Nötige in ein schlankes Runtime-Image. Rails 8 bringt außerdem Thruster mit, einen kleinen HTTP-Proxy vor Puma, der HTTP/2, Kompression und X-Sendfile übernimmt, weshalb das generierte Image intern Port 80 exponiert und mit ./bin/thrust ./bin/rails server startet statt Puma direkt zu starten. Du erreichst diesen Port nicht direkt aus dem Internet, Caddy sitzt davor.

Weil du --database=postgresql übergeben hast, ist config/database.yml für das pg-Gem statt für SQLite eingerichtet, und deine Gemfile.lock listet jetzt pg, was genau das ist, was einem Host sagt, dass diese App eine Postgres-Datenbank will statt MySQL oder SQLite.

Prüfe, dass die Dateien da sind:

ls Dockerfile .dockerignore config/database.yml

Du solltest alle drei Pfade fehlerfrei ausgegeben sehen.

Schritt 5: Datenbankverbindung und Secrets konfigurieren

Öffne config/database.yml und richte den production-Abschnitt auf einen einzigen Connection-String statt separate Host-/Username-/Password-Keys, da du genau das von Docker Compose injizieren wirst:

production:
  <<: *default
  url: <%= ENV.fetch("DATABASE_URL") %>

Rails verschlüsselt config/credentials.yml.enc mit config/master.key, einer Datei, die absichtlich in .gitignore steht. Du wirst diese Datei nicht auf den Server verschicken. Lies statt dessen einmal ihren Wert aus:

cat config/master.key

Erstelle auf dem Droplet ein Verzeichnis für die App und eine .env-Datei mit diesem Key und einem Datenbank-Passwort, das du selbst generierst:

mkdir -p /opt/myapp
cat > /opt/myapp/.env <<'EOF'
RAILS_MASTER_KEY=paste-the-value-from-config-master-key-here
POSTGRES_PASSWORD=a-long-random-password
EOF
chmod 600 /opt/myapp/.env

Prüfe es:

cat /opt/myapp/.env

Du solltest beide Variablen mit echten Werten sehen, nicht mit Platzhaltern, und die Datei sollte nicht weltlesbar sein (-rw------- bei ls -l).

Schritt 6: Die Docker-Compose-Datei schreiben

Kopiere deine App auf das Droplet (scp, rsync, oder ein git clone deines Repositorys nach /opt/myapp), und füge dann compose.yml daneben hinzu:

services:
  app:
    build: .
    image: myapp:latest
    restart: unless-stopped
    env_file: .env
    environment:
      RAILS_ENV: production
      DATABASE_URL: postgres://myapp:${POSTGRES_PASSWORD}@db:5432/myapp_production
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - storage:/rails/storage
    expose:
      - "80"

  db:
    image: postgres:16.4-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: myapp_production
    volumes:
      - pgdata:/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:
  pgdata:
  storage:
  caddy_data:
  caddy_config:

Der app-Service verbindet sich mit db über dessen Servicenamen, nicht über localhost, da Compose beide Container in dasselbe private Netzwerk setzt. Die Zeile depends_on: condition: service_healthy verhindert, dass die App startet, bevor Postgres tatsächlich Verbindungen annehmen kann, nicht nur bevor sein Container existiert. Named Volumes halten deine Datenbank- und Active-Storage-Dateien über Neustarts und Rebuilds hinweg auf Disk.

Prüfe, dass die Datei sauber geparst wird, bevor du weitermachst:

docker compose config

Du solltest die vollständig aufgelöste Konfiguration ohne Fehlerzeilen ausgegeben sehen.

Schritt 7: Caddy als Reverse-Proxy mit HTTPS hinzufügen

Füge ein Caddyfile neben compose.yml hinzu:

myapp.example.com {
	reverse_proxy app:80
}

Ersetze myapp.example.com durch die Domain, die du in Schritt 2 auf das Droplet gelegt hast. Caddy liest das und macht zwei Dinge von sich aus: Es proxyt jeden Request zu Port 80 des app-Containers, und es fordert ein kostenloses TLS-Zertifikat von Let’s Encrypt für genau diesen Hostnamen an, das es automatisch erneuert, bevor es abläuft. Das funktioniert nur, wenn die Domain bereits auf dieses Droplet auflöst, weshalb DNS in dieser Anleitung vor Caddy kam.

Schritt 8: Den Stack booten

Von /opt/myapp aus:

docker compose build
docker compose up -d db
docker compose run --rm app bin/rails db:prepare
docker compose up -d

bin/rails db:prepare erstellt die Datenbank, falls sie noch nicht existiert, und führt alle anstehenden Migrationen aus, genau der Befehl, den Railyard später in dieser Anleitung vor jedem Release ausführt.

Prüfe den Stack:

docker compose ps
curl -I https://myapp.example.com

Du solltest alle drei Services als Up sehen (und db als healthy), und curl sollte HTTP/2 200 ohne Zertifikatswarnung zurückgeben.

Schritt 9: Postgres sichern

Ein Dump, der außerhalb des Datenvolumes gezogen wird, ist das, was dich tatsächlich schützt, falls das Droplet 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_production | 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 mit 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 Hobbyprojekts kopiere diese Dateien auch vom Droplet weg, mit rclone zu DigitalOcean Spaces oder einem anderen Object Store, damit ein verlorenes Droplet nicht auch ein verlorenes Backup bedeutet.

Schritt 10: Ein Upgrade verschicken

Jede spätere Änderung folgt derselben Form: den neuen Code auf das Droplet bringen, das Image neu bauen, anstehende Migrationen ausführen, dann den laufenden Container ersetzen.

git pull
docker compose build app
docker compose run --rm app bin/rails db:prepare
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 oder zwei Sekunden, während der alte Container stoppt und der neue startet, da dieses manuelle Setup einen einzelnen App-Container betreibt. Prüfe, ob es angekommen ist:

docker compose logs --tail=50 app
curl -I https://myapp.example.com

Du solltest deine App saubere in den Logs hochfahren sehen und curl weiterhin 200 auf der neuen Version zurückgeben.

Die Abkürzung: mit Railyard

Alles oben, Droplet-Größe, Docker, Postgres, Caddy, Backups, Zero-Downtime-Releases, ist auch das, was Railyard für eine Rails-App auf DigitalOcean automatisiert, ohne dass du eine der oben genannten Dateien von Hand schreibst.

Füge die URL deines Repositorys bei https://app.railyard.run/deploy?repo= ein und wähle DigitalOcean als Provider. Beim ersten Mal generierst du im DigitalOcean-Control-Panel ein Token (öffne API, dann generiere einen Personal Access Token mit Lese- und Schreibzugriff), damit Railyard den Server für dich provisionieren kann. Railyard setzt diese Kombination standardmäßig auf dasselbe s-1vcpu-2gb-Droplet, das in dieser Anleitung verwendet wurde, und du kannst jede DigitalOcean-Region wählen, einschließlich Frankfurt oder Amsterdam, falls dir EU-Datenresidenz wichtig ist.

Ab da liest Railyard deine App auf die gleiche Weise, wie diese Anleitung es von Hand getan hat: Es sieht pg in deiner Gemfile.lock und provisioniert eine gemanagte Postgres-Datenbank statt SQLite, es führt bin/rails db:prepare vor jedem Release aus, genauso wie Schritt 8 es manuell tat, und wenn du auf Rails 8 bist, gibt es den Cache-, Queue- und Cable-Datenbanken ihre eigenen Connection-Strings statt sie zu teilen. Active Storage auf Disk bekommt automatisch ein persistentes Volume, passend zum storage-Volume aus Schritt 6. SECRET_KEY_BASE wird einmal generiert und behalten, sodass du es nie von Hand verwalten musst. Wenn deine Gemfile.lock Sidekiq, Solid Queue, GoodJob, Resque oder Delayed Job enthält, bietet Railyard es als Worker-Prozess neben deinem Web-Prozess an, kein separater Compose-Service zu schreiben. HTTPS und der Reverse-Proxy aus Schritt 7 werden auf die gleiche Weise gehandhabt, sobald du deine Domain hinzufügst.

Mehr dazu in Deployment aus einem Repository, Datenbanken und Backups, dem One-Click-Deploy-Button, Erste Schritte, dem Rails-auf-DigitalOcean-Referenzguide, und Hosting-Kostenvergleichen, wenn du sehen willst, wie die Nur-Droplet-Kosten hier mit einem gemanagten Weg vergleichen. Railyards Free-Plan deckt einen Server plus einen Datenbankserver und drei Apps ohne Kreditkarte ab, Pro kostet 19 $/Monat pro Team, und Team kostet 49 $/Monat pro Team.

Troubleshooting

Port bereits belegt. Falls docker compose up fehlschlägt, weil etwas an Port 80 oder 443 gebunden ist, läuft möglicherweise bereits Ubuntus Standard-Apache oder -Nginx. Prüfe mit ss -tlnp | grep -E ':80|:443' und stoppe oder entferne den Service, der sie belegt, bevor Caddy versucht zu binden.

Falscher RAILS_MASTER_KEY oder Credentials. Ein Fehler wie ActiveSupport::MessageEncryptor::InvalidMessage beim Boot bedeutet fast immer, dass der RAILS_MASTER_KEY in /opt/myapp/.env nicht zu dem Key passt, der config/credentials.yml.enc verschlüsselt hat. Führe cat config/master.key lokal erneut aus und vergleiche ihn Zeichen für Zeichen mit dem, was in .env auf dem Droplet steht.

Postgres-Verbindung abgelehnt. Falls die App-Logs could not connect to server: Connection refused zeigen, prüfe, ob DATABASE_URL db als Hostnamen verwendet, nicht localhost oder 127.0.0.1. Innerhalb von Compose’ Netzwerk ist db die einzige Adresse, die zum Postgres-Container aufgelöst wird; localhost meint den app-Container selbst, der keine Datenbank laufen hat.

HTTPS wird nicht ausgestellt. Falls docker compose logs caddy wiederholte Zertifikatsfehler oder NXDOMAIN zeigt, hat sich dein DNS-Eintrag aus Schritt 2 entweder noch nicht propagiert oder zeigt auf die falsche IP. Führe dig +short myapp.example.com erneut aus, bestätige, dass es zum Droplet passt, und starte Caddy mit docker compose restart caddy neu, sobald es passt.

Speichermangel beim Asset-Precompile. Auf einem 2-GB-Droplet kann docker compose build bei größeren Apps mitten in der Asset-Kompilierung abgebrochen werden. Füge vor dem Build eine 2-GB-Swap-Datei hinzu: fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile, und mache sie dann dauerhaft, indem du sie zu /etc/fstab hinzufügst.

FAQ

Reicht ein 2-GB-Droplet für Produktion? Für eine kleine bis mittelgroße Rails-App mit moderatem Traffic, ja, da Puma und Postgres zusammen bequem hineinpassen, und der Swap-Fix im Troubleshooting oben den einen Fall abdeckt (große Asset-Builds), in dem 2 GB knapp werden.

Kann ich Postgres überspringen und bei SQLite bleiben? Du kannst, SQLite funktioniert auf einem einzelnen Server problemlos, solange du seine Datenbankdatei auf einem persistenten Volume mountest, damit sie Container-Neustarts überlebt, aber Postgres gibt dir einen saubereren Weg, falls du jemals einen zweiten App-Server hinzufügst.

Wie füge ich Redis oder Sidekiq hinzu? Füge compose.yml einen redis-Service mit dem offiziellen redis-Image und eigenem Named Volume hinzu, füge apps Umgebung REDIS_URL hinzu, das auf redis://redis:6379/0 zeigt, und füge einen worker-Service hinzu, der bundle exec sidekiq gegen dasselbe Image statt den Rails-Server-Befehl ausführt.

Überlebt das einen Droplet-Reboot? Ja, restart: unless-stopped bei jedem Service bedeutet, dass Docker den gesamten Stack automatisch wieder hochfährt, nachdem das Droplet neu gestartet ist, solange Dockers eigener systemd-Service aktiviert ist, was das Installationsskript in Schritt 3 standardmäßig tut.

Wie unterscheidet sich das von Heroku oder Railway? Diese Plattformen managen den Server für dich zu einem Preis pro App; diese Anleitung gibt dir dasselbe Ergebnis auf einem Server, den du direkt an DigitalOcean zahlst, zum Preis, die Upgrade- und Backup-Befehle selbst auszuführen statt ein Dashboard das machen zu lassen.

Das ist ein vollständiges Produktions-Setup: ein Droplet, Docker Compose, das deine App und Postgres ausführt, Caddy, das HTTPS übernimmt, ein nächtliches Backup und ein wiederholbares Drei-Befehle-Release. Behalte dieses compose.yml und Caddyfile in deinem Repository neben der App, und jedes zukünftige Deployment ist derselbe git pull, Build, Migrate, Restart, den du gerade in Schritt 10 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.

Überspring das Droplet-Setup

Füge die URL deines Repositorys bei Railyard ein und bekomme Postgres, einen Worker, HTTPS und Backups automatisch konfiguriert.

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