| name | rebase-deployment |
| description | Guide for deploying Rebase applications. Use this skill when the user needs to deploy to Rebase Cloud, set up Docker, self-host on AWS/GCP/Hetzner, or use a PaaS like Railway or Render. |
Rebase Deployment
Rebase supports multiple deployment strategies — from fully managed Rebase Cloud to self-hosted Docker deployments.
Deployment Options
| Option | Best For | Complexity |
|---|
| Rebase Cloud | Fastest setup, managed infrastructure | ⭐ Easy |
| Docker (Self-Hosted) | Full control on any VPS or bare metal | ⭐⭐ Medium |
| AWS | ECS/Fargate or EC2 with RDS PostgreSQL | ⭐⭐⭐ Advanced |
| GCP | Cloud Run with Cloud SQL PostgreSQL | ⭐⭐⭐ Advanced |
| Azure | Container Apps with Azure Database for PostgreSQL | ⭐⭐⭐ Advanced |
| Scaleway | Serverless Containers with Managed PostgreSQL (EU-only) | ⭐⭐⭐ Advanced |
| Hetzner | Cost-effective VPS with Docker Compose | ⭐⭐ Medium |
| PaaS | Railway, Render, Fly.io — Docker-based platforms | ⭐⭐ Medium |
Rebase Cloud
The simplest deployment path. Sign up at app.rebase.pro.
rebase login
rebase init
rebase deploy
rebase deploy --env dev
What a Cloud Deployment Serves
rebase deploy ships one container per project, served at https://<project>.apps.rebase.pro. That container runs your backend, which handles:
/api/* — the data API, auth, realtime, storage
- everything else — your built
frontend/ as a static SPA (via serveSPA(), see below)
There is no separate admin URL — the admin panel is part of your frontend, so where it appears depends on what your frontend is:
| Project type | What the root URL shows | Where the admin is |
|---|
Default scaffold (rebase init, cms flavor) | The admin panel itself (login / bootstrap) | / — the frontend is the admin |
| Custom product frontend | Your product app | Wherever you mount it — commonly /admin (see below) |
Backend-only (--flavor backend) | Nothing (API only) | Not deployed |
IMPORTANT FOR AGENTS: On the first visit to a freshly deployed project's admin, Rebase shows the bootstrap screen ("Create your admin account"). The earliest-registered account receives admin privileges — the project owner should claim it immediately after deploying. Never fill this form on the user's behalf.
Mounting the admin at /admin alongside a product app — a single Vite entry can serve both, split by URL, so visitors never download the admin bundle:
const isAdmin = window.location.pathname.startsWith("/admin");
const ProductApp = lazy(() => import("./App"));
const AdminApp = lazy(() => import("./AdminApp"));
Set either a router basename="/admin" or <RebaseAdmin basePath="/admin"> — not both, or the prefix is applied twice.
Docker (Self-Hosted)
Production Dockerfile (Multi-Stage)
The production Dockerfile is a multi-stage build. The build context is the monorepo root (where pnpm-workspace.yaml lives), not the app directory.
IMPORTANT FOR AGENTS: The Dockerfile uses node:22-alpine, corepack enable (NOT npm install -g pnpm), and requires the monorepo root as the build context. Never generate a single-stage Dockerfile or use node:20.
# ── Stage 1: Install + Build ─────────────────────────────────────────
FROM node:22-alpine AS builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV CI=true
RUN corepack enable
RUN apk add --no-cache python3 make g++
WORKDIR /app
# Copy workspace root
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Copy source packages and app
COPY packages ./packages
COPY app ./app
# Install all dependencies with flat node_modules for Docker compatibility
# (many packages rely on hoisted devDependencies like @vitejs/plugin-react)
RUN pnpm install --shamefully-hoist
# Build all packages using pnpm recursive with --no-bail.
# Some packages may fail tsc declarations — that's fine, we only need vite bundles + esbuild outputs.
RUN pnpm --filter './packages/*' -r --no-bail run build; exit 0
# Build the backend (TypeScript → JavaScript), then resolve ESM import extensions
# 1. tsc compiles TS→JS 2. tsc-alias resolves path aliases 3. sed adds .js to remaining relative imports
RUN cd app/backend && npx tsc -p tsconfig.docker.json && npx tsc-alias -p tsconfig.docker.json -f \
&& find dist -name '*.js' -exec sed -i 's/from "\(\.[^"]*\)"/from "\1.js"/g; s/\.js\.js/.js/g' {} +
# Build frontend (reads .env.production for VITE_API_URL etc.)
RUN cd app/frontend && npx vite build
# Prune devDependencies to reduce image size
RUN pnpm install --shamefully-hoist --prod
# ── Stage 2: Production Runtime ──────────────────────────────────────
FROM node:22-alpine AS runtime
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_ENV=production
RUN corepack enable
# Security: run as non-root
RUN addgroup -g 1001 rebase && adduser -u 1001 -G rebase -s /bin/sh -D rebase
WORKDIR /app
# Copy only production artifacts (node_modules already pruned of devDependencies)
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages ./packages
COPY --from=builder /app/app ./app
# Uploads directory with correct ownership
RUN mkdir -p /app/app/uploads && chown -R rebase:rebase /app
ENV STORAGE_PATH=/app/app/uploads
USER rebase
WORKDIR /app/app/backend
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# Copy the entrypoint script and drizzle config for auto-migration
COPY --from=builder /app/app/backend/entrypoint.sh ./entrypoint.sh
COPY --from=builder /app/app/backend/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder /app/app/backend/drizzle ./drizzle
# Auto-migrate then start the compiled JavaScript backend
CMD ["sh", "entrypoint.sh"]
Build command (run from monorepo root):
docker build -t rebase-backend -f app/backend/Dockerfile .
Docker Entrypoint (Auto-Migration)
The Docker image uses an entrypoint script (entrypoint.sh) that automatically runs database migrations before starting the server. Migrations are non-fatal — if they fail (e.g. already applied), the server still starts.
#!/bin/sh
set -e
echo "🔄 Running database migrations..."
npx drizzle-kit migrate --config=drizzle.config.ts 2>&1 || echo "⚠️ Migrations skipped or failed (non-fatal)"
echo "🚀 Starting Rebase backend..."
exec node dist/app/backend/src/index.js
IMPORTANT FOR AGENTS: The entrypoint runs drizzle-kit migrate automatically on container start. Never add manual migration steps to a Dockerfile CMD — they are handled by entrypoint.sh.
Docker Compose (Monorepo)
This is the monorepo-internal Docker Compose (app/backend/docker-compose.yml). Note: the build context is ../.. (monorepo root).
services:
db:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_USER: rebase
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rebasepassword}
POSTGRES_DB: rebase
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rebase -d rebase"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
command:
- "postgres"
- "-c"
- "shared_buffers=256MB"
- "-c"
- "max_connections=100"
- "-c"
- "log_min_duration_statement=1000"
backend:
build:
context: ../..
dockerfile: app/backend/Dockerfile
restart: unless-stopped
ports:
- "${PORT:-3001}:3001"
environment:
DATABASE_URL: postgresql://rebase:${POSTGRES_PASSWORD:-rebasepassword}@db:5432/rebase
ADMIN_CONNECTION_STRING: postgresql://rebase:${POSTGRES_PASSWORD:-rebasepassword}@db:5432/rebase
JWT_SECRET: ${JWT_SECRET:-super-secret-jwt-key-change-in-production}
NODE_ENV: ${NODE_ENV:-development}
PORT: "3001"
ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-true}
depends_on:
db:
condition: service_healthy
volumes:
- uploads:/app/app/backend/uploads
volumes:
postgres_data:
driver: local
uploads:
driver: local
WARNING FOR AGENTS: The build context is ../.. (monorepo root), NOT . or app/backend. The Dockerfile path is relative to the monorepo root: app/backend/Dockerfile. The depends_on uses condition: service_healthy so the backend waits for postgres to be ready. Never use version: '3.8' — modern Docker Compose doesn't use the version key.
Docker Compose (User Template)
For user-generated projects (created via rebase init), the Docker Compose file is at the project root. It includes separate backend + frontend services:
services:
db:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_USER: rebase
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
POSTGRES_DB: rebase
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rebase -d rebase"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
command:
- "postgres"
- "-c"
- "shared_buffers=256MB"
- "-c"
- "max_connections=100"
- "-c"
- "work_mem=4MB"
- "-c"
- "effective_cache_size=768MB"
- "-c"
- "log_min_duration_statement=1000"
backend:
build:
context: .
dockerfile: backend/Dockerfile
restart: unless-stopped
ports:
- "${PORT:-3001}:3001"
env_file: .env
environment:
DATABASE_URL: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
ADMIN_CONNECTION_STRING: postgresql://rebase:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
NODE_ENV: production
PORT: "3001"
depends_on:
db:
condition: service_healthy
volumes:
- uploads:/app/backend/uploads
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
volumes:
postgres_data:
driver: local
uploads:
driver: local
Key differences from the monorepo compose:
- Build context is
. (project root), dockerfile is backend/Dockerfile
- Uses
env_file: .env to load all env vars
- Sets
NODE_ENV: production explicitly
- Includes a separate
frontend service (nginx)
- Adds Postgres tuning for
work_mem and effective_cache_size
Environment Variables
loadEnv() — Validated Environment Loading
All environment variables are validated at startup via a Zod schema in loadEnv() from @rebasepro/server. The server fails immediately if required variables are missing or invalid.
import dotenv from "dotenv";
import { loadEnv } from "@rebasepro/server";
dotenv.config({ path: "../../.env" });
export const env = loadEnv();
IMPORTANT FOR AGENTS: loadEnv() does NOT load .env files itself. You MUST call dotenv.config() (or use --env-file, container injection, etc.) before calling loadEnv(). This is a deployment concern, not a framework concern.
Extending with Custom Variables
Use the extend option to add your own typed env variables on top of the base Rebase schema:
import dotenv from "dotenv";
import { loadEnv } from "@rebasepro/server";
import { z } from "zod";
dotenv.config({ path: "../../.env" });
export const env = loadEnv({
extend: z.object({
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.string().default("587").transform(Number),
STRIPE_SECRET_KEY: z.string(),
})
});
The extend schema is merged with the base rebaseEnvSchema using Zod's .merge(). All base variables remain available alongside your custom ones.
loadEnv() Function Signature
function loadEnv(): RebaseEnv;
function loadEnv<E extends z.AnyZodObject>(options: { extend: E }): RebaseEnv & z.infer<E>;
Complete Environment Variable Reference
All variables recognized by the base rebaseEnvSchema:
| Variable | Type | Required | Default | Description |
|---|
DATABASE_URL | string (URL) | ✅ Yes | — | PostgreSQL connection string |
ADMIN_CONNECTION_STRING | string (URL) | No | — | Separate admin connection for migrations |
JWT_SECRET | string (≥32 chars) | ✅ Yes | Auto-generated (dev only) | JWT signing secret |
JWT_ACCESS_EXPIRES_IN | string | No | "1h" | JWT access token expiry (e.g. "1h", "30m") |
JWT_REFRESH_EXPIRES_IN | string | No | "30d" | JWT refresh token expiry |
NODE_ENV | enum | No | "development" | "development", "production", or "test" |
PORT | string → number | No | "3001" → 3001 | Server port (string, transformed to number) |
CORS_ORIGINS | string | ⚠️ Prod | — | Comma-separated allowed origins |
FRONTEND_URL | string | ⚠️ Prod | — | Frontend URL (CORS fallback, email links) |
ALLOW_REGISTRATION | boolString | No | "false" → false | Enable new user self-registration |
ALLOW_LOCALHOST_IN_PRODUCTION | optionalBoolString | No | undefined → false | Allow localhost URLs in production |
GOOGLE_CLIENT_ID | string | No | — | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | string | No | — | Google OAuth client secret |
REBASE_SERVICE_KEY | string | No | Auto-generated (dev only) | Service-to-service auth key (≥32 chars) |
DB_POOL_MAX | string → number | No | "20" → 20 | Max database pool connections |
DB_POOL_IDLE_TIMEOUT | string → number | No | "30000" → 30000 | Idle connection timeout (ms) |
DB_POOL_CONNECT_TIMEOUT | string → number | No | "10000" → 10000 | Connection timeout (ms) |
DATABASE_DIRECT_URL | string (URL) | No | — | Direct database URL (bypasses pooler) |
DATABASE_READ_URL | string (URL) | No | — | Read replica database URL |
STORAGE_TYPE | enum | No | "local" | "local", "s3", or "gcs" |
STORAGE_PATH | string | No | — | Local file storage directory |
FORCE_LOCAL_STORAGE | optionalBoolString | No | undefined → false | Allow STORAGE_TYPE=local in production — the backend refuses to boot without it |
S3_BUCKET | string | If S3 | — | S3 bucket name |
S3_REGION | string | No | — | S3 region |
S3_ACCESS_KEY_ID | string | If S3 | — | S3 access key |
S3_SECRET_ACCESS_KEY | string | If S3 | — | S3 secret key |
GCS_BUCKET | string | If GCS | — | GCS bucket name |
GCS_PROJECT_ID | string | No | — | GCP project (auto-detected from credentials) |
GCS_KEY_FILENAME | string | No | — | Service-account key file; omit on GKE (Workload Identity/ADC) |
Deploying with STORAGE_TYPE=local fails to boot in production. Local storage is the container filesystem, so uploads are destroyed on the next restart — silently, with no error at write or read time. Set STORAGE_TYPE=s3/gcs, or FORCE_LOCAL_STORAGE=true if a durable volume really is mounted at STORAGE_PATH. A crashed rollout is recoverable; lost user files are not.
| S3_ENDPOINT | string (URL) | No | — | Custom S3 endpoint (MinIO, R2) |
| S3_FORCE_PATH_STYLE | optionalBoolString | No | undefined → false | Use path-style S3 URLs (required for MinIO) |
| GCS_BUCKET | string | If GCS | — | Google Cloud Storage bucket name |
| GCS_PROJECT_ID | string | If GCS | — | GCP project ID for GCS |
| GOOGLE_APPLICATION_CREDENTIALS | string | If GCS (non-GCP) | — | Path to GCP service account JSON key file (not needed on GCP with default credentials) |
Zod Type Helpers
| Helper | Accepts | Result |
|---|
boolString | "true", "false", "" | Defaults to "false", transforms to boolean |
optionalBoolString | "true", "false", "", undefined | Transforms "true" → true, everything else → false |
Auto-Generated Secrets (Dev Only)
In non-production mode, loadEnv() auto-generates ephemeral secrets for:
JWT_SECRET — if not set
REBASE_SERVICE_KEY — if not set
These are random hex strings generated via crypto.randomBytes(48). A warning is logged:
⚠️ Auto-generated secrets for: JWT_SECRET, REBASE_SERVICE_KEY. These are ephemeral — existing tokens will be invalidated on restart. Set them explicitly in .env for persistent sessions.
WARNING FOR AGENTS: Auto-generated secrets are blocked in production. The server will crash with a Zod validation error if JWT_SECRET or REBASE_SERVICE_KEY are not explicitly set when NODE_ENV=production.
Production Validations
When NODE_ENV=production, loadEnv() enforces these additional rules via Zod .superRefine():
| Rule | Error if violated |
|---|
| CORS required | At least one of CORS_ORIGINS or FRONTEND_URL must be set |
| No auto-generated secrets | JWT_SECRET and REBASE_SERVICE_KEY must be explicitly set |
| No localhost URLs | All URL-type env vars are scanned — any containing localhost, 127.0.0.1, or ::1 will fail validation (unless ALLOW_LOCALHOST_IN_PRODUCTION=true) |
The localhost check applies to all string env vars (except CORS_ORIGINS) using a helper that parses URLs, custom protocols (e.g. postgres://), and plain host:port formats.
.env.example Reference
Copy the .env.example file as a starting point:
cp .env.example .env
The .env.example includes all variables with inline documentation. See app/.env.example in the repository.
.env.production (Frontend Build)
The .env.production file is read by Vite at build time to bake frontend config into the client bundle:
VITE_API_URL=
VITE_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
IMPORTANT FOR AGENTS: VITE_API_URL= (empty) means same-origin — the frontend assumes the API is served from the same host. This works when using serveSPA() or a reverse proxy. Only set a full URL when the API is on a different domain.
SPA Serving with serveSPA()
In production, the backend can serve the frontend SPA directly, eliminating the need for a separate web server or CDN.
serveSPA() Function Signature
function serveSPA<E extends Env>(app: Hono<E>, config: ServeSPAConfig): void;
ServeSPAConfig Options
| Property | Type | Default | Description |
|---|
frontendPath | string | — | Required. Absolute path to the frontend build directory |
apiBasePath | string | "/api" | Base path for API routes (excluded from SPA handling) |
excludePaths | string[] | [] | Additional paths to exclude from SPA handling (e.g. ["/health", "/ws", "/metrics"]) |
indexFile | string | "index.html" | Index file to serve for SPA routes |
How It Works
- Serves static files from
frontendPath using @hono/node-server/serve-static
- For any GET request not matching
apiBasePath or excludePaths, returns index.html (SPA fallback)
- If
frontendPath doesn't exist, logs a warning and disables SPA serving (does not crash)
- If
index.html is missing, passes through to the next handler
Usage Example
import { serveSPA } from "@rebasepro/server";
import path from "path";
const isProduction = env.NODE_ENV === "production";
if (isProduction) {
serveSPA(app, {
frontendPath: path.resolve(process.cwd(), "../frontend/dist"),
});
}
IMPORTANT FOR AGENTS: Call serveSPA() after initializeRebaseBackend() and after mounting the /health endpoint. The SPA catch-all route (*) must be the last route registered to avoid intercepting API or health check requests.
AWS (ECS/Fargate + RDS)
Deploy the Rebase Docker image to AWS using ECS (Fargate) with a managed PostgreSQL database via RDS.
Architecture
Internet → ALB (HTTPS) → ECS Fargate (Rebase container) → RDS PostgreSQL
→ S3 (file storage)
Key Steps
- PostgreSQL — Create an RDS PostgreSQL 16+ instance (or Aurora Serverless v2). Enable automated backups. Place in a private subnet.
- Docker Image — Push the Rebase Docker image to ECR:
docker build -t rebase-backend -f app/backend/Dockerfile .
docker tag rebase-backend:latest <account-id>.dkr.ecr.<region>.amazonaws.com/rebase-backend:latest
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/rebase-backend:latest
- ECS Task Definition — Define a Fargate task with the container image, port
3001, and environment variables (see env var reference above). Use AWS Secrets Manager for JWT_SECRET, REBASE_SERVICE_KEY, and DATABASE_URL.
- ALB — Create an Application Load Balancer with HTTPS listener (ACM certificate). Target group pointing to the ECS service on port
3001. Configure the health check path to /health.
- File Storage — Set
STORAGE_TYPE=s3 with an S3 bucket. Grant the ECS task role s3:GetObject, s3:PutObject, s3:DeleteObject permissions.
Key Environment Variables
DATABASE_URL=postgresql://rebase:<password>@<rds-endpoint>:5432/rebase
JWT_SECRET=<your-secret-min-32-chars>
REBASE_SERVICE_KEY=<your-service-key-min-32-chars>
CORS_ORIGINS=https://yourdomain.com
FRONTEND_URL=https://yourdomain.com
NODE_ENV=production
STORAGE_TYPE=s3
S3_BUCKET=your-rebase-uploads
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=<iam-key-or-use-task-role>
S3_SECRET_ACCESS_KEY=<iam-secret>
TIP: Use IAM task roles instead of static access keys for S3. Attach the policy to the ECS task execution role and omit S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY — the AWS SDK will use the role credentials automatically.
GCP (Cloud Run + Cloud SQL)
Deploy the Rebase Docker image to Cloud Run with Cloud SQL for PostgreSQL.
Architecture
Internet → Cloud Run (HTTPS, auto-scaling) → Cloud SQL PostgreSQL
→ GCS or S3-compatible (file storage)
Key Steps
- PostgreSQL — Create a Cloud SQL for PostgreSQL 16+ instance. Enable private IP or use the Cloud SQL Auth Proxy.
- Docker Image — Push to Artifact Registry:
docker build -t rebase-backend -f app/backend/Dockerfile .
docker tag rebase-backend:latest <region>-docker.pkg.dev/<project-id>/rebase/backend:latest
gcloud auth configure-docker <region>-docker.pkg.dev
docker push <region>-docker.pkg.dev/<project-id>/rebase/backend:latest
- Cloud Run Service — Deploy with the Cloud SQL connection:
gcloud run deploy rebase-backend \
--image <region>-docker.pkg.dev/<project-id>/rebase/backend:latest \
--region <region> \
--port 3001 \
--add-cloudsql-instances <project-id>:<region>:<instance-name> \
--set-env-vars "NODE_ENV=production,CORS_ORIGINS=https://yourdomain.com,FRONTEND_URL=https://yourdomain.com" \
--set-secrets "DATABASE_URL=rebase-db-url:latest,JWT_SECRET=rebase-jwt-secret:latest,REBASE_SERVICE_KEY=rebase-service-key:latest" \
--min-instances 1 \
--memory 512Mi \
--allow-unauthenticated
- Custom Domain — Map a custom domain in Cloud Run settings. Cloud Run provides HTTPS automatically.
- File Storage — Use native GCS support with
STORAGE_TYPE=gcs:
STORAGE_TYPE=gcs
GCS_BUCKET=your-rebase-uploads
GCS_PROJECT_ID=your-gcp-project-id
On Cloud Run, the default service account credentials are used automatically — no key file needed. Alternatively, you can fall back to STORAGE_TYPE=s3 with the GCS S3-compatible interop endpoint.
Cloud SQL Auth Proxy (Connection String)
When using the Cloud SQL connector in Cloud Run, the database is exposed via a Unix socket:
DATABASE_URL=postgresql://rebase:<password>@localhost/rebase?host=/cloudsql/<project-id>:<region>:<instance-name>
WARNING: Cloud Run has a request timeout (default 300s, max 3600s) and can scale to zero. WebSocket connections will be terminated when instances scale down. If you rely on Rebase realtime features, set --min-instances 1 and increase the request timeout. For heavy realtime usage, consider GCE (Compute Engine) with Docker Compose instead.
Azure (Container Apps + PostgreSQL Flexible Server)
Deploy the Rebase Docker image to Azure Container Apps with Azure Database for PostgreSQL.
Architecture
Internet → Azure Container Apps (HTTPS, auto-scaling) → Azure Database for PostgreSQL
→ Azure Blob Storage (file storage)
Key Steps
- PostgreSQL — Create an Azure Database for PostgreSQL Flexible Server. Choose a Burstable or General Purpose tier. Place in an EU region (West Europe, North Europe, or France Central).
- Docker Image — Push to Azure Container Registry (ACR):
az acr login --name YourRegistryName
docker build -t yourregistryname.azurecr.io/rebase-backend:latest -f app/backend/Dockerfile .
docker push yourregistryname.azurecr.io/rebase-backend:latest
- Container App — Create a Container Apps Environment and deploy:
- Point to your ACR image
- Set the target port to
3001
- Enable Ingress for external traffic
- Configure environment variables (see below)
- Networking — If the PostgreSQL server uses private networking, configure VNet integration in the Container Apps Environment.
- File Storage — Use
STORAGE_TYPE=s3 with Azure Blob Storage via the S3-compatible API, or use Cloudflare R2.
Key Environment Variables
DATABASE_URL=postgresql://rebase_admin:<password>@<server-name>.postgres.database.azure.com:5432/rebase
JWT_SECRET=<your-secret-min-32-chars>
REBASE_SERVICE_KEY=<your-service-key-min-32-chars>
CORS_ORIGINS=https://yourdomain.com
FRONTEND_URL=https://yourdomain.com
NODE_ENV=production
ALLOW_REGISTRATION=false
TIP: Azure Container Apps provides built-in HTTPS with automatic TLS certificates. Use Managed Identity instead of static credentials for accessing Azure services.
Scaleway (Serverless Containers + Managed PostgreSQL)
Deploy the Rebase Docker image to Scaleway Serverless Containers. Scaleway is a premier European cloud provider with datacenters in Paris, Amsterdam, and Warsaw — ideal for EU data sovereignty.
Architecture
Internet → Scaleway Serverless Container (HTTPS) → Managed PostgreSQL
→ Object Storage (S3-compatible)
Key Steps
- PostgreSQL — Create a Managed Database for PostgreSQL in your preferred EU region (e.g., Paris
fr-par).
- Docker Image — Push to Scaleway Container Registry:
docker build -t rg.fr-par.scw.cloud/rebase-apps/rebase-backend:latest -f app/backend/Dockerfile .
docker push rg.fr-par.scw.cloud/rebase-apps/rebase-backend:latest
- Serverless Container — Deploy from the Scaleway Console or CLI:
- Select your image from the Container Registry
- Set the port to
3001
- Configure environment variables (see below)
- File Storage — Use
STORAGE_TYPE=s3 with Scaleway Object Storage (natively S3-compatible):
S3_ENDPOINT=https://s3.fr-par.scw.cloud
S3_BUCKET=your-rebase-uploads
S3_REGION=fr-par
S3_FORCE_PATH_STYLE=true
Key Environment Variables
DATABASE_URL=postgresql://user:<password>@<instance-ip>:5432/rebase
JWT_SECRET=<your-secret-min-32-chars>
REBASE_SERVICE_KEY=<your-service-key-min-32-chars>
CORS_ORIGINS=https://yourdomain.com
FRONTEND_URL=https://yourdomain.com
NODE_ENV=production
ALLOW_REGISTRATION=false
STORAGE_TYPE=s3
S3_ENDPOINT=https://s3.fr-par.scw.cloud
S3_BUCKET=your-rebase-uploads
S3_REGION=fr-par
S3_FORCE_PATH_STYLE=true
TIP: Scaleway Object Storage is natively S3-compatible — no interop layer needed. Set S3_FORCE_PATH_STYLE=true for compatibility.
Hetzner (VPS + Docker Compose)
The most cost-effective production deployment. A single Hetzner VPS running Docker Compose.
Architecture
Internet → Caddy (HTTPS, auto-TLS) → Rebase container (port 3001)
→ PostgreSQL container
→ Hetzner Volume (persistent storage)
Setup
-
Provision a VPS — A Hetzner CPX21 (3 vCPU, 4 GB RAM, ~€8/mo) handles most workloads. Choose Ubuntu 24.04.
-
Install Docker — SSH in and install Docker + Docker Compose:
curl -fsSL https://get.docker.com | sh
-
Clone your project and copy your .env file to the server.
-
Use the existing docker-compose.yml — the template generated by rebase init works out of the box. Just update .env with production values:
DATABASE_PASSWORD=<strong-random-password>
JWT_SECRET=<your-secret-min-32-chars>
REBASE_SERVICE_KEY=<your-service-key-min-32-chars>
CORS_ORIGINS=https://yourdomain.com
FRONTEND_URL=https://yourdomain.com
NODE_ENV=production
ALLOW_REGISTRATION=false
-
Reverse Proxy (Caddy) — Add a Caddy service for automatic HTTPS:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- backend
caddy_data:
driver: local
caddy_config:
driver: local
Create a Caddyfile:
yourdomain.com {
reverse_proxy backend:3001
}
-
Start everything:
docker compose up -d
-
Persistent Storage — Attach a Hetzner Volume for the postgres_data and uploads Docker volumes if you need data to survive server rebuilds.
Backups
Schedule automated PostgreSQL backups via cron on the host:
0 3 * * * root docker compose -f /path/to/docker-compose.yml exec -T db pg_dump -U rebase rebase | gzip > /backups/rebase-$(date +\%Y\%m\%d).sql.gz
TIP: Hetzner also offers managed entities (server-level) and the option to attach a Hetzner Volume for data that survives server rebuilds. For production, strongly consider both.
PaaS (Railway, Render, Fly.io)
Docker-based PaaS platforms that deploy directly from your repo or Docker image.
General Pattern
All PaaS platforms follow the same workflow:
- Connect your Git repository or point to a Dockerfile
- Provision a managed PostgreSQL add-on
- Set environment variables (see env var reference above)
- Deploy — the platform builds and runs the Docker image
Platform-Specific Notes
| Platform | PostgreSQL | Notes |
|---|
| Railway | Built-in add-on | Connects via DATABASE_URL injected automatically. Supports persistent volumes for file uploads. |
| Render | Managed PostgreSQL | Use a "Web Service" with Docker runtime. Free tier has cold starts — use paid for production. |
| Fly.io | Fly Postgres (community) | Closest to self-hosted — Fly Postgres is a managed wrapper around standard Postgres. Supports persistent volumes via fly volumes. |
Minimum Environment Variables
DATABASE_URL=<provided-by-platform>
JWT_SECRET=<your-secret-min-32-chars>
REBASE_SERVICE_KEY=<your-service-key-min-32-chars>
CORS_ORIGINS=https://your-app.up.railway.app
FRONTEND_URL=https://your-app.up.railway.app
NODE_ENV=production
ALLOW_REGISTRATION=false
WARNING: Most PaaS free tiers have ephemeral filesystems — uploaded files will be lost on redeploy. Use STORAGE_TYPE=s3 with an external object storage provider (Cloudflare R2, AWS S3, MinIO) for production file storage.
Health Check Endpoint
The Docker HEALTHCHECK instruction hits /health. Mount it in your backend entry:
app.get("/health", async (c) => {
const result = await backend.healthCheck();
const status = result.healthy ? 200 : 503;
return c.json({
status: result.healthy ? "ok" : "degraded",
latencyMs: result.latencyMs,
...(result.details ? { details: result.details } : {})
}, status);
});
healthCheck() runs SELECT 1 against the database and returns latency. Returns { healthy: boolean, latencyMs: number, details?: { error: string } }.
Graceful Shutdown
The RebaseBackendInstance exposes a shutdown() method for clean container termination:
const backend = await initializeRebaseBackend({ ... });
const gracefulShutdown = async (signal: string) => {
await backend.shutdown();
await postgresResources.pool.end();
process.exit(0);
};
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
shutdown(timeoutMs?: number):
- Default timeout:
15000 ms
- Stops cron scheduler
- Destroys realtime services (LISTEN clients, debounce timers)
- Closes HTTP server and drains in-flight requests
- Pass
0 to skip the force-exit timer (useful in tests)
WARNING FOR AGENTS: Do NOT call server.close() separately when using backend.shutdown() — it already closes the HTTP server internally. Calling server.close() twice will deadlock.
⛔ Agent Deployment Rules
Agents should NEVER deploy or run deployment commands unless explicitly asked by the user in the current conversation. This includes:
rebase deploy (any variant)
gcloud run deploy
terraform apply (any variant that deploys resources)
docker compose up in production
- Any command targeting staging or production environments
What agents CAN do:
- Edit source code
- Run builds (
pnpm run build)
- Run tests (
pnpm test)
- Run local dev server (
pnpm dev)
- Check logs (read-only)
- Create or edit Dockerfiles and docker-compose files
- Create or edit
.env files
- Run deployment commands only if the user explicitly asks you to deploy in the current conversation. Otherwise, provide the exact commands for the user to run.
References
- Documentation: rebase.pro/docs
- GitHub: github.com/rebasepro/rebase
- Dockerfile:
app/backend/Dockerfile
- Docker Compose (monorepo):
app/backend/docker-compose.yml
- Docker Compose (template):
packages/cli/templates/template/docker-compose.yml
- Entrypoint:
app/backend/entrypoint.sh
- Env Schema:
packages/server/src/env.ts
- serveSPA:
packages/server/src/serve-spa.ts
- Backend Entry:
app/backend/src/index.ts
- .env.example:
app/.env.example