Deployment
Orbiter uses better-sqlite3 — a native Node.js module. It requires a real Node.js runtime with persistent filesystem access.
Supported environments
| Environment | Status | Notes |
|---|---|---|
| Node.js VPS | ✅ Recommended | Hetzner, DigitalOcean, any VPS |
| Docker | ✅ | Mount pod as a volume |
| Railway / Render / Fly.io | ✅ | Use a persistent volume for the pod |
| Netlify / Vercel | ⚠️ Read-only | Serverless FS is ephemeral — run admin separately, use Git sync |
| Cloudflare Workers | ❌ | No 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.
- Push your project to GitHub and connect the repo in Railway.
- In Railway → your service → Volumes, add a volume mounted at
/data. - Set environment variables:
ORBITER_POD=/data/content.pod PORT=4321
- Set the start command:
node dist/server/entry.mjs
- 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/dataso 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.
- Install the Fly CLI and run
fly launchin your project root. Accept the generatedfly.toml. - Create a volume:
fly volumes create orbiter_data --size 1 --region fra
- Mount it in
fly.toml:[mounts] source = "orbiter_data" destination = "/data"
- Set the pod path:
fly secrets set ORBITER_POD=/data/content.pod
- 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
- Create a new Web Service from your GitHub repo.
- Build command:
npm install && npx astro build - Start command:
node dist/server/entry.mjs - Add a Disk (Render's persistent storage) mounted at
/data. - Set
ORBITER_POD=/data/content.podas an environment variable. - 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.