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/jsonbei 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
| Scope | Kann |
|---|---|
read | jeden GET |
write | alles, 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:400Bad Request ·401nicht authentifiziert ·403falscher Scope ·404nicht gefunden (oder nicht in deinem Team) ·409Konflikt (z. B. läuft bereits ein Deploy) ·422Validierung fehlgeschlagen ·429Rate-Limit. - Zeitstempel – ISO-8601 UTC (
2026-09-02T14:03:11Z). - IDs – Integer, pro Team stabil.
- Pagination – Listen-Endpunkte, die
limitakzeptieren, nehmen1–100(Standard30). 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
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | /api/me | read | Token- und Team-Info, Limits, Zähler |
| GET | /api/applications | read | Apps auflisten |
| POST | /api/applications | write | Eine App erstellen |
| GET | /api/applications/:id | read | App-Detail |
| PATCH | /api/applications/:id | write | Eine App aktualisieren |
| DELETE | /api/applications/:id | write | Eine App löschen (und ihre Deploys/Logs) |
| POST | /api/applications/:id/archive | write | Container stoppen, Datensatz behalten |
| POST | /api/applications/:id/unarchive | write | Archivierung aufheben |
| GET | /api/applications/:id/env | read | Env-Variablen lesen |
| PATCH | /api/applications/:id/env | write | Env-Variablen zusammenführen (null-Wert löscht) |
| DELETE | /api/applications/:id/env?key=KEY | write | Eine Env-Variable löschen |
| GET | /api/applications/:id/edge | read | Edge-Einstellungen (gleiche Form wie der edge:-Abschnitt des Manifests, siehe Edge rules) |
| PATCH | /api/applications/:id/edge | write | Edge-Einstellungen zusammenführen: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}} |
| GET | /api/applications/:id/certificates | read | Hochgeladene Zertifikate (Namen, abgedeckte Hosts, Gültigkeit, Fingerprint; nie der Key) |
| POST | /api/applications/:id/certificates | write | Hochladen {"certificate":{"certificate_pem":"…","private_key_pem":"…"}} |
| DELETE | /api/applications/:id/certificates/:cert_id | write | Eines entfernen (automatische Zertifikate nach dem nächsten Deploy) |
| GET | /api/applications/:id/domains | read | Eigene Domains auflisten |
| POST | /api/applications/:id/domains | write | Eigene Domain hinzufügen |
| DELETE | /api/applications/:id/domains/:domain_id | write | Eigene Domain entfernen |
| GET | /api/applications/:id/deploys | read | Deploys für eine App |
| POST | /api/applications/:id/deploys | write | Einen Deploy auslösen ({"image": "registry/repo:tag"} deployt dieses vorgebaute Image einmalig) |
| GET | /api/applications/:id/registry | write | Die Build-Registry des Teams für deploy --local: repository, username, password, platform |
| POST | /api/applications/:id/tunnel | write | Einen 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/logs | read | Laufzeit-Container-Logs (?tail=, ?process=) |
| GET | /api/applications/:id/processes | read | Zustand pro Container (für ps) |
| POST | /api/applications/:id/restart | write | Jeden Container neu starten, oder einen einzelnen mit ?process= |
| GET | /api/applications/:id/process_types | read | Prozess-Typen mit Env-, Shutdown- und Größen-Einstellungen |
| PATCH | /api/applications/:id/process_types/:pt_id | write | env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit setzen |
| GET | /api/applications/:id/commands | read | Letzte einmalige Command-Läufe |
| POST | /api/applications/:id/commands | write | Einen einmaligen Command ausführen ({"command": "..."}) |
| GET | /api/applications/:id/commands/:cmd_id | read | Ein Lauf: Status, Output, Exit-Code |
| POST | /api/applications/:id/reindex | write | Die 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/clone | write | Die App in eine Staging-Kopie klonen |
| POST | /api/applications/:id/promote | write | Den Release einer Staging-App in Production befördern |
| GET | /api/applications/:id/manifest | read | Die App als railyard.yml (?secrets=true schließt Variablenwerte ein) |
| POST | /api/applications/:id/apply_manifest | write | Ein Manifest anwenden ({"manifest": "<yaml text>"}) |
| POST | /api/applications/validate_manifest | read | Ein 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_query | read / write | SQL ausführen ({"sql": "...", "write": false}); write: true braucht ein write-Token |
| GET/POST | /api/applications/:id/database_backups | read / write | Backups auflisten / jetzt eines erstellen |
| POST | /api/database_backups/:id/restore | write | Ein Backup wiederherstellen |
| GET | /api/database_backups/:id/download | read | Signierter Download-Link |
| GET/POST | /api/applications/:id/volume_backups | read / write | Backups auflisten / ein Volume sichern ({"volume": "name"}) |
| POST | /api/volume_backups/:id/restore | write | Ein Volume-Backup wiederherstellen |
| GET | /api/applications/:id/traffic | read | Requests, p95, Fehlerrate und langsamste Endpunkte (?range=1h|24h|7d|30d) |
| PATCH | /api/applications/:id/autoscale | write | Web-Replicas nach p95/CPU skalieren ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} schaltet es aus |
| POST | /api/applications/:id/data_move | write | Die Daten der App auf einen Datenbankserver verschieben ({"database_server_id": 12}); 202 mit dem, was kopiert wird |
| POST | /api/applications/:id/external_datastores | write | database, 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/:kind | write | Diese Daten zurück unter Railyard bringen (?copy=true kopiert sie zurück) |
| GET | /api/applications/:id/addons/:kind/transfers | read | Letzte Exports/Imports eines Add-ons (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), mit Download-Links |
| POST | /api/applications/:id/addons/:kind/transfers | write | Die Daten des Add-ons exportieren; mit multipart file + confirm=<app name>, eine Datei darin laden |
| GET | /api/applications/:id/storage/objects | read | Den Bucket des Storage-Add-ons durchsuchen (?prefix=, ?token=): Ordner, Dateien mit Größe, Public-Flag und einem signierten Link |
| POST | /api/applications/:id/storage/objects | write | Eine Datei hochladen (multipart file, prefix oder key) |
| DELETE | /api/applications/:id/storage/object?key= | write | Eine Datei löschen, oder einen Ordner, wenn der Key auf / endet |
| PATCH | /api/applications/:id/storage/visibility | write | Einen Ordner öffentlich oder privat machen ({"prefix":"avatars/","public":true}) |
| GET | /api/applications/:id/redis | read | Wo das Redis der App liegt (geteilte DB N oder eigenes), ihr Limit und ihre Policy, genutzter Speicher |
| PATCH | /api/applications/:id/redis | write | {"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/deploys | read | Deploys im ganzen Team (?application_id=, ?limit=) |
| GET | /api/deploys/:id | read | Deploy-Detail |
| GET | /api/deploys/:id/logs | read | Deploy-Log-Zeilen (älteste zuerst, ≤5000) |
| GET | /api/applications/:id/log_search | read | Gespeicherte Log-Zeilen: Zeitraum, Suche, Cursor-Seiten (siehe Logs, metrics and alerts) |
| GET | /api/applications/:id/log_download | read | Gespeicherte Log-Zeilen für einen Zeitraum als Text |
| GET/PATCH/DELETE | /api/applications/:id/otel_export | read / write | OpenTelemetry-Export-Einstellungen (siehe Logs, metrics and alerts) |
| GET/POST | /api/applications/:id/metric_alerts | read / write | Metrik-Schwellenwert-Alerts und ihre Historie |
| PATCH/DELETE | /api/applications/:id/metric_alerts/:alert_id | write | Einen Alert ändern oder entfernen |
| GET | /api/applications/:id/job_queue | read | Job-Queue-Statistiken, fehlgeschlagene Jobs, Historie (siehe Logs, metrics and alerts) |
| POST | /api/applications/:id/job_queue/failed/:job_id/retry | write | Einen fehlgeschlagenen Job erneut versuchen |
| DELETE | /api/applications/:id/job_queue/failed/:job_id | write | Einen fehlgeschlagenen Job verwerfen |
| GET | /api/servers | read | Die Flotte |
| GET | /api/servers/:id | read | Server-Detail + Live-Prozess-Zustände |
| POST | /api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_docker | write | Server-Lifecycle-Aktionen |
| PATCH | /api/servers/:id/resize | write | Beim Provider resizen ({"size": "..."}) |
| POST | /api/servers/:id/trust_ip | write | Eine 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_rules | read | Team-weite Benachrichtigungsregeln |
| GET | /api/webhook_endpoints | read | Team-weite Webhook-Endpunkte |
| GET | /api/config | read | Team-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(oderserver_id) kann eine Server-ID oder ein Hostname sein. Weglassen, und die App landet auf dem ersten uncordoned Server im Team.- Ein leerer
hostnamewird abgeleitet –<name>.<server app_domain>, sonstAPP_DOMAIN_SUFFIX, sonst<name>.<server-ip>.sslip.io. - Erstellen deployt nicht. Danach
POST …/deploysausfü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 dergit_refder App, den einrefnie ändert. Dertriggerdes Deploys istref, und seinref-Feld zeigt, was verwendet wurde. Namen mit Zeichen, die git Refs nicht haben können, bekommen422. Wird bei einerimage://-App ignoriert.message(optional) – eine Deploy-Notiz (bis zu 500 Zeichen), wird alsmessagezurückgegeben und im Dashboard, in der Activity, in Benachrichtigungen und dendeploy.*-Webhooks angezeigt.at(optional) – ISO-8601-Zeit, zu der statt jetzt deployt werden soll. Antwortet mit201mitstatus: "scheduled"undscheduled_for; die Zeit muss in der Zukunft liegen und innerhalb des Deploy-Fensters (sonst422). Abbrechen mitPOST /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 mit409undoutside_window: trueundwindow_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ügtrestored_confighinzu."restore_config": falsesenden, 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 mitstatus: "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.422mitmissing_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": truesenden, 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.
| Methode | Pfad | Scope | Zweck |
|---|---|---|---|
| GET | /api/cloud/sizes | read | Servergrößen mit Stundenkosten, Aufpreis und Preis |
| GET | /api/cloud/ssh_keys | read | SSH-Keys auflisten |
| POST | /api/cloud/ssh_keys | write | Einen SSH-Key hinzufügen (201; 200, falls derselbe Key schon existiert) |
| DELETE | /api/cloud/ssh_keys/:id | write | Einen SSH-Key entfernen |
| GET | /api/cloud/servers | read | Laufende Server auflisten |
| POST | /api/cloud/servers | write | Einen Server starten (201, pending) |
| GET | /api/cloud/servers/:id | read | Server-Detail, bei jedem Lesen aktualisiert |
| DELETE | /api/cloud/servers/:id | write | Einen Server terminieren (200, terminated) |
| POST | /api/cloud/servers/:id/power | write | Einen gestoppten Server starten |
| DELETE | /api/cloud/servers/:id/power | write | Einen laufenden Server stoppen |
| POST | /api/cloud/servers/:id/images | write | Einen Server als Snapshot in ein Image sichern (201, creating) |
| GET | /api/cloud/images | read | Images auflisten (?name= filtert) |
| GET | /api/cloud/images/:id | read | Image-Detail, bei jedem Lesen aktualisiert |
| DELETE | /api/cloud/images/:id | write | Ein Image löschen |
| GET | /api/cloud/addresses | read | Statische Adressen auflisten |
| POST | /api/cloud/addresses | write | Eine statische Adresse reservieren |
| GET | /api/cloud/addresses/:id | read | Adress-Detail |
| DELETE | /api/cloud/addresses/:id | write | Eine statische Adresse freigeben |
| POST | /api/cloud/addresses/:id/assignment | write | An einen Server anhängen ({"server_id": 9}) |
| DELETE | /api/cloud/addresses/:id/assignment | write | Abhängen |
| GET | /api/cloud/zone | read | Die DNS-Zone des Teams |
| GET | /api/cloud/zone/records | read | Records auflisten |
| PUT | /api/cloud/zone/records/:type/:name | write | Einen Record erstellen oder ersetzen |
| DELETE | /api/cloud/zone/records/:type/:name | write | Einen Record entfernen |
| GET | /api/cloud/buckets | read | Buckets auflisten |
| POST | /api/cloud/buckets | write | Einen Bucket erstellen; die Antwort trägt den Storage-Key des Teams |
| DELETE | /api/cloud/buckets/:id | write | Einen Bucket löschen (muss leer sein) |
| GET | /api/cloud/usage | read | Diese 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:
| Status | Wann |
|---|---|
402 | Keine Karte hinterlegt. Body: {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"} |
403 | Read-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 |
409 | Konflikt: 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 |
422 | Ungü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 |
502 | Die Cloud hat den Request abgelehnt; die Meldung erklärt warum |
503 | Railyard 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,...}}
nameist relativ zur Zone (n8n-production) oder vollqualifiziert innerhalb von ihr; alles außerhalb deiner Zone gibt422, 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.PUTersetzt die Werte des Records;DELETEauf 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).