REST API

REST API

Die team-gebundene JSON-API hinter der CLI: Apps, Env, Deploys, Logs und Server.

Eine kleine, team-gebundene JSON-API, um Railyard von einer CLI, einem CI-Job oder einem Skript aus zu steuern – Apps aufzählen und erstellen, Env setzen, Deploys auslösen, Deploy-Logs lesen, die Flotte prüfen.

  • Base URL – deine Control Plane, unter /api. Lokales Compose: http://localhost:3000/api
  • Format – nur JSON. Sende Content-Type: application/json bei Writes. Kein CSRF, keine Session, keine Cookies.
  • Alles ist auf ein Team begrenzt – das Team, zu dem das Token gehört.

Authentifizierung

Jeder Request braucht einen Bearer-Token:

Authorization: Bearer rly_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Token erstellen: Dashboard → Team → API tokens → benennen, einen Scope wählen, Create. Der rly_…-String wird einmal angezeigt – dann kopieren. Nur für Admins.

Scopes

ScopeKann
readjeden GET
writealles, was read kann, plus POST / PATCH / DELETE

Ein read-only Token auf einem mutierenden Endpoint → 403 {"error":"this API token is read-only"}. Fehlendes / ungültiges / zurückgezogenes / abgelaufenes Token → 401 {"error":"missing or invalid API token"}.

Widerrufen von derselben Team-Seite; ein widerrufenes oder abgelaufenes (expires_at überschritten) Token funktioniert sofort nicht mehr.


Konventionen

  • Fehler – Antworten ohne 2xx sind { "error": "<message>" }. Status-Codes: 400 Bad Request · 401 nicht authentifiziert · 403 falscher Scope · 404 nicht gefunden (oder nicht in deinem Team) · 409 Konflikt (z. B. läuft bereits ein Deploy) · 422 Validierung fehlgeschlagen · 429 Rate-Limit.
  • Zeitstempel – ISO-8601 UTC (2026-09-02T14:03:11Z).
  • IDs – Integer, pro Team stabil.
  • Pagination – Listen-Endpunkte, die limit akzeptieren, nehmen 1–100 (Standard 30). Noch kein Cursor; neueste zuerst, wo nach Zeit sortiert wird.
  • Rate-Limit – das Erstellen von Deploys ist pro Team pro Stunde begrenzt (GET /api/me → limits.deploys_per_hour); darüber hinaus → 429.

Endpunkte

MethodePfadScopeZweck
GET/api/mereadToken- und Team-Info, Limits, Zähler
GET/api/applicationsreadApps auflisten
POST/api/applicationswriteEine App erstellen
GET/api/applications/:idreadApp-Detail
PATCH/api/applications/:idwriteEine App aktualisieren
DELETE/api/applications/:idwriteEine App löschen (und ihre Deploys/Logs)
POST/api/applications/:id/archivewriteContainer stoppen, Datensatz behalten
POST/api/applications/:id/unarchivewriteArchivierung aufheben
GET/api/applications/:id/envreadEnv-Variablen lesen
PATCH/api/applications/:id/envwriteEnv-Variablen zusammenführen (null-Wert löscht)
DELETE/api/applications/:id/env?key=KEYwriteEine Env-Variable löschen
GET/api/applications/:id/edgereadEdge-Einstellungen (gleiche Form wie der edge:-Abschnitt des Manifests, siehe Edge rules)
PATCH/api/applications/:id/edgewriteEdge-Einstellungen zusammenführen: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}}
GET/api/applications/:id/certificatesreadHochgeladene Zertifikate (Namen, abgedeckte Hosts, Gültigkeit, Fingerprint; nie der Key)
POST/api/applications/:id/certificateswriteHochladen {"certificate":{"certificate_pem":"…","private_key_pem":"…"}}
DELETE/api/applications/:id/certificates/:cert_idwriteEines entfernen (automatische Zertifikate nach dem nächsten Deploy)
GET/api/applications/:id/domainsreadEigene Domains auflisten
POST/api/applications/:id/domainswriteEigene Domain hinzufügen
DELETE/api/applications/:id/domains/:domain_idwriteEigene Domain entfernen
GET/api/applications/:id/deploysreadDeploys für eine App
POST/api/applications/:id/deployswriteEinen Deploy auslösen ({"image": "registry/repo:tag"} deployt dieses vorgebaute Image einmalig)
GET/api/applications/:id/registrywriteDie Build-Registry des Teams für deploy --local: repository, username, password, platform
POST/api/applications/:id/tunnelwriteEinen Tunnel öffnen ({"target": "web|db|redis|<process>", "local_port": 5432}): gibt ein 12-Stunden-ticket für den TunnelChannel-WebSocket zurück, den entfernten port, und eine localhost-url für Datenbanken; wird als app.tunnel_opened geloggt
GET/api/applications/:id/logsreadLaufzeit-Container-Logs (?tail=, ?process=)
GET/api/applications/:id/processesreadZustand pro Container (für ps)
POST/api/applications/:id/restartwriteJeden Container neu starten, oder einen einzelnen mit ?process=
GET/api/applications/:id/process_typesreadProzess-Typen mit Env-, Shutdown- und Größen-Einstellungen
PATCH/api/applications/:id/process_types/:pt_idwriteenv_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit setzen
GET/api/applications/:id/commandsreadLetzte einmalige Command-Läufe
POST/api/applications/:id/commandswriteEinen einmaligen Command ausführen ({"command": "..."})
GET/api/applications/:id/commands/:cmd_idreadEin Lauf: Status, Output, Exit-Code
POST/api/applications/:id/reindexwriteDie Such-Indizes neu aufbauen: führt den search.reindex_command der App in einem einmaligen Container aus (202; 422 ohne Command oder erfolgreichen Deploy, 409 während einer läuft). reindex_command / auto_reindex setzen mit PATCH /api/applications/:id; GET zeigt sie unter search mit der erkannten library
POST/api/applications/:id/clonewriteDie App in eine Staging-Kopie klonen
POST/api/applications/:id/promotewriteDen Release einer Staging-App in Production befördern
GET/api/applications/:id/manifestreadDie App als railyard.yml (?secrets=true schließt Variablenwerte ein)
POST/api/applications/:id/apply_manifestwriteEin Manifest anwenden ({"manifest": "<yaml text>"})
POST/api/applications/validate_manifestreadEin Manifest auf Fehler prüfen, ohne es anzuwenden – kein :id, jedes App-Token funktioniert ({"manifest": "<yaml text>"} → {"problems": [...]}, leer wenn gültig)
POST/api/applications/:id/db_queryread / writeSQL ausführen ({"sql": "...", "write": false}); write: true braucht ein write-Token
GET/POST/api/applications/:id/database_backupsread / writeBackups auflisten / jetzt eines erstellen
POST/api/database_backups/:id/restorewriteEin Backup wiederherstellen
GET/api/database_backups/:id/downloadreadSignierter Download-Link
GET/POST/api/applications/:id/volume_backupsread / writeBackups auflisten / ein Volume sichern ({"volume": "name"})
POST/api/volume_backups/:id/restorewriteEin Volume-Backup wiederherstellen
GET/api/applications/:id/trafficreadRequests, p95, Fehlerrate und langsamste Endpunkte (?range=1h|24h|7d|30d)
PATCH/api/applications/:id/autoscalewriteWeb-Replicas nach p95/CPU skalieren ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} schaltet es aus
POST/api/applications/:id/data_movewriteDie Daten der App auf einen Datenbankserver verschieben ({"database_server_id": 12}); 202 mit dem, was kopiert wird
POST/api/applications/:id/external_datastoreswritedatabase, redis oder elasticsearch der App auf eine externe URL richten ({"kind","url","copy"}); die URL wird zuerst geprüft (502 wenn unerreichbar), 202 wenn eingereiht
DELETE/api/applications/:id/external_datastores/:kindwriteDiese Daten zurück unter Railyard bringen (?copy=true kopiert sie zurück)
GET/api/applications/:id/addons/:kind/transfersreadLetzte Exports/Imports eines Add-ons (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), mit Download-Links
POST/api/applications/:id/addons/:kind/transferswriteDie Daten des Add-ons exportieren; mit multipart file + confirm=<app name>, eine Datei darin laden
GET/api/applications/:id/storage/objectsreadDen Bucket des Storage-Add-ons durchsuchen (?prefix=, ?token=): Ordner, Dateien mit Größe, Public-Flag und einem signierten Link
POST/api/applications/:id/storage/objectswriteEine Datei hochladen (multipart file, prefix oder key)
DELETE/api/applications/:id/storage/object?key=writeEine Datei löschen, oder einen Ordner, wenn der Key auf / endet
PATCH/api/applications/:id/storage/visibilitywriteEinen Ordner öffentlich oder privat machen ({"prefix":"avatars/","public":true})
GET/api/applications/:id/redisreadWo das Redis der App liegt (geteilte DB N oder eigenes), ihr Limit und ihre Policy, genutzter Speicher
PATCH/api/applications/:id/rediswrite{"dedicated":true,"maxmemory_mb":512,"policy":"allkeys-lru"} gibt der App ihr eigenes Redis (Keys werden kopiert) oder ändert ihr Limit; {"dedicated":false} verschiebt es zurück
GET/api/deploysreadDeploys im ganzen Team (?application_id=, ?limit=)
GET/api/deploys/:idreadDeploy-Detail
GET/api/deploys/:id/logsreadDeploy-Log-Zeilen (älteste zuerst, ≤5000)
GET/api/applications/:id/log_searchreadGespeicherte Log-Zeilen: Zeitraum, Suche, Cursor-Seiten (siehe Logs, metrics and alerts)
GET/api/applications/:id/log_downloadreadGespeicherte Log-Zeilen für einen Zeitraum als Text
GET/PATCH/DELETE/api/applications/:id/otel_exportread / writeOpenTelemetry-Export-Einstellungen (siehe Logs, metrics and alerts)
GET/POST/api/applications/:id/metric_alertsread / writeMetrik-Schwellenwert-Alerts und ihre Historie
PATCH/DELETE/api/applications/:id/metric_alerts/:alert_idwriteEinen Alert ändern oder entfernen
GET/api/applications/:id/job_queuereadJob-Queue-Statistiken, fehlgeschlagene Jobs, Historie (siehe Logs, metrics and alerts)
POST/api/applications/:id/job_queue/failed/:job_id/retrywriteEinen fehlgeschlagenen Job erneut versuchen
DELETE/api/applications/:id/job_queue/failed/:job_idwriteEinen fehlgeschlagenen Job verwerfen
GET/api/serversreadDie Flotte
GET/api/servers/:idreadServer-Detail + Live-Prozess-Zustände
POST/api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_dockerwriteServer-Lifecycle-Aktionen
PATCH/api/servers/:id/resizewriteBeim Provider resizen ({"size": "..."})
POST/api/servers/:id/trust_ipwriteEine IP entbannen und vertrauen ({"ip": "..."}) – ActiveProtect hebt den Bann innerhalb von ~1 Min auf. Lässt Watchtower einen Bann aufheben, ohne das Dashboard zu öffnen.
GET/api/notification_rulesreadTeam-weite Benachrichtigungsregeln
GET/api/webhook_endpointsreadTeam-weite Webhook-Endpunkte
GET/api/configreadTeam-weite Konfigurationseinträge

GET /api/me

curl -s https://cp.example.com/api/me -H "Authorization: Bearer $RLY"
{
  "team":  { "id": 1, "name": "Acme", "slug": "acme", "suspended": false },
  "token": { "name": "ci", "scope": "write", "last_used_at": "2026-09-02T14:00:00Z" },
  "limits": { "servers": 10, "apps": 50, "deploys_per_hour": 30 },
  "counts": { "applications": 11, "servers": 3 }
}

Applications

app-Objekt (Listenform):

{
  "id": 11,
  "name": "chatwoot-copy",
  "environment": "production",
  "parent_id": null,
  "git_url": "image://chatwoot/chatwoot:v4.17.1",
  "git_ref": "main",
  "hostname": "chatwoot-copy.apps.example.com",
  "url": "https://chatwoot-copy.apps.example.com",
  "server": "railyard-8",
  "container_port": 3000,
  "addons": ["postgres", "redis"],
  "archived": false,
  "auto_deploy": false,
  "latest_deploy": { "id": 31, "status": "succeeded", "sha": null, "created_at": "2026-09-02T13:00:00Z" },
  "created_at": "2026-09-01T09:00:00Z"
}

GET /api/applications/:id fügt hinzu: release_command, health_path, deploy_approval_required, deploy_window {days, start, end, time_zone, open_now, opens_at}, strict_pushes, db_extensions[], processes[] {name, command, role}, env_keys[] (nur Namen – Werte werden im App-Detail nie zurückgegeben; dafür den Env-Endpoint nutzen), domains[] {hostname, redirect, cert_state}.

Erstellen – POST /api/applications

curl -s -X POST https://cp.example.com/api/applications \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"application":{
        "name":"blog",
        "git_url":"https://github.com/acme/blog",
        "git_ref":"main",
        "container_port":3000,
        "server":"railyard-8",
        "addons":["postgres"],
        "env_vars":{"RAILS_ENV":"production"}
      }}'

Body ist {"application": { … }}. Akzeptierte Keys: name, git_url, git_ref, container_port, hostname, health_path, release_command, build_method (auto | dockerfile | nixpacks), dockerfile_path, docker_build_target, cpu_limit, memory_limit, process_types_text (ein name: command pro Zeile), scheduled_tasks_text (name | cron | command pro Zeile), db_extensions, deploy_approval_required, strict_pushes (ein git push, dessen Build fehlschlägt, wird abgelehnt; Standard true), addons (Array), env_vars (Objekt). Beim Update zusätzlich: server_id.

  • server (oder server_id) kann eine Server-ID oder ein Hostname sein. Weglassen, und die App landet auf dem ersten uncordoned Server im Team.
  • Ein leerer hostname wird abgeleitet – <name>.<server app_domain>, sonst APP_DOMAIN_SUFFIX, sonst <name>.<server-ip>.sslip.io.
  • Erstellen deployt nicht. Danach POST …/deploys ausführen.
  • Über dem App-Limit des Teams → 422.

201 mit dem detaillierten App-Objekt. PATCH → 200. DELETE → 204.

Archivieren / dearchivieren – POST /api/applications/:id/archive stoppt den laufenden Container und setzt archived: true; Datensatz, Konfiguration und Historie bleiben erhalten. unarchive entfernt das Flag (erneuter Deploy, um sie zurückzuholen).


Env-Variablen – /api/applications/:id/env

# lesen
curl -s https://cp.example.com/api/applications/11/env -H "Authorization: Bearer $RLY"
# {"env":{"RAILS_ENV":"production","LOG_LEVEL":"info"}}

# zusammenführen (zwei setzen, eine löschen)
curl -s -X PATCH https://cp.example.com/api/applications/11/env \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"env":{"LOG_LEVEL":"debug","FEATURE_X":"1","OLD_KEY":null}}'

# eine löschen
curl -s -X DELETE "https://cp.example.com/api/applications/11/env?key=FEATURE_X" \
  -H "Authorization: Bearer $RLY"

PATCH führt zusammen mit der bestehenden Menge; ein null-Wert entfernt diesen Key. Alle drei geben das vollständige {"env": { … }} zurück. Änderungen wirken sich beim nächsten Deploy aus.


Eigene Domains – /api/applications/:id/domains

curl -s -X POST https://cp.example.com/api/applications/11/domains \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"domain":{"hostname":"support.acme.com","redirect":false}}'
{ "id": 4, "hostname": "support.acme.com", "redirect": false,
  "cert_state": "pending", "dns_target": "203.0.113.9", "cert_issuer": null }

redirect: true lässt den Hostnamen per 301 auf den primären Hostnamen der App weiterleiten, statt ihn auszuliefern. Den angezeigten dns_target in deinem DNS eintragen, dann stellt Caddy das Zertifikat aus. DELETE …/domains/:domain_id → 204.


Deploys

deploy-Objekt:

{
  "id": 31, "application_id": 11, "application": "chatwoot-copy",
  "status": "succeeded", "ref": "main", "trigger": "manual", "message": null, "sha": null, "rollback": false,
  "created_at": "2026-09-02T13:00:00Z",
  "started_at": "2026-09-02T13:00:04Z",
  "finished_at": "2026-09-02T13:02:31Z"
}

status ∈ queued · pending_approval · running · succeeded · failed. GET /api/deploys/:id fügt error, triggered_by (E-Mail), duration_seconds hinzu.

Ein Canary-Deploy hat außerdem ein canary-Objekt (sonst null):

"canary": {
  "percent": 10, "state": "running", "started_at": "2026-09-29T12:00:00Z",
  "promote_at": "2026-09-29T12:15:00Z", "requests": 412, "error_rate": 0.49, "p95_ms": 88.4,
  "rest_of_app": { "requests": 3710, "error_rate": 0.4, "p95_ms": 91.0 }
}

state ∈ running · promoted · rolled_back · replaced.

Canary – POST /api/deploys/:id/canary/promote startet den Promotion- Deploy (202, promotion_deploy_id); DELETE /api/deploys/:id/canary macht ihn rückgängig. Beide brauchen ein write-Token und antworten mit 409, sobald der Canary nicht mehr live ist.

Deploy-Strategie – PATCH /api/applications/:id/deploy_strategy mit einem der folgenden: deploy_strategy (standard · canary), canary_percent (1–99), canary_steps_text (kommagetrennte, aufsteigende Prozentwerte, z. B. "5, 25, 50"; leer = ein Schritt), canary_promote_after_minutes (1–1440, leer = manuell), canary_max_error_rate (0–100). GET /api/applications/:id zeigt dieselben Felder. Mit gesetzten Schritten startet die Promotion eines Schritts den nächsten Schritt als eigenen Canary, statt direkt auf 100 % zu gehen.

Redeploy bei Image-Änderung (nur Image-Apps, sonst 422) – PATCH /api/applications/:id/image_watch mit enabled (true/false); POST /api/applications/:id/image_watch/check prüft die Registry jetzt (202). Beide geben zurück, und GET /api/applications/:id zeigt es als image_watch:

{ "enabled": true, "image": "ghcr.io/acme/api:prod",
  "webhook_url": "https://cp.example.com/webhooks/image/2cV…",
  "digest": "sha256:fb84…", "checked_at": "2026-09-29T12:05:00Z", "error": null }

Registries rufen POST /webhooks/image/<token> (kein API-Token; beliebiger Body) auf, um eine Prüfung auszulösen.

Auslösen – POST /api/applications/:id/deploys

curl -s -X POST https://cp.example.com/api/applications/11/deploys \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"ref":"v2.1.0"}'
  • ref (optional) – Branch, Tag oder SHA, der einmalig deployt wird; Standard ist der git_ref der App, den ein ref nie ändert. Der trigger des Deploys ist ref, und sein ref-Feld zeigt, was verwendet wurde. Namen mit Zeichen, die git Refs nicht haben können, bekommen 422. Wird bei einer image://-App ignoriert.
  • message (optional) – eine Deploy-Notiz (bis zu 500 Zeichen), wird als message zurückgegeben und im Dashboard, in der Activity, in Benachrichtigungen und den deploy.*-Webhooks angezeigt.
  • at (optional) – ISO-8601-Zeit, zu der statt jetzt deployt werden soll. Antwortet mit 201 mit status: "scheduled" und scheduled_for; die Zeit muss in der Zukunft liegen und innerhalb des Deploy-Fensters (sonst 422). Abbrechen mit POST /api/deploys/:id/cancel (bricht auch einen wartenden oder laufenden Deploy ab).
  • ignore_window (optional) – deployt, auch wenn die App außerhalb ihres Deploy-Fensters liegt. Ohne das antwortet solch ein Deploy mit 409 und outside_window: true und window_opens_at.
  • rollback_to (optional) – ID eines früheren erfolgreichen Deploys. Deployt erneut dessen Commit und stellt die Variablen wieder her, mit denen er lief; die Antwort fügt restored_config hinzu. "restore_config": false senden, um nur den Code zurückzurollen. Releases, die deployt wurden, bevor Railyard eine Kopie ihrer Variablen aufbewahrte, rollen immer nur den Code zurück.
  • 202 Accepted + das Deploy-Objekt, wenn er eingereiht ist und läuft.
  • 200 OK + das Objekt mit status: "pending_approval", wenn die App Deploy-Freigabe erfordert (im Dashboard freigeben).
  • 409, wenn für diese App bereits ein Deploy aktiv ist oder auf Freigabe wartet.
  • 429, wenn das Deploys-pro-Stunde-Limit des Teams erreicht ist.
  • 422, wenn die App archiviert ist.
  • 422 mit missing_env (Array von Namen), wenn die App noch nie erfolgreich deployt wurde und Zugangsdaten, die ihr Repo dokumentiert, nicht gesetzt oder noch Platzhalter sind. Sie über den Env-Endpoint setzen, oder "force": true senden, um trotzdem zu deployen – nach diesen Namen wird dann nicht erneut gefragt.

Deploy-Fenster – PATCH /api/applications/:id/deploy_window

curl -s -X PATCH https://cp.example.com/api/applications/11/deploy_window \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"days":["mon","tue","wed","thu","fri"],"start":"09:00","end":"17:00","time_zone":"America/Toronto"}'

Gibt {days, start, end, time_zone, open_now, opens_at} zurück (auch unter deploy_window in GET /api/applications/:id). "days": [] entfernt das Fenster. Pushes, GitHub-/GitLab-Webhooks und zusammengefasste Pushes außerhalb des Fensters werden zu einem scheduled-Deploy, der startet, wenn es öffnet.

Logs – GET /api/deploys/:id/logs

{
  "deploy_id": 31,
  "status": "succeeded",
  "lines": [
    { "stream": "system", "line": "Deploying web (web)", "at": "2026-09-02T13:00:05Z" },
    { "stream": "stdout", "line": "Pulled chatwoot/chatwoot:v4.17.1", "at": "2026-09-02T13:00:40Z" },
    { "stream": "system", "line": "Deployed at https://chatwoot-copy.apps.example.com", "at": "2026-09-02T13:02:30Z" }
  ]
}

stream ∈ stdout · stderr · system. Begrenzt auf 5000 Zeilen, älteste zuerst. Pollen, solange status queued/running ist.

Liste – GET /api/deploys?application_id=11&limit=50 oder GET /api/applications/11/deploys.


Laufzeit – Logs, Prozesse, Commands, Restart

Die programmatische Hälfte der App-Seiten-Konsole. Alles hier geht an den Agenten auf der Maschine über einen mTLS-Kanal.

Laufzeit-Logs – GET /api/applications/:id/logs?tail=200&process=web

Was die laufende App jetzt ausgibt – nicht der Build-Output (das ist /api/deploys/:id/logs). tail ist 1–5000 (Standard 200); process filtert auf einen Prozess. 409 mit {"error": "No running deployment"}, wenn noch nichts läuft, oder der Agent nicht erreichbar ist.

{
  "application_id": 11,
  "lines": [
    { "stream": "stdout", "process": "web", "line": "GET / 200 12ms", "at": "2026-09-06T12:00:01Z" }
  ]
}

Es gibt noch kein Server-Push-Follow – railyard logs --follow fragt in einem Intervall erneut an.

Prozesse – GET /api/applications/:id/processes

Zuletzt gemeldeter Zustand jedes Containers, aktualisiert bei jedem Agenten-Heartbeat. Leere Liste = nichts läuft.

{ "processes": [
  { "name": "web", "container": "railyard-app-11-web", "state": "running",
    "status": "running", "health": "healthy", "crash_looping": false,
    "restarts": 0, "cpu_percent": 1.2, "mem_bytes": 92210000,
    "mem_percent": 44.1, "last_seen_at": "2026-09-06T12:00:00Z" } ] }

status ∈ running · unhealthy · crash-looping · exited · dead · …

Restart – POST /api/applications/:id/restart[?process=web]

Startet jeden Container neu, oder nur den eines Prozesses. 409, wenn nichts läuft.

{ "restarted": 2, "processes": ["railyard-app-11-web", "railyard-app-11-worker-1"] }

Prozess-Typen – GET /api/applications/:id/process_types, PATCH /api/applications/:id/process_types/:pt_id

{ "id": 7, "name": "worker", "role": "worker", "command": "bundle exec sidekiq",
  "replicas": 2, "env_vars": {}, "kill_timeout_seconds": 30, "stop_signal": "",
  "size": "small", "cpu_limit": "0.5", "memory_limit": "512m", "cpus": "0.5", "memory": "512m" }

size ist small, medium, large (abgeleitet von PROCESS_SIZE_*), custom (dann cpu_limit und memory_limit senden) oder leer für das App-weite Limit. cpus / memory sind die effektiven Limits nach diesem Fallback.

kill_timeout_seconds (1–300, leer für den Standard) ist, wie lange der Prozess nach stop_signal (SIGTERM, SIGINT, SIGQUIT, leer für das des Images) Zeit bekommt, bevor er gekillt wird. Änderungen greifen beim nächsten Deploy. Ein ungültiger Wert → 422.

Einmalige Commands – POST /api/applications/:id/commands

curl -s -X POST https://cp.example.com/api/applications/11/commands \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"command":"bin/rails db:migrate"}'

Läuft im Web-Container über die RunTask-RPC des Agenten. 202 Accepted mit der Zeile; GET /api/applications/:id/commands/:cmd_id für status pollen (queued → running → succeeded/failed), gestreamter output, und exit_code. GET /api/applications/:id/commands listet letzte Läufe auf.

Interaktive console (ein TTY, Exec-RPC) ist noch nicht in der API.


Server (nur lesend)

curl -s https://cp.example.com/api/servers -H "Authorization: Bearer $RLY"

server-Objekt:

{
  "id": 5, "hostname": "railyard-8", "name": "railyard-8",
  "status": "online",
  "agent_version": "20260902-a9821d1",
  "cpu_percent": 1.5, "memory_percent": 64.5, "disk_percent": 40.3,
  "last_heartbeat_at": "2026-09-02T14:03:00Z",
  "cordoned": false
}

status ∈ online (Heartbeat < 30 s) · stale (< 5 Min) · offline. GET /api/servers/:id fügt apps[] (Namen) und processes[] {container, state, restarts, crash_looping} hinzu.

Cordon, Reboot, Power-Cycle, Prune und Resize eines bestehenden Servers sind POST/PATCH auf /api/servers/:id (siehe Tabelle oben). Das Hinzufügen eines neuen Servers geht derzeit nur über das Dashboard.

Wartungsfenster für Daten – GET/PATCH /api/maintenance_window (Team) und GET/PATCH /api/servers/:id/maintenance_window (ein Server; leerer Tag = Team-Standard verwenden). Felder: maintenance_day 0–6 (Sonntag = 0), maintenance_hour 0–23 UTC, maintenance_hours 1–12. Siehe Database servers.

Postgres-Major-Upgrades – GET/POST /api/servers/:id/postgres_upgrades (to_image; das Erstellen führt die Kompatibilitätsprüfung aus), GET …/:id (mit dem Log), PATCH …/:id (when=window, um auf das Wartungsfenster zu warten, sonst startet es sofort), POST …/:id/rollback, DELETE …/:id (entfernt die aufbewahrte alte Version, oder verwirft einen noch nicht gestarteten Plan).

Connection Pooler – GET/POST/DELETE /api/servers/:id/connection_pooler (PgBouncer, Transaction-Mode, Port 6432). Pro App: PATCH /api/applications/:id/database mit app_database[pooled]=true richtet DATABASE_URL ab dem nächsten Deploy auf den Pooler aus und behält DATABASE_DIRECT_URL für Migrationen.

Datenbank-Forks – GET/POST /api/applications/:id/database_forks (target_server_id für eine neue Datenbank oder target_application_id für eine Staging-/Preview-Umgebung, source=live|backup, optional mask_sql), GET …/:id (mit der URL des Forks, sobald fertig), DELETE …/:id (verwirft ihn).

Standby-Replicas – GET/POST /api/servers/:id/database_replicas (replica_server_id), POST …/:id/promote, DELETE …/:id.


Cloud

Railyard Cloud macht aus dem Railyard-Account eines Teams dessen eigene kleine Cloud: Server, Images, statische Adressen, eine DNS-Zone und S3-Buckets, abgerechnet zu Cloud-Kosten plus einem veröffentlichten Aufpreis. Es ist für Tools wie AliaJS gebaut, die die API eines Cloud- Providers erwarten, daher folgen die Formen Hetzner: Jede Antwort umschließt ihr Objekt ({"server": {...}}, {"servers": [...]}). Eine Karte muss hinterlegt sein (auf der Cloud-Seite hinzufügen), und die Endpunkte antworten mit 503, bis der Betreiber Railyard Cloud einschaltet.

MethodePfadScopeZweck
GET/api/cloud/sizesreadServergrößen mit Stundenkosten, Aufpreis und Preis
GET/api/cloud/ssh_keysreadSSH-Keys auflisten
POST/api/cloud/ssh_keyswriteEinen SSH-Key hinzufügen (201; 200, falls derselbe Key schon existiert)
DELETE/api/cloud/ssh_keys/:idwriteEinen SSH-Key entfernen
GET/api/cloud/serversreadLaufende Server auflisten
POST/api/cloud/serverswriteEinen Server starten (201, pending)
GET/api/cloud/servers/:idreadServer-Detail, bei jedem Lesen aktualisiert
DELETE/api/cloud/servers/:idwriteEinen Server terminieren (200, terminated)
POST/api/cloud/servers/:id/powerwriteEinen gestoppten Server starten
DELETE/api/cloud/servers/:id/powerwriteEinen laufenden Server stoppen
POST/api/cloud/servers/:id/imageswriteEinen Server als Snapshot in ein Image sichern (201, creating)
GET/api/cloud/imagesreadImages auflisten (?name= filtert)
GET/api/cloud/images/:idreadImage-Detail, bei jedem Lesen aktualisiert
DELETE/api/cloud/images/:idwriteEin Image löschen
GET/api/cloud/addressesreadStatische Adressen auflisten
POST/api/cloud/addresseswriteEine statische Adresse reservieren
GET/api/cloud/addresses/:idreadAdress-Detail
DELETE/api/cloud/addresses/:idwriteEine statische Adresse freigeben
POST/api/cloud/addresses/:id/assignmentwriteAn einen Server anhängen ({"server_id": 9})
DELETE/api/cloud/addresses/:id/assignmentwriteAbhängen
GET/api/cloud/zonereadDie DNS-Zone des Teams
GET/api/cloud/zone/recordsreadRecords auflisten
PUT/api/cloud/zone/records/:type/:namewriteEinen Record erstellen oder ersetzen
DELETE/api/cloud/zone/records/:type/:namewriteEinen Record entfernen
GET/api/cloud/bucketsreadBuckets auflisten
POST/api/cloud/bucketswriteEinen Bucket erstellen; die Antwort trägt den Storage-Key des Teams
DELETE/api/cloud/buckets/:idwriteEinen Bucket löschen (muss leer sein)
GET/api/cloud/usagereadDiese Monats-Nutzungszeilen mit Kosten, Aufpreis und Preis

Request-Bodies sind JSON und können flach ({"name": "web"}) oder umschlossen ({"server": {"name": "web"}}) sein. App-gebundene Tokens werden abgelehnt (403).

Fehler zusätzlich zu den üblichen:

StatusWann
402Keine Karte hinterlegt. Body: {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"}
403Read-only oder App-gebundenes Token, ein Team-Limit erreicht ("this team has reached its limit of 3 servers"), oder das monatliche Ausgabenlimit würde überschritten
409Konflikt: ein SSH-Key-Name mit einem anderen Key, ein bereits genutzter Bucket-Name, ein Bucket, der noch Objekte hat, ein CNAME-Konflikt, Starten eines Servers, der nicht gestoppt ist
422Ungültige Eingabe: unbekannte Größe, unbekanntes Image, Architektur-Mismatch zwischen Image/Größe, DNS-Name außerhalb deiner Zone, private IP in einem A-Record
502Die Cloud hat den Request abgelehnt; die Meldung erklärt warum
503Railyard Cloud ist nicht eingeschaltet

Größen: GET /api/cloud/sizes

{ "sizes": [
  { "name": "t4g.small", "vcpus": 2, "memory_gb": 2, "disk_gb": 20, "arch": "arm64",
    "hourly_cost": "0.023992", "markup_percent": "10.0", "hourly_price": "0.026391", "monthly_price": "19.27" }
] }

hourly_cost ist, was der Server Railyard kostet (Maschine, öffentliche IPv4 und Disk); hourly_price ist, was das Team zahlt: cost × (1 + markup_percent / 100).

Server

curl -s -X POST https://app.railyard.run/api/cloud/ssh_keys \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d "{\"name\":\"laptop\",\"public_key\":\"$(cat ~/.ssh/id_ed25519.pub)\"}"

curl -s -X POST https://app.railyard.run/api/cloud/servers \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"name":"n8n-production","size":"t4g.small","image":"ubuntu-24.04","ssh_keys":["laptop"]}'
{ "server": { "id": 9, "name": "n8n-production", "status": "pending", "size": "t4g.small", "arch": "arm64",
  "image": "ubuntu-24.04", "disk_gb": 20, "public_ipv4": null, "private_ipv4": null, "public_ipv6": null,
  "ssh_keys": ["laptop"], "hourly_price": "0.026391", "failure": null, "created_at": "2026-10-10T12:00:00Z" } }

GET /api/cloud/servers/9 alle paar Sekunden pollen, bis status running ist und public_ipv4 gesetzt ist (üblicherweise 10 bis 30 Sekunden), dann ssh ubuntu@<public_ipv4>. failed bedeutet, der Start wurde abgelehnt; failure erklärt warum. image ist ubuntu-24.04 oder der Name eines deiner eigenen Images, gebaut für dieselbe Architektur wie die Größe.

Images

curl -s -X POST https://app.railyard.run/api/cloud/servers/9/images \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"name":"n8n-base"}'

GET /api/cloud/images/:id pollen, bis status available ist (einige Minuten). Neue Server davon starten mit "image": "n8n-base". Ein ruhendes Image kostet nur seine gespeicherte Größe (stored_gb), weshalb eine Standby-Kopie eines Dienstes für Cent im Monat warten kann.

Adressen: eine statische IPv4, die das Ersetzen des dahinterstehenden Servers überlebt.

curl -s -X POST https://app.railyard.run/api/cloud/addresses -H "Authorization: Bearer $RLY"
# {"address":{"id":3,"ip":"198.51.100.9","server_id":null,"created_at":"..."}}

curl -s -X POST https://app.railyard.run/api/cloud/addresses/3/assignment \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"server_id":9}'

DNS-Zone: jedes Team erhält seine eigene Zone, daher ist keine eigene Domain nötig.

curl -s https://app.railyard.run/api/cloud/zone -H "Authorization: Bearer $RLY"
# {"zone":{"name":"acme.cloud.railyard.run"}}

curl -s -X PUT https://app.railyard.run/api/cloud/zone/records/A/n8n-production \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"values":["198.51.100.9"],"ttl":300}'
# {"record":{"id":11,"name":"n8n-production","fqdn":"n8n-production.acme.cloud.railyard.run","type":"A","values":["198.51.100.9"],"ttl":300,...}}
  • name ist relativ zur Zone (n8n-production) oder vollqualifiziert innerhalb von ihr; alles außerhalb deiner Zone gibt 422, und der Zonen-Apex ist reserviert.
  • Typen: A, AAAA, CNAME, TXT. TXT funktioniert für certbots DNS-01- Challenge: PUT /api/cloud/zone/records/TXT/_acme-challenge.n8n-production.
  • A-Werte müssen öffentliche Adressen sein. Ein CNAME kann sich seinen Namen nicht mit anderen Records teilen.
  • PUT ersetzt die Werte des Records; DELETE auf demselben Pfad entfernt ihn (204).

Buckets

curl -s -X POST https://app.railyard.run/api/cloud/buckets \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"name":"n8n-backups"}'
{ "bucket": { "id": 2, "name": "n8n-backups", "url": "s3:https://s3.us-east-2.amazonaws.com/rly-7-n8n-backups-ab12cd",
  "region": "us-east-2", "created_at": "2026-10-10T12:00:00Z", "access_key_id": "AKIA…", "secret_access_key": "…" } }

url ist in restics s3:-Form. Der Access-Key gehört dem Team und funktioniert nur auf den Buckets des Teams; jeder Bucket, den du erstellst, gibt denselben Key zurück, und das Auflisten von Buckets zeigt ihn nie. DELETE braucht den Bucket leer.

Nutzung: GET /api/cloud/usage

{ "usage": {
  "lines": [ { "description": "Server n8n-production (t4g.small)", "metric": "server_hours", "quantity": "1.0",
               "period_start": "2026-10-10T12:00:00Z", "cost_usd": "0.023992", "markup_percent": "10.0",
               "price_usd": "0.026391", "invoiced": false } ],
  "cost_usd": "0.023992", "price_usd": "0.026391", "our_cut_usd": "0.002399", "daily_price_usd": "2.21",
  "spend_limit_usd": "100.0" } }

Die Nutzung wird jede Stunde pro Ressource gemessen: Server-Stunden, ausgehender Traffic, Stunden statischer Adressen, Image-Storage und Bucket-Storage. daily_price_usd ist, was die letzten 24 gemessenen Stunden gekostet haben. Karten werden belastet, wenn der nicht abgerechnete Gesamtbetrag einen Schwellenwert überschreitet oder zu Beginn jedes Monats.


Templates

Vorgebaute One-Click-Apps – derselbe Katalog, den die “deploy a template”-Seite des Dashboards anbietet.

curl -s https://cp.example.com/api/templates -H "Authorization: Bearer $RLY"
{ "templates": [
  { "key": "n8n", "name": "n8n", "category": "automation", "verified": true,
    "addons": ["postgres"], "description": "Workflow automation" }
] }

Starten – POST /api/templates/:key/launch

curl -s -X POST https://cp.example.com/api/templates/n8n/launch \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"server_id":9}'
{ "application_id": 42, "name": "n8n", "url": "https://n8n.apps.example.com", "deploy_id": 101 }

server_id ist optional – weglassen, und Railyard wählt den ersten Online-App- Host. 201 mit dem bereits eingereihten ersten Deploy. 422, wenn das Team über seinem App-Limit liegt oder die Standardwerte des Templates die Validierung nicht bestehen.


Schnelle Rezepte

Bei jedem Push deployen (CI):

curl -fsS -X POST "$RAILYARD/api/applications/$APP_ID/deploys" \
  -H "Authorization: Bearer $RAILYARD_TOKEN" -H "Content-Type: application/json" \
  -d "{\"ref\":\"$GIT_SHA\"}"

Auf das Ende eines Deploys warten:

id=$(curl -fsS -X POST "$RAILYARD/api/applications/$APP_ID/deploys" \
      -H "Authorization: Bearer $TOKEN" -d '{}' | jq .id)
while :; do
  s=$(curl -fsS "$RAILYARD/api/deploys/$id" -H "Authorization: Bearer $TOKEN" | jq -r .status)
  case $s in succeeded) exit 0;; failed) exit 1;; esac
  sleep 5
done

Env aus einer Datei synchronisieren (KEY=value-Zeilen):

jq -Rn '[inputs | select(test("=")) | split("=") | {(.[0]): (.[1:] | join("="))}] | add | {env: .}' .env.prod \
| curl -fsS -X PATCH "$RAILYARD/api/applications/$APP_ID/env" \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-

Noch nicht in der API

Interaktive Console (ein TTY über die Exec-RPC), das Hinzufügen eines neuen Servers, Add-on-Verwaltung über das addons-Array hinaus, das Erstellen oder Bearbeiten von Benachrichtigungs- regeln / Webhook-Endpunkten / Konfigurationseinträgen (Auflisten ist heute nur lesend), Activity-Feed. Diese laufen heute nur über das Dashboard; eröffne ein Issue, falls du eines über die API brauchst.

MCP (KI-Agenten)

POST /api/mcp ist ein Model-Context-Protocol-Server (JSON-RPC über streamable HTTP), sodass KI-Agenten Railyard mit denselben API-Tokens nutzen können.

  • Ein read-Token kann Server und Apps auflisten, eine App anzeigen und ihre Logs lesen.
  • Ein write-Token kann außerdem deployen, neu starten und Env-Variablen setzen.
  • Das Löschen einer App oder das Entfernen einer Env-Variable läuft nie direkt: Es erstellt einen Freigabe-Request. Ein Team-Admin genehmigt oder lehnt ihn unter Approvals ab (er wird auch im Dashboard angezeigt).

Claude Code:

claude mcp add --transport http railyard https://app.railyard.run/api/mcp --header "Authorization: Bearer rly_..."

Cursor (.cursor/mcp.json):

{ "mcpServers": { "railyard": { "url": "https://app.railyard.run/api/mcp", "headers": { "Authorization": "Bearer rly_..." } } } }

Tools: list_servers, list_apps, get_app, app_logs, deploy_app, restart_app, set_env, unset_env (Freigabe), delete_app (Freigabe).