Run Your Coding Agent as an API: OpenCode Server on Railway

TL;DR
Turn a terminal coding agent into a password-protected HTTP API: opencode serve in a container, deployed to Railway with a persistent volume, reachable from any machine, script, or webhook. The complete build, start to finish.
Every coding agent session starts the same way: a process next to the code and a terminal in front of you. That holds until the code lives on another machine, or you want work to continue after you close the laptop, or a webhook needs to trigger a task without cold-booting an agent every time. At that point the agent needs a network address.
OpenCode ships the primitive for exactly that: opencode serve, a headless HTTP server that exposes sessions, prompts, files, and an event stream. The shape of this build is one container, two files, and a password - the agent itself becomes the service, so anything that can send an HTTP request can use it. Railway is the host because this is squarely its lane: a Dockerfile deploy, a persistent volume, sealed variables for secrets, and an HTTPS domain with automatic SSL.
Seven steps, under an hour, every step ending in something you can run. Every command and response below was verified against OpenCode 1.18.4 while writing this, and that version is pinned in the Dockerfile.
Official Sources#
| Resource | Description |
|---|---|
| OpenCode Server docs | opencode serve, basic auth, and every HTTP endpoint |
| OpenCode CLI docs | serve, attach, run --attach, and the environment variable table |
| OpenCode SDK docs | The typed JS client and request body shapes |
| OpenCode install docs | curl install and npm install -g opencode-ai |
| AI SDK DeepSeek provider | The DEEPSEEK_API_KEY environment variable convention |
| Railway Dockerfiles | Dockerfile detection and custom paths |
| Railway Variables | Service variables and sealed secrets |
| Railway Volumes | Persistent storage, size limits, and caveats |
| Railway Public Networking | Domains, SSL, and target ports |
| Railway Free Trial | The one-time grant and trial resource caps |
| Railway Pricing Plans | Plan pricing, included usage, and per-resource rates |
Step 1: Install OpenCode and prove the CLI works#
Prerequisites: Node.js 20 or newer (for the container build later), a Git repository you can clone - a throwaway one is ideal for the first run - a provider API key, and a Railway account. New accounts get a one-time $5 trial grant valid for 30 days, per Railway's free trial docs, as of 2026-10-05, which covers this build several times over.
Install OpenCode with the official one-liner from the install docs:
curl -fsSL https://opencode.ai/install | bash
Authenticate a provider (opencode auth login), then confirm the capability everything else depends on - one task in, one answer out, no TUI:
opencode run "print the current directory tree, two levels deep"
If that prints a tree and exits cleanly, the worker side is proven.
One server detail worth knowing now: OpenCode reads provider keys from the environment as well as from the credentials file, per the Providers docs. That is what lets the same setup run in a container with no interactive login. For a DeepSeek key the environment variable is DEEPSEEK_API_KEY, the default the AI SDK DeepSeek provider reads, as of 2026-10-05.
What you have now: a working agent CLI and a provider credential you can hand to a server as an environment variable.
Step 2: Start the server locally and call it over HTTP#
opencode serve starts a headless server on port 4096 by default, with --port and --hostname to override, per the Server docs, as of 2026-10-05. Set the password environment variable first - without it the server has no authentication at all:
export OPENCODE_SERVER_PASSWORD="$(openssl rand -base64 24)"
opencode serve --port 4096 --hostname 127.0.0.1
In a second terminal, prove the door is locked and then open it. The username defaults to opencode; OPENCODE_SERVER_USERNAME overrides it, per the authentication section:
# Unauthenticated request: expect 401
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:4096/global/health
# Authenticated request: expect {"healthy":true,"version":"1.18.4"}
curl -s -u "opencode:$OPENCODE_SERVER_PASSWORD" http://127.0.0.1:4096/global/health
Now drive it. Create a session, then send a prompt and read the text parts back (sessions and messages endpoints):
SID=$(curl -s -u "opencode:$OPENCODE_SERVER_PASSWORD" \
-X POST http://127.0.0.1:4096/session \
-H 'content-type: application/json' -d '{}' | jq -r .id)
curl -s -u "opencode:$OPENCODE_SERVER_PASSWORD" \
-X POST "http://127.0.0.1:4096/session/$SID/message" \
-H 'content-type: application/json' \
-d '{"parts":[{"type":"text","text":"Run git log --oneline -5 and summarize what changed."}]}' \
| jq -r '.parts[] | select(.type=="text") | .text'
The body shape is the one the SDK docs use for session.prompt: an array of parts, where a text part is {"type":"text","text":"..."}. The synchronous endpoint holds the request open until the agent finishes, which makes it right for quick questions. For anything longer, use the async sibling and poll - prompt_async returns 204 No Content immediately and the work continues in the background:
curl -s -o /dev/null -w "%{http_code}\n" -u "opencode:$OPENCODE_SERVER_PASSWORD" \
-X POST "http://127.0.0.1:4096/session/$SID/prompt_async" \
-H 'content-type: application/json' \
-d '{"parts":[{"type":"text","text":"Reply with the single word: ok. Nothing else."}]}'
# Poll for the assistant message
curl -s -u "opencode:$OPENCODE_SERVER_PASSWORD" \
"http://127.0.0.1:4096/session/$SID/message" \
| jq -r '.[] | select(.info.role=="assistant") | .parts[] | select(.type=="text") | .text'
There is also an SSE stream at GET /event for live progress instead of polling, and the full OpenAPI 3.1 spec at /doc, per the Server docs.
What you have now: an authenticated agent API on your machine, with create, prompt, async, and event surfaces all proven.
Step 3: Package it as a container#
Two files. First the Dockerfile - a normal Node image with Git, a pinned OpenCode install via the documented npm install -g opencode-ai, and HOME pointed at /data so credentials and sessions land on the volume instead of the container's disposable filesystem:
FROM node:22-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g opencode-ai@1.18.4
ENV HOME=/data \
OPENCODE_DISABLE_AUTOUPDATE=true
COPY start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh && mkdir -p /data
EXPOSE 4096
CMD ["start.sh"]
OPENCODE_DISABLE_AUTOUPDATE is from the CLI environment variable table; an image that upgrades itself underneath you is not an image you can reproduce.
Second, start.sh: clone the repo on first boot, fast-forward it on every later boot, then serve. The server's working directory is the project directory, so the clone step is what decides which codebase the API can see:
#!/bin/bash
set -euo pipefail
REPO_URL="${REPO_URL:?set REPO_URL in the service variables}"
WORKSPACE="${WORKSPACE_DIR:-/data/workspace}"
if [ ! -d "$WORKSPACE/.git" ]; then
git clone "$REPO_URL" "$WORKSPACE"
fi
cd "$WORKSPACE"
git pull --ff-only || true
exec opencode serve --hostname 0.0.0.0 --port "${PORT:-4096}"
Build and run it locally before involving any cloud:
docker build -t opencode-server .
docker run --rm -p 4096:4096 \
-e OPENCODE_SERVER_PASSWORD=testpass \
-e DEEPSEEK_API_KEY=sk-your-key \
-e REPO_URL=https://github.com/you/your-repo.git \
opencode-server
Then repeat the Step 2 health check against http://127.0.0.1:4096/global/health. If the container answers, the image is good.
What you have now: a container that clones a repo, keeps its state on /data, and serves the API on one port.
Step 4: Deploy it on Railway#
Push the two files to a small infra repository (keeping the agent's workspace repo separate from this one is the cleaner split), then in the Railway dashboard: New Project → Deploy from GitHub repo → select the repo. Railway detects the Dockerfile at the root and builds it, per the Dockerfiles docs, and service builds are not billed, per the resource usage pricing table, as of 2026-10-05.
Before the first successful run, add the service configuration:
- Variables -
OPENCODE_SERVER_PASSWORD(generate a fresh one, do not reuse the local test value),DEEPSEEK_API_KEY, andREPO_URL. Use the Variables tab and seal the two secrets; sealed values are never visible in the UI or readable through the API, per the Variables docs. - Volume - attach one and mount it at
/data. Trial plans allow a 0.5GB volume and Hobby plans 5GB, per the Volumes size limits, as of 2026-10-05. That is plenty for one repo plus the session database. - Domain - in Settings → Networking → Public Networking, click Generate Domain. Railway provisions a
*.up.railway.appdomain with automatic SSL, per the Public Networking docs. With a single listening port, Railway's target-port detection picks 4096 automatically; you can change it in the domain settings, per the target ports section.
The first deploy clones the repo onto the volume. When it goes live, verify from your own machine - the same health check, now over HTTPS:
curl -s -u "opencode:$OPENCODE_SERVER_PASSWORD" \
https://your-app.up.railway.app/global/health
If it returns {"healthy":true,...}, the same API you tested locally is now on the public internet, behind basic auth. Note the operational shape Railway documents for services with volumes: one volume per service, no replicas with volumes, and a short restart gap on redeploys because only one deployment can mount the volume at a time (Volumes caveats, as of 2026-10-05).
What you have now: a deployed agent API with persistent state, a sealed password, and a TLS domain.
Step 5: Attach from anywhere#
The server has a client in the same CLI. opencode attach points the full terminal interface at a running server, so the TUI you know works against the Railway instance, including on a machine that has never seen the repository:
opencode attach https://your-app.up.railway.app \
--username opencode --password "$OPENCODE_SERVER_PASSWORD"
For one-shot scripting, opencode run --attach skips the TUI entirely - and it skips the cold boot that a fresh CLI process pays on every invocation, because the server is already running, per the CLI docs:
opencode run --attach https://your-app.up.railway.app \
--password "$OPENCODE_SERVER_PASSWORD" \
--model deepseek/deepseek-v4-flash \
"Summarize the open issues in docs/ as a bullet list."
Everything else speaks plain HTTP, which is the point. A GitHub webhook, a cron job, a Slack slash command, or a phone shortcut can now create a session and fire a prompt with the two curl calls from Step 2. The cron automation guide is the scheduled version of this shape, and the agent webhook build is the event-driven one; both get simpler when the server stays warm and you only pay for the prompt. If you would rather stay in typed code, the JS SDK wraps every endpoint, including the SSE stream.
Two practical notes. Long agent runs belong on prompt_async with polling or /event, not on a single held-open request - public edge networks have request timeouts for a reason, and Railway documents its limits at Networking specs and limits. And sessions live under HOME, which is the volume, so your conversation history survives redeploys along with the cloned repo.
What you have now: one URL that gives any machine or script a session-scoped coding agent.
Step 6: Lock it down#
This build is a remote execution surface by design - the container has shell and file tools, and whoever holds the password can point them at the cloned repo. Basic auth over TLS is a real gate, but it is the only one, so treat this like an SSH box, not like a docs site:
- Strong password, generated fresh for the server.
openssl rand -base64 32, stored only as a sealed Railway variable. Rotating it is one variable edit and a redeploy. - A dedicated throwaway repo. Not the monorepo with deploy credentials in its history, not the repo whose issues contain customer data. The agent can read everything in the workspace.
- No credentials in the container you are not prepared to burn. The provider key should have a spend cap. Do not ship a GitHub push token unless the agent is meant to push; if it is, use a fine-grained token limited to the one repo.
- Skip the public domain when you do not need it. If only other services in the same Railway project call the API, private networking reaches the service without exposing it at all, per the private networking docs.
- Watch the first week. Railway's logs show every deploy and restart; a session you did not create is the signal to rotate the password and tear the service down.
What you have now: an agent API with a blast radius you chose on purpose.
Step 7: What to do next#
The Railway side stays predictable: the Hobby plan is $5 per month and includes $5 of resource usage, per the included usage section, as of 2026-10-05, and the trial's 1GB of RAM and 0.5GB volume are enough to build this end to end, per the plan resource table, as of 2026-10-05. The model side stays pay-per-token; the DeepSeek V4 Flash guide covers why a budget model is the right default for bounded tasks.
The natural next moves, in order of value: put a GitHub webhook in front of it so labeled issues become agent runs, schedule one recurring chore against it, and keep the server warm for your own laptop sessions. The remote MCP server build is the complementary piece - that one gives agents tools, this one gives tools an agent - and deploying from the agent itself closes the loop.
When you are done experimenting, delete the service and its volume; trial volumes are removed 30 days after credits expire, but a Hobby volume survives cancellation for 60 days, per the Volumes docs, as of 2026-10-05.
What you have now: a coding agent that outlives your terminal - Dockerfile in, HTTPS API out, state on a volume, all in under an hour.
FAQ#
Is it safe to expose a coding agent on a public URL?#
It is defensible only with the Step 6 discipline applied: a strong, unique password sealed in Railway, a dedicated throwaway repo, a spend-capped provider key, and no unrelated credentials in the container. The server is authenticated with HTTP basic auth over TLS, per the Server docs, but the agent can run shell commands inside its workspace, so the account behind it should have nothing you would miss. If you cannot accept that, skip the public domain and reach the service over Railway private networking instead.
What does hosting this cost?#
On Railway: the trial gives new accounts a one-time $5 grant valid for 30 days, then the Hobby plan is $5 per month including $5 of usage, with RAM at $10 per GB per month and volume storage at $0.15 per GB per month, per the plans page, as of 2026-10-05. A small service like this sits inside the included usage most months. Model tokens are billed separately by your provider and only when a prompt runs.
Do the cloned repo, credentials, and sessions survive a redeploy?#
Yes, if you set HOME=/data as the Dockerfile does and attach the volume at /data. That one move puts the credentials file, the session database, and the cloned workspace on persistent storage, and the start script fast-forwards the clone on each boot. Keep in mind Railway's documented volume caveats: one volume per service, no replicas alongside a volume, and a brief restart gap when redeploying a service that has one.
Can I point Claude Code or another harness at this server?#
No. opencode serve speaks OpenCode's own HTTP API, so the clients are opencode attach, opencode run --attach, curl, or the JS SDK. Other harnesses have their own server and client surfaces. The upside is that the protocol is plain HTTP with an OpenAPI spec, so anything on your side can call it without an SDK at all.
Why host the agent instead of running it locally?#
Because the trigger and the code do not live on your laptop. Hosting lets a webhook, a cron schedule, or a phone fire a task at any hour, keeps sessions and the workspace in one place, and removes the cold boot from every scripted call. The tradeoff is one more hosted execution surface to guard, which is what Step 6 exists for.
Some links to tools above are referral links - see our affiliate disclosure.
Sources#
| Source | URL |
|---|---|
| OpenCode Server docs | https://opencode.ai/docs/server |
| OpenCode CLI docs | https://opencode.ai/docs/cli |
| OpenCode SDK docs | https://opencode.ai/docs/sdk |
| OpenCode install docs | https://opencode.ai/docs/ |
| OpenCode Providers docs | https://opencode.ai/docs/providers |
| AI SDK DeepSeek provider | https://ai-sdk.dev/providers/ai-sdk-providers/deepseek |
| Railway Dockerfiles | https://docs.railway.com/builds/dockerfiles |
| Railway Variables | https://docs.railway.com/develop/variables |
| Railway Volumes | https://docs.railway.com/reference/volumes |
| Railway Public Networking | https://docs.railway.com/reference/public-networking |
| Railway Free Trial | https://docs.railway.com/reference/pricing/free-trial |
| Railway Pricing Plans | https://docs.railway.com/reference/pricing/plans |
Last updated: October 5, 2026
Continue Reading#
- Put an AI Agent on a Cron Job - the scheduled sibling: a warm server makes every chore cheaper
- Put an Agent on a Webhook - the event-driven sibling: issues become agent runs
- Ship a Remote MCP Server - the other direction: give your agent cloud tools
- Deploy From Your Coding Agent - let the agent run Railway itself
- OpenCode Developer Guide 2026 - the full tour of the CLI behind this server
Get the next deep dive like this in your inbox
One email a week on opencode and the rest of the AI dev stack. Free.
Read next on AI coding tools
Put an AI Agent on a Cron Job: Automating Dev Chores with OpenCode
An agent CLI plus a cron schedule turns recurring dev chores into background work: dependency bumps, doc freshness checks, morning briefs. The pattern, the guardrails, and where to run it - your own hardware or a cloud host.
11 min readPut an AI Agent Behind a Webhook: Turn GitHub Issues into Pull Requests
The most common trigger for an AI coding agent is not a clock, it is an event. A GitHub webhook, a Railway service, and OpenCode headless add up to a repo where a labeled issue gets a real pull request without anyone at the keyboard. The full build, start to finish.
10 min readShip a Remote MCP Server: Give Your Coding Agent Cloud Tools in an Afternoon
MCP just became stateless, which means your own MCP server is now just an HTTP endpoint that deploys like any web service. Build one with an agent, deploy it on Railway, and point opencode or Claude Code at the public URL. The full build, start to finish.
10 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








