Skip to content

OpenCode Hosting on Railway: No Docker Required

OpenCode hosting on Railway with a headless HTTP server, web interface, and secure API access without Docker

Running OpenCode locally is straightforward. Hosting it is where network binding, secrets, and persistent state become real concerns. Railway can run applications from a Git repository without requiring your own Dockerfile or Kubernetes cluster. For an OpenCode deployment, you must validate the selected package and runtime, bind to Railway’s port, listen on all interfaces, protect the public endpoint, and decide what must survive a redeploy.

This guide presents a validation-first deployment procedure for either the headless API or the browser UI. It uses opencode-ai version 1.0.0 and Node.js 22 as candidate repository choices, but does not assume that this exact combination has already been verified. Before treating the configuration as reproducible, confirm the package metadata, install it from a generated npm lockfile, test it under Node.js 22, and verify that the repository-local executable supports --version, serve, and web.

Check the candidate release against the official OpenCode installation documentation and the npm package metadata for the exact release. Retain the metadata, installation logs, runtime versions, CLI output, and lockfile as release evidence. If validation fails, select a documented OpenCode release or supported runtime instead of deploying this configuration unchanged.

What Railway hosting changes for OpenCode

A Railway deployment sits behind a managed public endpoint. The application needs to:

  • Start from a validated repository build.
  • Use Railway’s PORT environment variable.
  • Bind to 0.0.0.0, not the loopback interface.
  • Read credentials from environment variables.
  • Write durable state to a persistent volume when required.
  • Keep running as an always-on service.

These requirements and candidate choices come from separate sources:

  • Current OpenCode documentation: The serve and web commands, their host and port options, authentication variables, CORS options, and HTTP routes are described by OpenCode’s server documentation and OpenCode’s web documentation. These pages do not prove that every command exists in opencode-ai@1.0.0; test the pinned binary.
  • Railway facts: Railway’s public networking documentation requires a publicly reachable application to listen on 0.0.0.0 and documents PORT, generated domains, and automatic HTTPS.
  • Choices to validate: The package, Node.js 22, scripts, dependency layout, volume path, and always-on process are repository-specific decisions.

This is the same general pattern used when you deploy agents without Docker or Terraform: source code and locked dependencies go into Git, while runtime configuration and secrets stay outside it.

Headless API mode

Test:

./node_modules/.bin/opencode serve --help

According to the OpenCode server documentation, serve starts a headless HTTP server for OpenCode clients. Use this mode when another application, script, or OpenCode client will call the service, but only after the pinned binary confirms the command and required options.

Browser UI mode

Test:

./node_modules/.bin/opencode web --help

According to the OpenCode web documentation, web starts OpenCode as a web application. Its documented local defaults do not fit Railway’s public networking model, so supply the host and Railway-provided port explicitly.

Use web mode when developers need browser access. Avoid running both modes in one Railway service unless you have a clear routing and lifecycle plan.

Prepare a reproducible Railway project

Use this repository structure:

.
├── scripts/
│   ├── install-opencode.sh
│   └── start-opencode.sh
├── opencode.json
├── package.json
├── package-lock.json
└── README.md

Create this candidate package.json:

{
  "name": "opencode-railway",
  "version": "1.0.0",
  "private": true,
  "description": "Candidate OpenCode deployment for Railway",
  "engines": {
    "node": "22.x"
  },
  "dependencies": {
    "opencode-ai": "1.0.0"
  }
}

The exact dependency version is deliberately written without ^ or ~. The engines field requests Node.js 22 but does not prove that the package supports it.

Inspect the registry metadata:

npm view opencode-ai@1.0.0 version
npm view opencode-ai@1.0.0 dist.integrity
npm view opencode-ai@1.0.0 engines
npm view opencode-ai@1.0.0 bin

Compare the results with the OpenCode installation documentation and the selected npm release page. Confirm the intended version, a usable CLI in bin, and compatible runtime constraints. Absence of an engines constraint is not proof of Node.js 22 compatibility.

Generate and test the lockfile in a clean Node.js 22 environment:

node --version
npm --version
npm install --package-lock-only
npm ci --omit=dev

The generic behavior of npm ci is documented in the npm CLI documentation, but successful installation of this dependency graph must be demonstrated.

Verify the local executable:

test -x ./node_modules/.bin/opencode
./node_modules/.bin/opencode --version
./node_modules/.bin/opencode --help
./node_modules/.bin/opencode serve --help
./node_modules/.bin/opencode web --help

Do not proceed unless:

  • The repository-local executable exists.
  • --version identifies release 1.0.0.
  • serve and web are available.
  • Their help output lists the required hostname and port options.
  • No runtime or module-loading error occurs under Node.js 22.

If a check fails, select a verifiably supported release or runtime, regenerate the lockfile, and repeat the tests.

Commit both dependency files:

git add package.json package-lock.json
git commit -m "Pin validated OpenCode release for Railway"

Do not hand-edit package-lock.json. When updating OpenCode, change the exact version intentionally, regenerate the lockfile, and rerun every verification step.

Add scripts/install-opencode.sh:

#!/usr/bin/env sh
set -eu

test -f package.json
test -f package-lock.json

printf 'Node.js version:\n'
node --version

printf 'npm version:\n'
npm --version

npm ci --omit=dev

test -x ./node_modules/.bin/opencode

EXPECTED_VERSION="1.0.0"
INSTALLED_VERSION="$(./node_modules/.bin/opencode --version)"

printf 'Expected OpenCode version: %s\n' "$EXPECTED_VERSION"
printf 'Installed OpenCode version: %s\n' "$INSTALLED_VERSION"

case "$INSTALLED_VERSION" in
  *"$EXPECTED_VERSION"*) ;;
  *)
    printf 'OpenCode version verification failed.\n' >&2
    exit 1
    ;;
esac

./node_modules/.bin/opencode --help >/dev/null
./node_modules/.bin/opencode serve --help >/dev/null
./node_modules/.bin/opencode web --help >/dev/null

printf 'Validated local CLI commands: --version, serve, web\n'

Run the script in a clean environment and retain its successful output. Review the actual version format to ensure that matching 1.0.0 cannot accept an unrelated string.

Make it executable:

chmod +x scripts/install-opencode.sh

After confirming serve, --hostname, and --port, add scripts/start-opencode.sh:

#!/usr/bin/env sh
set -eu

: "${PORT:?Railway must provide PORT}"
: "${OPENCODE_SERVER_PASSWORD:?Set OPENCODE_SERVER_PASSWORD}"

exec ./node_modules/.bin/opencode serve \
  --hostname 0.0.0.0 \
  --port "$PORT"

Then:

chmod +x scripts/start-opencode.sh

The repository-local path ensures that the process uses the version installed from the committed lockfile rather than an unrelated global executable.

Current OpenCode documentation describes --hostname and --port in its server documentation. Railway requires 0.0.0.0 and the injected PORT value in its public networking guide.

For a browser-facing deployment, replace the final command only after validating web --help:

exec ./node_modules/.bin/opencode web \
  --hostname 0.0.0.0 \
  --port "$PORT"

OpenCode also documents server settings in opencode.json under server in its configuration reference. Command-line options keep Railway’s host and port explicit.

Deploy OpenCode on Railway without Docker

Create a Railway service from the GitHub repository after the package, runtime, lockfile, and CLI tests pass. Railway documents command overrides in its build configuration guide and start command guide.

Set the Railway Build Command to:

./scripts/install-opencode.sh

Set the Railway Start Command to:

./scripts/start-opencode.sh

Inspect the build logs and confirm the exact Node.js version. The engines field expresses the requested major version; the observed build log is the deployment evidence. If Railway does not select the tested release, configure the builder according to Railway’s current documentation or validate the runtime it actually uses.

Before the first public deployment, add OPENCODE_SERVER_PASSWORD and the required model-provider API keys as separate service variables. Railway documents their configuration in its variables guide.

Deploy and verify that the logs show:

  • The tested Node.js and npm versions.
  • The expected and installed OpenCode versions.
  • Successful validation of --version, serve, and web.
  • A process that starts and remains running.

Generate a Railway domain in the service networking settings. Railway’s public networking documentation describes generated domains, proxy routing, automatic SSL certificates, and HTTPS termination.

Do not hard-code OpenCode’s local default port into the public URL. Railway accepts public HTTPS traffic through its proxy, while OpenCode listens on the internal HTTP port supplied through PORT.

For API mode, the effective process is:

./node_modules/.bin/opencode serve \
  --hostname 0.0.0.0 \
  --port "$PORT"

Current OpenCode documentation describes an OpenAPI specification at /doc. After verifying that route in the pinned release, access it through the Railway domain:

https://your-service-domain.example/doc

For browser access, use the validated web command instead. Prefer serve for API clients and integrations, and web when the hosted browser interface is the primary entry point.

This flow follows the same broad stages described in this production agent deployment guide: validate the runtime, build, inject configuration, start the process, and verify the public endpoint.

Secure the public service and manage provider secrets

Do not expose OpenCode publicly without a password. The OpenCode server documentation states that the web server is unsecured when OPENCODE_SERVER_PASSWORD is absent. Confirm this behavior against the pinned release.

Set:

OPENCODE_SERVER_PASSWORD=<secret value>

Current documentation says that the server uses HTTP Basic authentication and defaults to the username opencode. The documented override is:

OPENCODE_SERVER_USERNAME=<chosen username>

Validate both details with the running release. Store provider credentials as Railway variables, following Railway’s variables documentation, never in:

  • opencode.json
  • Shell scripts
  • Committed .env files
  • Source code
  • Build output or runtime logs

Use separate development and production credentials. Restrict access to production variables and rotate any secret exposed in a commit or log. See the production agent security checklist for broader secrets and isolation patterns.

Current OpenCode documentation describes CORS through repeated --cors flags in its server reference. Confirm the syntax in the pinned binary before use.

Add only known frontend origins:

exec ./node_modules/.bin/opencode serve \
  --hostname 0.0.0.0 \
  --port "$PORT" \
  --cors "https://app.example.com"

CORS is a browser policy, not a substitute for authentication.

Configure persistence, domains, and operational safeguards

Potential durable data includes sessions, project configuration, generated files, and other state created during agent work. Railway volumes provide persistent storage at a configured mount path, as documented in Railway’s volumes guide.

OpenCode’s exact storage directories are version-specific. For the selected release:

  1. Run the locked installation in a test environment.
  2. Create representative sessions and files.
  3. Inspect the filesystem paths changed by those operations.
  4. Compare them with the selected release’s documentation.
  5. Record the verified paths.

If the state must survive redeployment:

  1. Attach a Railway volume.
  2. Select a mount path such as /data.
  3. Configure supported OpenCode or XDG paths beneath it.
  4. Create representative state.
  5. Redeploy and verify that the state remains available.

If testing confirms support for the relevant XDG paths, candidate variables are:

XDG_DATA_HOME=/data/.local/share
XDG_CONFIG_HOME=/data/.config
XDG_STATE_HOME=/data/.local/state

This mapping must be validated. It does not prove that project files or every artifact are stored there, and mounting a volume does not move existing data automatically.

Without a volume, treat the service filesystem as disposable and export important artifacts to an external durable system.

Use Railway’s generated HTTPS domain first. Add a custom domain only after it works. Railway documents domains, certificates, and routing in its public networking guide.

Monitor repeated restarts, provider failures, authentication errors, storage growth, resource pressure, unexpected model usage, version changes, failed CLI validation, and writes outside the volume. Review resource and provider spending using the guide to production AI agent costs.

Verify and troubleshoot the Railway deployment

The OpenCode server documentation lists GET /global/health. Verify its presence in the pinned release:

export OPENCODE_URL="https://your-service-domain.example"
export OPENCODE_USER="opencode"
export OPENCODE_PASSWORD="<your password>"

curl --fail-with-body \
  --user "$OPENCODE_USER:$OPENCODE_PASSWORD" \
  "$OPENCODE_URL/global/health"

Confirm that the returned version matches the clean installation and Railway build.

Test the API contract:

curl --fail-with-body \
  --user "$OPENCODE_USER:$OPENCODE_PASSWORD" \
  "$OPENCODE_URL/doc"

Use that contract to confirm request bodies and methods. The current documentation describes session listing as:

curl --fail-with-body \
  --user "$OPENCODE_USER:$OPENCODE_PASSWORD" \
  "$OPENCODE_URL/session"

It also describes an SSE stream at GET /global/event:

curl --no-buffer \
  --user "$OPENCODE_USER:$OPENCODE_PASSWORD" \
  "$OPENCODE_URL/global/event"

If the domain returns an error, check these issues in order:

  1. Registry metadata or installation does not match opencode-ai@1.0.0.
  2. Node.js 22 was not validated successfully.
  3. The pinned binary lacks serve, web, or a required option.
  4. The process listens on 127.0.0.1 instead of 0.0.0.0.
  5. The command uses a fixed port instead of $PORT.
  6. npm ci failed or the local executable is missing.
  7. The installed or running version differs from the tested release.
  8. Authentication or provider variables are missing or stale.
  9. State is written outside the persistent volume.
  10. package.json changed without regenerating and testing the lockfile.

Current documentation also describes attaching a terminal UI to an existing server. Test the command before use:

./node_modules/.bin/opencode attach --help
./node_modules/.bin/opencode attach http://localhost:4096

Keep that URL for a local server. For remote access, use an authenticated private path or suitable tunnel rather than assuming public attachment is safe.

Conclusion

A reliable OpenCode deployment on Railway needs more than a start command. Validate that opencode-ai@1.0.0 has the expected registry metadata, installs from the committed lockfile with npm ci --omit=dev, works under the selected Node.js 22 release, and exposes --version, serve, and web.

Retain package metadata, runtime versions, installation logs, CLI output, the lockfile, Railway build logs, and runtime checks as evidence. If a test fails, choose a documented and successfully tested package-runtime combination.

After validation, bind OpenCode to 0.0.0.0 and Railway’s $PORT, enable HTTP Basic authentication, keep provider keys in Railway variables, and use Railway’s HTTPS endpoint. Attach a volume only after verifying OpenCode’s state paths and testing persistence through a redeploy.

Use serve for a validated API deployment and web for a validated browser deployment. Include /global/health, /doc, version comparisons, logs, and recovery tests in the deployment checklist.

Railway suits an always-on managed service once the package and runtime combination has been validated. For agents that run only on schedules or events, compare this daemon model with job-oriented hosting before committing to the runtime.