Zum Inhalt springen

Bereitstellung

Ein Rebase-Projekt wird als ein Server bereitgestellt (auf Rebase Cloud: https://<project>.rebase.website). Dieser Server übernimmt:

  • /api/* — die Daten-API, Authentifizierung, Echtzeit und Speicher
  • alles andere — Ihr gebautes frontend/ als statische SPA

Es gibt keinen separaten Admin-Dienst: Das Admin-Panel ist Teil eines Frontends, daher hängt es davon ab, was Ihr Frontend ist und ob Sie ihm einen eigenen Hostnamen geben, wo es erscheint.

Projekttyp Root-URL zeigt Admin-Panel befindet sich unter
Standard-Scaffold (rebase init) Das Admin-Panel / — das Frontend ist der Admin
Benutzerdefiniertes Produkt-Frontend Ihre App Wo Sie es einbinden, üblicherweise /admin — siehe Basis-URL ändern
Admin als App auf einem eigenen Hostnamen Ihre App https://admin.example.com/, vom selben Server ausgeliefert — siehe Basis-URL ändern
Reines Backend-Projekt Nichts (nur API) Nicht bereitgestellt

Das generierte Projekt enthält bereits eine funktionierende docker-compose.yml — diese Datei ist die, die man für ein gescaffoldetes Projekt nimmt, so wie sie ist, statt sie von Hand zu schreiben oder anderswo zu kopieren. rebase init hat ihre Secrets, ihr erstes Admin-Konto und ihre fixierte Runtime-Version eingetragen, und das Akzeptanz-Gate des Frameworks startet sie bei jedem Push. Sie betreibt zwei Container: Postgres und die veröffentlichte Rebase-Runtime, in die Ihr gebautes Bundle eingehängt wird. Es gibt kein Anwendungs-Image zu bauen.

Self-Hosting behandelt dieselbe Bereitstellung ohne Scaffold dahinter, mit infra/docker/docker-compose.selfhost.yml aus dem Rebase-Repository — und die zwei Dinge, die diese Datei bewusst weglässt: einen Connection Pooler und den Betrieb von Functions und Job-Worker als eigene Prozesse.

rebase build # produces ./dist-bundle
docker compose up -d

Immer zuerst rebase build: Der api-Dienst hängt ./dist-bundle ein, und ohne das startet der Container gegen ein leeres Verzeichnis.

Die Form der generierten Datei:

services:
db:
image: pgvector/pgvector:pg18
environment:
POSTGRES_USER: rebase_app
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
POSTGRES_DB: rebase
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rebase_app -d rebase"]
api:
# The published runtime. Upgrading Rebase is a tag change, not a rebuild.
image: rebasepro/server:${REBASE_VERSION:-latest}
depends_on:
db:
condition: service_healthy
ports:
- "${PORT:-3001}:3001"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
REBASE_SERVICE_KEY: ${REBASE_SERVICE_KEY:?set REBASE_SERVICE_KEY in .env}
CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS in .env}
# This service runs in production, where the first account to register is
# not promoted to admin. So the admin is named instead.
REBASE_ADMIN_EMAIL: ${REBASE_ADMIN_EMAIL:?set REBASE_ADMIN_EMAIL in .env}
REBASE_ADMIN_PASSWORD: ${REBASE_ADMIN_PASSWORD:?set REBASE_ADMIN_PASSWORD in .env}
DISABLE_SELF_REGISTRATION: ${DISABLE_SELF_REGISTRATION:-true}
volumes:
# Your built project, from `rebase build`. Read-only: the build vendors
# the bundle's dependencies by default, so nothing has to write here.
- ./dist-bundle:/bundle:ro
volumes:
postgres_data:

Die drei REBASE_ADMIN_*- / DISABLE_SELF_REGISTRATION-Zeilen sind neu — in 0.17.3 wird das erste registrierte Konto zum Administrator, auch in der Produktion. Siehe Ihr erster Administrator weiter unten.

Das Bundle wird schreibgeschützt eingehängt. rebase build installiert die deklarierten Abhängigkeiten des Projekts in dist-bundle, sofern Sie nicht --no-vendor übergeben — in dem Fall installiert die Runtime sie bei jedem Start und die Einhängung muss beschreibbar sein: lassen Sie dann das :ro weg. Siehe Self-Hosting.

rebase init schreibt all das für Sie in die .env, einschließlich eines generierten Admin-Passworts. Jede Variable ist mit ${VAR:?…} deklariert, sodass eine fehlende den Stack mit einer Meldung anhält, die sie benennt, statt etwas halb Konfiguriertes zu starten — und Compose interpoliert die ganze Datei, bevor es Dienste auswählt, sodass eine fehlende auch docker compose up -d db anhält.

Ändern Sie die Admin-E-Mail auf Ihre, melden Sie sich an und ändern Sie das Passwort. Siehe Ihr erster Administrator.

Die Laufzeitumgebung legt beim Start fehlende Tabellen an, auch die Ihrer Collections — REBASE_MIGRATE_ON_BOOT steht standardmäßig auf ensure, was über das gesamte Schema additiv ist und die Row-Level-Security gleich mit anwendet. Ein erstes docker compose up gegen eine leere Datenbank kommt hoch und bedient Ihre Collections.

Was der Start nie tut, ist etwas zu ändern, das bereits existiert: Er ändert keinen Spaltentyp, verwirft nichts und bearbeitet die Labels eines bestehenden Enums nicht, denn ein Container-Neustart darf ein Schema nicht als Nebeneffekt eines Deploys umformen. Das läuft über die CLI, aus einem Checkout oder einem CI-Job, der auf die Produktionsdatenbank zeigt:

pnpm run db:push

Führen Sie das für jede Änderung aus, die nicht rein additiv ist — eine umbenannte Spalte, ein verengter Typ, ein entferntes Feld.

Für einen versionierten Team-Workflow committen Sie stattdessen Migrationsdateien mit pnpm run db:generate und führen pnpm run db:migrate als Release-Schritt aus. So oder so läuft es aus einem Projekt-Checkout, nicht im laufenden Container — das Runtime-Image wird ohne die CLI ausgeliefert.

Setzen Sie REBASE_ADMIN_EMAIL und REBASE_ADMIN_PASSWORD vor dem ersten Start. Jede Plattformanleitung auf dieser Seite verweist hierher, denn dies ist der eine Schritt, für den es von außen keine Reparatur gibt.

Eine frische Datenbank hat keine Benutzer, und außerhalb der Produktion lässt die Registrierungsrichtlinie die erste Anmeldung zu und macht sie zum Administrator. Das muss so sein: Um einen Administrator zu ernennen, braucht es einen bereits angemeldeten Aufrufer — eine leere Datenbank ohne diese Regel ist eine Sackgasse. Auf einem Laptop ist die Person an der Tastatur der Betreiber, und genau das ist richtig so.

Auf einem Host mit öffentlichem Namen ist es genau falsch. Die ausgelieferten Artefakte bringen DNS und TLS hoch, bevor der Betreiber irgendetwas getippt hat — das Fenster steht also ab der ersten Sekunde im Internet offen, und wer das Registrierungsformular zuerst erreicht, besitzt die Bereitstellung.

Unter NODE_ENV=production ist dieses Fenster daher geschlossen. Eine leere Benutzertabelle weist die Bootstrap-Registrierung mit SETUP_REQUIRED ab, ein über offene Registrierung angelegtes Konto ist ein gewöhnliches Konto, GET /api/auth/config meldet nie needsSetup, und POST /api/admin/bootstrap verweigert. In 0.17.3 und früher war das Fenster auch in der Produktion offen — aktualisieren Sie, bevor Sie eine frische Bereitstellung öffentlich machen.

rebase dev liest dieselbe .env, ignoriert beide Variablen aber bewusst und sagt es beim Start: lokal bleibt die erste Registrierung der Weg hinein. Die Werte, die rebase init geschrieben hat, gehören dem Produktionsstart. Auf beiden Seiten zu seeden würde das Fenster verbrauchen, bevor die Entwicklerin die App überhaupt geöffnet hat — genau das hat den ersten Schritt des Quickstarts ein Konto ohne Rolle erzeugen lassen.

Damit bleiben zwei Wege hinein, von denen keiner ein Wettlauf ist:

REBASE_ADMIN_EMAIL=you@example.com
REBASE_ADMIN_PASSWORD=<at least 12 characters>
DISABLE_SELF_REGISTRATION=true

Die Laufzeitumgebung legt dieses Konto einmal an, solange die Benutzertabelle leer ist, und tut bei jedem weiteren Start nichts. Oder weisen Sie einem bestehenden Benutzer die Rolle mit dem Service-Key zu, wenn Sie Konten außerhalb der Anwendung bereitstellen.

Zwei Regeln erzwingt die Laufzeitumgebung beim Start, und beide erzeugen sonst ein Konto, das niemand benutzen kann:

  • Das Passwort muss mindestens 12 Zeichen haben, sonst wird es abgelehnt und kein Konto angelegt.
  • Die Adresse muss eine sein, die POST /api/auth/login akzeptiert — die Route prüft ihren Body mit z.string().email(), sodass eine Domain ohne Punkt (admin@localhost) sauber angelegt wird und danach bei jeder Anmeldung mit 400 antwortet. Auch diese Adresse verweigert der Start.

Setzen Sie beide oder keine: eine halbe Zugangsdatenangabe ist ein Tippfehler, und die daraus entstehende Bereitstellung — Selbstregistrierung aus, kein Administrator — lässt sich nur an einer psql-Konsole retten. Der Start warnt, wenn die Tabelle in der Produktion leer ist und kein Administrator benannt wurde.

Melden Sie sich an und ändern Sie das Passwort. Es liegt im Klartext dort, wo Sie Ihre Umgebungsvariablen abgelegt haben.

Bevor Sie in die Produktion bereitstellen, stellen Sie sicher:

Punkt Details
Erster Administrator Setzen Sie REBASE_ADMIN_EMAIL und REBASE_ADMIN_PASSWORD vor dem ersten Start, dazu DISABLE_SELF_REGISTRATION=true. In der Produktion wird das erste registrierte Konto nicht befördert — siehe Ihr erster Administrator.
NODE_ENV NODE_ENV=production. Das ist es, was das Bootstrap-Fenster schließt, lokalen Dateispeicher ablehnt, CORS_ORIGINS verlangt und die OpenAPI-Doku abschaltet. Eine Bereitstellung, die auf dem Standardwert bleibt, läuft im Entwicklungsmodus.
Datenbankschema Der Start legt Ihre Collection-Tabellen und deren RLS additiv an. Führen Sie pnpm run db:push (oder pnpm run db:migrate) für alles aus, was nicht rein additiv ist.
JWT_SECRET Verwenden Sie eine kryptografisch starke Zufallszeichenkette (≥ 32 Zeichen). Niemals über Umgebungen hinweg wiederverwenden.
DATABASE_URL Verwenden Sie eine verwaltete Postgres-Instanz (Neon, Supabase, RDS) mit aktiviertem TLS
CORS_ORIGINS Immer, nicht nur wenn das Frontend auf einer anderen Domain liegt. Die Laufzeitumgebung verweigert den Start in der Produktion, wenn weder CORS_ORIGINS noch FRONTEND_URL gesetzt ist, denn eine API, die ihre erlaubten Origins errät, erlaubt irgendwann die falsche.
Zugriffskontrolle für Storage Ein konfigurierter Bucket verweigert in der Produktion den Start ohne ein Zugriffskontrollmodell. Storage unterliegt keiner Row-Level-Security und seine Schlüssel teilen sich einen flachen Namensraum, sodass ein Alles-erlauben-Standard jedem angemeldeten Benutzer erlaubt aufzulisten (GET /storage/list?prefix=) und danach die Dateien aller anderen zu lesen, zu überschreiben oder zu löschen. Erfüllen Sie es mit einem storageAuthorize-Hook oder mit storagePolicies (das Scaffold liefert einen Hook in config/storage.ts mit), oder erklären Sie die Absicht mit STORAGE_PUBLIC_READ für ein echtes öffentliches CDN oder STORAGE_ALLOW_ANY_AUTHENTICATED für eine Single-Tenant-App, in der jedem Konto jede Datei anvertraut wird.
Storage-Backend STORAGE_TYPE=local wird in der Produktion fallen gelassen, und Uploads antworten mit 501 STORAGE_NOT_CONFIGURED — das Container-Dateisystem wird beim nächsten Neustart zerstört, ein lokales Backend ist also stiller Datenverlust. Verwenden Sie s3 oder gcs, oder setzen Sie FORCE_LOCAL_STORAGE=true, wenn der Pfad wirklich ein dauerhaftes Volume ist.
MFA_ENCRYPTION_KEY Setzen Sie ihn (32+ zufällige Zeichen), wenn Sie TOTP verwenden. Ohne ihn werden gespeicherte Secrets mit JWT_SECRET verschlüsselt — dessen Rotation meldet also alle ab und macht jeden eingerichteten Authenticator unentschlüsselbar.
HTTPS Terminieren Sie TLS an Ihrem Reverse-Proxy (nginx, Cloudflare, Load Balancer)
Öffentliche Lesezugriffe brauchen trotzdem einen Aufrufer access: "public" erweitert, welche Zeilen ein Aufrufer sieht, nicht wer aufrufen darf: Eine anonyme Anfrage an /api/data/* antwortet mit 401, solange AUTH_REQUIRE aktiv ist. Setzen Sie AUTH_REQUIRE=false für eine öffentliche Website, die ihr eigenes Backend liest, und überlassen Sie die Entscheidung allein RLS. Es ist eine Umgebungsvariable — eine lokale .env, die sie setzt, reist also nicht mit Ihrer Bereitstellung mit.

Die verwaltete Laufzeitumgebung von Rebase Cloud führt Ihr Bundle in einem gemeinsam genutzten Image aus. Dort gibt es keinen Compiler und keine Möglichkeit, ein natives Modul zu laden — also alles, was eine vorkompilierte .node-Binärdatei mitbringt. Das mit Abstand häufigste ist sharp, zugleich die naheliegende Abhängigkeit für alles, was Bilder ausliefert.

rebase cloud deploy lehnt das vor dem Upload ab, nicht danach:

This bundle depends on native modules (sharp), which the managed runtime cannot run

Drei Wege hindurch, in der Reihenfolge, in der sie meist richtig sind:

  1. Verlagern Sie die Arbeit in den Build. Skalieren und kodieren Sie Bilder in Ihrem Build-Schritt und stellen Sie die Ergebnisse bereit. Im Anfrage-Pfad läuft dann nichts Natives.
  2. Nutzen Sie einen Dienst. Ein Bild-CDN oder eine Transform-API erledigt dieselbe Arbeit hinter einer URL.
  3. Betreiben Sie Ihren eigenen Container. Eine selbst gehostete Bereitstellung (Docker, Kubernetes, jede der Plattformanleitungen) ist Ihr Image und darf deshalb mitbringen, was sie will.

Funktionen, die lediglich Node brauchen und keine native Binärdatei, sind unproblematisch — die Bereitstellung meldet diese getrennt (1 of 3 function(s) depend on Node) und führt sie aus.

In der Produktion kann das Backend das Frontend als statische SPA ausliefern:

import { serveSPA } from "@rebasepro/server";
import path from "path";
// After initializeRebaseBackend()
serveSPA(app, { frontendPath: path.resolve(process.cwd(), "../frontend/dist") });

Bauen Sie zuerst das Frontend:

cd frontend && pnpm build

Auf diese Weise müssen Sie nur einen Server bereitstellen, der sowohl SPA als auch API übernimmt.

Detaillierte Schritt-für-Schritt-Anleitungen für jede Plattform:

Plattform Typ Anleitung
AWS App Runner / ECS + RDS Auf AWS bereitstellen →
Google Cloud Cloud Run + Cloud SQL Auf GCP bereitstellen →
Azure Container Apps + PostgreSQL Auf Azure bereitstellen →
Hetzner Cloud VPS + Docker Compose Auf Hetzner bereitstellen →
Scaleway Serverless-Container Auf Scaleway bereitstellen →
Railway PaaS (Dockerfile automatisch erkannt) Auf Railway bereitstellen →
Fly.io Container-Runtime Auf Fly.io bereitstellen →

Wenn die Administration unter einem Unterpfad laufen soll (z. B. /admin), ändern Sie den path der App in rebase.json und mit ihm ihr cms:

"admin": {
"type": "static",
"root": "frontend",
"build": "npm run build --workspace frontend",
"output": "frontend/dist",
"path": "/admin",
"cms": "/admin"
}

cms gibt an, wo das CMS liegt, worauf Rebase Cloud verlinkt, und muss innerhalb des path der App liegen. Das Scaffold setzt beide auf /, daher wird es abgelehnt, nur path zu verschieben — das CMS läge dann unter /, außerhalb einer App, die jetzt bei /admin beginnt.

Um der Administration statt eines Unterpfads einen eigenen Hostnamen zu geben, machen Sie path zu einer vollständigen URL — "path": "https://admin.example.com" — und lassen cms auf "/". Die Administration antwortet dann nur auf diesem Hostnamen und nicht mehr unter der eigenen Adresse des Projekts, ist also nicht erreichbar, bis das DNS des Hostnamens verifiziert ist. Die API bleibt auch auf diesem Hostnamen unter /api. Unter Eine App auf einem eigenen Hostnamen steht, wie Anfragen zugeordnet werden und was ein Deployment auf Rebase Cloud mit dem Hostnamen macht.

rebase build übergibt path an Vite als base (über REBASE_APP_BASE), Vite gibt ihn als import.meta.env.BASE_URL zurück, und die main.tsx des Scaffolds reicht ihn bereits an den Router weiter — so stimmen Assets, Routen und Server überein, ohne dass das Präfix an drei Stellen steht:

// At "/" this is "".
const basename = import.meta.env.BASE_URL.replace(/\/$/, "");
const router = createBrowserRouter([
{
path: "/*",
element: <App/>
}
], { basename });

Betreiben Sie die Administration auf einem Data Router — createBrowserRouter mit <RouterProvider>, wie es das Scaffold tut. Unter ihm stoppt ein Datensatz mit ungespeicherten Änderungen auch die In-App-Navigation (ein Link im Drawer, die Zurück-Schaltfläche des Browsers) und fragt zuerst nach, über useBlocker, den nur ein Data Router bereitstellt.

Seit 0.25 Die Administration läuft auch unter einem einfachen <BrowserRouter>. Dort fällt der Schutz vor ungespeicherten Änderungen auf die eigene Verlassen-Abfrage des Browsers zurück, die einen Reload, das Schließen des Tabs oder die Eingabe einer URL abdeckt; die Navigation innerhalb der App geht ohne Nachfrage weiter. Unter 0.24 wirft die Administration außerhalb eines Data Routers einen Fehler.

Backend — wenn Sie auch die API verschieben, passen Sie deren Basispfad an:

await initializeRebaseBackend({
// ...
basePath: "/admin/api"
});

Der häufige Grund, den Admin nach /admin zu verschieben, ist die Auslieferung Ihrer eigenen Produkt-App im Root derselben Bereitstellung. Ein einziger Vite-Einstiegspunkt kann beide ausliefern, nach URL aufgeteilt, sodass jede App lazy geladen wird und Produktbesucher niemals das Admin-Bundle herunterladen:

const isAdmin = window.location.pathname.startsWith("/admin");
const ProductApp = lazy(() => import("./App"));
const AdminApp = lazy(() => import("./AdminApp"));
const router = isAdmin
// The admin lives under /admin, and `basename` is how the router is told.
? createBrowserRouter([{ path: "/*", element: <AdminApp/> }], { basename: "/admin" })
: createBrowserRouter([{ path: "/*", element: <ProductApp/> }]);
root.render(<RouterProvider router={router}/>);

Ein Router für beide Hälften: Die Administration ist auf einem Data Router besser aufgehoben, der ungespeicherte Änderungen die In-App-Navigation stoppen lässt, und es gibt keinen Grund, die Produkt-App auf einen anderen zu setzen.

Das Backend benötigt für dieses Muster keine Änderungen — die API bleibt unter /api und der SPA- Catch-all liefert index.html sowohl für / als auch für /admin/*.