Self-hosting ryOS on a VPS or Coolify (without Vercel)
ryOS can run without Vercel as a single Bun production server. That server handles:
- API routes under
api/
- built frontend assets from
dist/ - SPA deep links like
/chatsand/ipod/:id - docs clean URLs like
/docs/overview - optional local websocket realtime
This makes it a good fit for:
- a plain VPS + systemd
- Coolify Dockerfile deployments
- self-hosted containers behind Traefik / Nginx / Caddy
1) Prerequisites
- Linux VPS or container host
- Bun installed (or Docker/Coolify)
- a public domain + TLS if exposing to the internet
- Redis if using self-hosted Redis / local websocket fanout
2) Build + run locally in production mode
bun install
bun run build
APP_PUBLIC_ORIGIN="https://your-domain.com" \
API_ALLOWED_ORIGINS="https://your-domain.com" \
bun run start
Important runtime envs:
PORTorAPI_PORT— listening port (defaults to3000)
API_HOST— bind host (defaults to0.0.0.0)APP_PUBLIC_ORIGIN— canonical browser origin, e.g.https://your-domain.comAPI_ALLOWED_ORIGINS— comma-separated allowed browser origins for/api/; supports wildcard subdomain patterns (e.g..example.com), andto allow all originsSTORAGE_PROVIDER— optional explicit storage backend (s3,vercel-blob); auto-detected if unset
3) Redis backend options
ryOS supports two Redis modes.
Option A — Standard Redis / Valkey / self-hosted Redis
Recommended for Coolify / VPS deployments:
REDIS_URL="redis://default:password@redis:6379/0"
This mode also enables Redis pub/sub for local websocket fanout.
Option B — Upstash REST
Keeps compatibility with the existing Vercel-oriented setup:
REDIS_KV_REST_API_URL="https://..."
REDIS_KV_REST_API_TOKEN="..."
4) Realtime backend options
ryOS supports two realtime modes.
Option A — Pusher
REALTIME_PROVIDER="pusher"
PUSHER_APP_ID="..."
PUSHER_KEY="..."
PUSHER_SECRET="..."
PUSHER_CLUSTER="us3"
Option B — Local websocket
Recommended for self-hosted deployments:
REALTIME_PROVIDER="local"
REALTIME_WS_PATH="/ws"
Notes:
- local websocket mode works best with
REDIS_URL
- with
REDIS_URL, websocket events can fan out across instances via Redis pub/sub - without
REDIS_URL, local websocket delivery falls back to in-process fanout only
5) Object storage backend
ryOS supports two object storage backends for cloud sync (backups, wallpapers, images).
Option A — S3-compatible (recommended for self-hosted)
Works with MinIO, Cloudflare R2, Backblaze B2, AWS S3, or any S3-compatible provider:
STORAGE_PROVIDER=s3
S3_BUCKET=ryos-sync
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.your-provider.com
S3_PUBLIC_ENDPOINT=https://s3.your-provider.com # public-facing URL (for presigned URLs)
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_FORCE_PATH_STYLE=true # required for MinIO and some providers
Option B — Vercel Blob
Keeps compatibility with the existing Vercel-oriented setup:
STORAGE_PROVIDER=vercel-blob # optional, auto-detected from token
BLOB_READ_WRITE_TOKEN=vercel_blob_...
When STORAGE_PROVIDER is not set explicitly, the backend is auto-detected: Vercel Blob if BLOB_READ_WRITE_TOKEN is present, S3 if the S3 environment variables are set.
6) Coolify deployment
The repository includes a Dockerfile suitable for Coolify Dockerfile deployments. The image includes a health check endpoint at /health.
Suggested Coolify settings
- Build Pack: Dockerfile
- Port Exposes:
3000(or whatever you set viaPORT) - Command override: not required
- Environment variables: set them in the Coolify UI
Recommended environment set for a fully self-hosted stack:
NODE_ENV=production
PORT=3000
API_HOST=0.0.0.0
APP_PUBLIC_ORIGIN=https://your-domain.com
API_ALLOWED_ORIGINS=https://your-domain.com
REDIS_URL=redis://default:password@redis:6379/0
REALTIME_PROVIDER=local
REALTIME_WS_PATH=/ws
STORAGE_PROVIDER=s3
S3_BUCKET=ryos-sync
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.your-provider.com
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
Coolify's reverse proxy supports WebSockets, so /ws can stay behind the normal app domain.
Version / build number (avoid "dev")
The app shows a version like 10.3 (abc1234) from version.json. Without a commit SHA at build time, it falls back to dev. To show the real commit:
- In Coolify → your app → Advanced → enable Include Source Commit in Build
- The build script reads
SOURCE_COMMIT(Coolify) orVERCEL_GIT_COMMIT_SHA(Vercel) orGIT_COMMIT_SHA(generic CI)
No extra env vars needed — Coolify injects SOURCE_COMMIT when that option is enabled.
Coolify deployments are auto-detected (via COOLIFY_ environment variables) and displayed in the Admin app's Server page.
7) VPS + systemd example
Example unit file:
[Unit]
Description=ryOS self-hosted server
After=network.target
[Service]
Type=simple
WorkingDirectory=/srv/ryos
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=API_HOST=127.0.0.1
Environment=APP_PUBLIC_ORIGIN=https://your-domain.com
Environment=API_ALLOWED_ORIGINS=https://your-domain.com
Environment=REDIS_URL=redis://127.0.0.1:6379/0
Environment=REALTIME_PROVIDER=local
Environment=REALTIME_WS_PATH=/ws
EnvironmentFile=/srv/ryos/.env.local
ExecStart=/usr/local/bin/bun run start
Restart=always
RestartSec=3
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target
8) Reverse proxy example (optional)
If you still want Nginx or Caddy in front, just proxy the single Bun server:
server {
listen 443 ssl http2;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
}
}
No separate static file server is required.
9) Notes and caveats
/api/sync/*endpoints use the switchable storage adapter (api/_utils/storage.ts), which supports both Vercel Blob and S3-compatible storage. See section 5 for configuration.
- For local development without Vercel:
bun run dev- or
bun run dev:api+bun run dev:vite
- API tests can target standalone mode:
API_URL=http://localhost:3000 bun run test:new-api
10) Troubleshooting Docker bridge networking
If you deploy a multi-container self-hosted stack with REDIS_URL and see:
- container-to-container Redis connections timing out
- local websocket fanout failing across instances
- Docker DNS resolving service names correctly, but TCP never connects
then the issue is usually the host's Docker bridge / iptables setup, not ryOS itself.
This showed up in nested Docker testing when the host had mixed iptables-nft and iptables-legacy state, and bridge traffic was blocked even though containers and DNS were healthy.
Quick checks
From an app container, verify Redis TCP directly:
docker exec <app-container> node -e "const net=require('net'); const s=net.createConnection(6379,'redis'); s.on('connect',()=>{console.log('connected'); s.destroy(); process.exit(0)}); s.on('error',e=>{console.error(e.message); process.exit(1)}); setTimeout(()=>{console.error('timeout'); process.exit(2)},3000);"
If that times out, inspect both iptables frontends:
update-alternatives --display iptables
sudo iptables -S FORWARD
sudo iptables-legacy -S FORWARD
sudo iptables-nft -S FORWARD
Known fix for mixed iptables backend hosts
If Docker bridge traffic is being blocked by legacy forwarding rules, this restores bridge connectivity immediately:
sudo iptables-legacy -P FORWARD ACCEPT
After applying it, re-test Redis TCP between containers before debugging ryOS.
Important note
This is a host-level Docker networking issue. If you see the timeout only on a specific VM, VPS image, or nested-container environment, fix the Docker / iptables configuration there rather than changing ryOS application code.