Deploy to Railway¶
Every nuvel-scaffolded ADK agent ships with Dockerfile + railway.json ready for Railway. Path of least resistance.
What's in the box¶
After nuvel new my-agent --framework adk:
Dockerfile—python:3.12-slim, installsgccfor C extensions, copiesrequirements.txtfirst for layer caching, runspython run_adk.py.railway.json— points at the Dockerfile, sets/healthas the healthcheck, on-failure restart with up to 3 retries.
Deploy¶
- Push your agent to GitHub (a repo for the agent itself, separate from nuvel).
- Create a Railway project → New Project → Deploy from GitHub repo. Pick your agent's repo.
- Set environment variables in the Railway dashboard (Variables tab). At minimum:
OPENROUTER_API_KEY=sk-or-...
API_KEY=<long random string> # Bearer token for /run, /run_sse, etc.
SESSION_SERVICE_URI=postgresql://... # Required in production (DEV_MODE=false)
For each enabled feature, add the relevant vars (Composio, channels, etc.).
-
Add a Postgres plugin via Railway's marketplace, then copy the
DATABASE_URLit provisions intoSESSION_SERVICE_URI. -
Deploy. Railway picks up
railway.jsonand builds from the Dockerfile.
Healthcheck¶
The generated run_adk.py exposes GET /health returning {"status":"healthy"}. Railway uses this for the /health probe configured in railway.json. The endpoint is exempt from APIKeyMiddleware, so Railway can hit it without credentials.
Custom domain¶
Once deployed, Railway gives you a *.up.railway.app hostname. To use a custom domain:
- Settings → Networking → Custom domain → add yours.
- Update DNS (CNAME record to the Railway-provided target).
- Wait for the cert to provision (Railway handles Let's Encrypt automatically).
Two-process deployments (Teams)¶
If you enabled --with-teams, you have a sidecar process that needs its own deployment. On Railway:
- Service A: agent server (the one auto-detected). Keeps running
python run_adk.py. - Service B: Teams bridge. New service in the same project. In its Settings → Deploy, override the start command to
python -m my_agent.gateways.teams_bridge. SetAGENT_BASE_URLto Service A's internal URL (http://${{agent.RAILWAY_PRIVATE_DOMAIN}}:8000). Both services share the same Postgres.
Cron / scheduled jobs on Railway¶
If you enabled the cron scheduler (NUVEL_CRON_ENABLED=1), three Railway specifics matter.
1. Persist jobs across deploys with a Volume¶
By default the scheduler stores jobs at ~/.nuvel/cron/jobs.json and outputs at ~/.nuvel/cron/output/. Railway's container filesystem is wiped on every deploy — without persistence, every job you create is lost the next time you push code.
Fix: attach a Railway Volume to the agent service.
- Service → Settings → Volumes → New Volume. Mount path
/data, size 1 GB is plenty. - Set
NUVEL_CRON_DIR=/data/cronin the service's variables. - Redeploy. From now on, jobs and outputs live on the volume and survive deploys.
Cost is ~$0.25/GB/month — negligible.
2. Stay on a non-sleeping plan¶
The scheduler is an in-process asyncio task. If Railway sleeps the container (Hobby tier, idle services), the scheduler stops ticking and cron silently halts. Either stay on a paid plan that keeps the container warm, or set up an external uptime monitor (UptimeRobot etc.) hitting /health every minute to keep the container active.
3. Single replica only (for now)¶
Each replica runs its own scheduler — scaling beyond 1 instance will fire every cron job N times. The MVP doesn't have a distributed lock yet, so keep replicas=1 on the agent service if cron is enabled. Postgres-backed storage with advisory locks is a planned follow-up.
Railway-native cron as an alternative¶
If you only need a couple of fixed scheduled tasks and don't want to think about persistence, Railway offers a separate Cron Jobs service type — point it at your agent's POST /cron/jobs/{id}/run endpoint with the right auth header and let Railway own the clock. Trade-off: you lose /cron add from chat.
Other platforms¶
The scaffold is platform-agnostic — Dockerfile + a port via $PORT env var works on Fly.io, Render, Cloud Run, ECS, anything that takes a container.
railway.json is harmless on other platforms (it's ignored). Remove it if it bothers you.
Logs¶
The agent emits structured logs (configurable via LOG_FORMAT=json|text). For production aggregation, set LOG_FORMAT=json and route Railway logs to your aggregator of choice.