Docs

Deployment

Orbiter uses better-sqlite3 — a native Node.js module. It requires a real Node.js runtime with persistent filesystem access.

Supported environments

EnvironmentStatusNotes
Node.js VPS✅ RecommendedHetzner, DigitalOcean, any VPS
DockerMount pod as a volume
Railway / Render / Fly.ioUse a persistent volume for the pod
Netlify / Vercel⚠️ Read-onlyServerless FS is ephemeral — run admin separately, use Git sync
Cloudflare WorkersNo native Node.js support

Node.js (recommended)

npm install @astrojs/node@^10
npx astro build
node dist/server/entry.mjs

The admin runs separately on port 4322:

ORBITER_POD=/path/to/content.pod node node_modules/@a83/orbiter-admin/src/server.js

Docker

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install && npx astro build
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]
docker run -p 4321:4321 \
  -v $(pwd)/content.pod:/app/content.pod \
  my-orbiter-site

Mount the pod as a volume so it persists across container restarts and can be shared with the admin container.

Railway

Railway supports persistent volumes and is the easiest cloud option for Orbiter.

  1. Push your project to GitHub and connect the repo in Railway.
  2. In Railway → your service → Volumes, add a volume mounted at /data.
  3. Set environment variables:
    ORBITER_POD=/data/content.pod
    PORT=4321
  4. Set the start command:
    node dist/server/entry.mjs
  5. Add a second service for the admin, same repo, different start command:
    ORBITER_POD=/data/content.pod PORT=4322 node node_modules/@a83/orbiter-admin/src/server.js
    Mount the same volume at /data so both services share the pod.
Tip: Set ADMIN_ORIGIN=https://your-site.up.railway.app on the admin service so CORS allows requests from your frontend domain.

Fly.io

Fly.io uses persistent volumes via fly volumes.

  1. Install the Fly CLI and run fly launch in your project root. Accept the generated fly.toml.
  2. Create a volume:
    fly volumes create orbiter_data --size 1 --region fra
  3. Mount it in fly.toml:
    [mounts]
      source      = "orbiter_data"
      destination = "/data"
  4. Set the pod path:
    fly secrets set ORBITER_POD=/data/content.pod
  5. Deploy:
    fly deploy

Run the admin as a second Fly app pointing at the same volume, or deploy both processes in one app using a Procfile:

web:   node dist/server/entry.mjs
admin: node node_modules/@a83/orbiter-admin/src/server.js

Render

  1. Create a new Web Service from your GitHub repo.
  2. Build command: npm install && npx astro build
  3. Start command: node dist/server/entry.mjs
  4. Add a Disk (Render's persistent storage) mounted at /data.
  5. Set ORBITER_POD=/data/content.pod as an environment variable.
  6. For the admin, create a second Web Service with start command:
    node node_modules/@a83/orbiter-admin/src/server.js
    Same disk, same mount path.

Netlify / Vercel (with separate admin)

Run the admin on a VPS or Railway. Deploy the Astro site statically to Netlify/Vercel. Use the build webhook to trigger a rebuild when content changes.

┌──────────────────────────┐  build webhook  ┌──────────────────┐
│  Orbiter Admin (VPS)     │ ──────────────▶ │  Netlify/Vercel  │
│  port 4322               │                 │  static deploy   │
│  content.pod persists    │                 └──────────────────┘
└──────────────────────────┘

See Git sync mode if you want the pod committed to your repository.

Astro config for static hosting

// astro.config.mjs
export default defineConfig({
  output: 'static',
  integrations: [orbiter({ pod: './content.pod' })],
});
Static output means no /orbiter/media/[id] route at runtime — media must use an external backend (S3, GitHub) or a CDN. Use output: 'hybrid' or 'server' to keep the media route alive.