Skip to content

Latest commit

Β 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

OpenCode Railway + Tailscale

Railway Tailscale License

A secure, production-ready deployment of OpenCode on Railway with Tailscale network access. This setup provides a fully-featured AI coding agent accessible via Tailscale's mesh VPN and SSH, with zero public internet exposure required.

Features

  • Tailscale-Only Access: Zero trust networking β€” access your OpenCode server only via your Tailscale network
  • Auto SSH Key Management: Automatic SSH key generation or custom key injection
  • Password-Protected Web UI: Built-in authentication layer for the OpenCode web interface
  • Persistent Storage: Workspace data persists across container restarts via Railway volumes
  • Persistent Tailscale Identity: Same Tailscale IP across deployments (no zombie nodes)
  • Pre-installed Development Tools: Complete toolchain including Node.js, Bun, Docker CLI, Git, and more
  • Memory Monitoring: Optional auto-restart on memory thresholds or idle timeouts
  • GitHub CLI Integration: Pre-authenticated gh CLI for seamless GitHub workflows
  • Health Check Endpoints: Built-in monitoring for Railway deployment health
  • Graceful Shutdowns: Proper signal handling for zero-downtime redeployments

Quick Start

Deploy to Railway

Click the button below to deploy instantly:

Deploy on Railway

Manual Deployment

  1. Fork or clone this repository

    git clone https://github.com/yourusername/railway-opencode-tailscale.git
    cd railway-opencode-tailscale
  2. Create a new Railway project

    railway init
  3. Configure environment variables (see Environment Variables)

  4. Deploy

    railway up

Detailed Setup

1. Set OpenCode Server Password (Optional)

The OPENCODE_SERVER_PASSWORD variable is optional. When running with Tailscale-only access (recommended), authentication is handled by your tailnet membership:

# Via Railway CLI (optional - for password protection)
railway variables set OPENCODE_SERVER_PASSWORD="your-secure-password"

# Or via Railway Dashboard
# Project β†’ Your Service β†’ Variables β†’ Add

Tailscale-only mode: If no password is set, the web UI is accessible to anyone in your Tailscale network (requires Tailscale authentication).

2. Tailscale Authentication

Choose one authentication method:

Option A: Tagged Auth Key (Recommended)

Generate a reusable, non-ephemeral auth key with tags for persistent node identity:

  1. In your Tailscale admin panel:

    • Go to Settings β†’ Keys β†’ Auth Keys
    • Click Generate auth key
    • Reusable: βœ… Yes
    • Ephemeral: ❌ No (disabled for persistent identity)
    • Tags: Add tag:railway-opencode (see ACL Policy)
    • Pre-approved: βœ… Yes (if device approval is enabled)
  2. Set the environment variable:

    railway variables set TAILSCALE_AUTH_KEY="tskey-auth-..."
    railway variables set TAILSCALE_HOSTNAME="railway-opencode"
  3. On first deployment, the node will register with your tailnet. Subsequent deployments will reuse the same node identity and IP address.

Option B: Ephemeral Key (Alternative)

For short-lived testing environments where IP stability isn't required:

  1. Generate an ephemeral auth key in your Tailscale admin panel:

    • Reusable: βœ… Yes
    • Ephemeral: βœ… Yes (node auto-removes after 30-60 min offline)
  2. Set the environment variable:

    railway variables set TAILSCALE_AUTH_KEY="tskey-auth-..."

Note: Ephemeral keys create new nodes on each deployment with different IPs.

Option B: Interactive Authentication

  1. Deploy without TAILSCALE_AUTH_KEY:

    railway up
  2. Check Railway logs for the authentication URL:

    railway logs
  3. Visit the displayed URL to authorize the device

  4. The container will automatically continue once authenticated

3. SSH Access Configuration

SSH is enabled by default. Configure key access:

Option A: Provide Your Own Keys

Set your public key as an environment variable:

# Copy your public key
export TAILSCALE_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)"
railway variables set TAILSCALE_SSH_PUBKEY="$TAILSCALE_SSH_PUBKEY"

Optionally, also set the private key for outbound SSH:

export TAILSCALE_SSH_PRIVKEY="$(cat ~/.ssh/id_ed25519)"
railway variables set TAILSCALE_SSH_PRIVKEY="$TAILSCALE_SSH_PRIVKEY"

Option B: Auto-Generated Keys

If TAILSCALE_SSH_PUBKEY is not set, the container will:

  1. Generate a new ed25519 keypair on first startup
  2. Display the private key in the logs (copy this!)
  3. Display the public key in the logs
  4. Save both to /var/lib/opencode/.ssh/

⚠️ Important: Save the displayed private key immediately β€” it won't be shown again!

Tailscale ACL Policy

Use Tailscale ACLs to control access to your OpenCode Railway nodes:

  1. Go to Tailscale Admin Console β†’ Access Controls
  2. Adapt the example from tailscale-acl.hujson
  3. Key configurations:
    • Define tag:railway-opencode for your Railway nodes
    • Set tagOwners to control who can create tagged devices
    • Configure SSH access rules for the opencode user

Example ACL snippet:

{
  "tagOwners": {
    "tag:railway-opencode": ["your-email@example.com"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["your-email@example.com"],
      "dst": ["tag:railway-opencode:22", "tag:railway-opencode:18080"]
    }
  ],
  "ssh": [
    {
      "action": "check",
      "src": ["your-email@example.com"],
      "dst": ["tag:railway-opencode"],
      "users": ["opencode", "root"]
    }
  ]
}

Cleaning Up Legacy Nodes

If you previously used ephemeral keys or undeployed services, you may have zombie oc-* nodes in your tailnet. Use the cleanup utility:

# 1. Get a Tailscale API key
# Visit: https://login.tailscale.com/admin/settings/keys
# Create key with "Devices: Write" scope

# 2. Preview what will be deleted
export TAILSCALE_API_KEY="tskey-api-xxxx"
./tailscale-cleanup.sh --dry-run

# 3. Delete matching nodes
./tailscale-cleanup.sh

The script matches nodes with names starting with oc- (e.g., oc-abc123).

Environment Variables

Variable Required Default Description
OPENCODE_SERVER_PASSWORD ❌ No β€” Password for web UI authentication (optional with Tailscale)
TAILSCALE_AUTH_KEY ❌ No β€” Tailscale auth key (recommended: reusable + tagged)
TAILSCALE_HOSTNAME ❌ No railway-opencode Hostname in Tailscale network (used for node identity)
TAILSCALE_SSH_PUBKEY ❌ No β€” SSH public key for authentication
TAILSCALE_SSH_PRIVKEY ❌ No β€” SSH private key for outbound connections
ENABLE_SSH ❌ No true Enable SSH server (true/false)
ENABLE_MONITOR ❌ No false Enable memory/idle monitoring (true/false)
IDLE_TIME_MINUTES ❌ No 10 Idle time before auto-restart
MEMORY_THRESHOLD_MB ❌ No 2000 Memory threshold for auto-restart (MB)
GITHUB_TOKEN ❌ No β€” GitHub CLI authentication token
GIT_USER_NAME ❌ No β€” Git global user.name
GIT_USER_EMAIL ❌ No β€” Git global user.email
LOG_LEVEL ❌ No INFO Server log level (DEBUG/INFO/WARN/ERROR)
RAILWAY_API_TOKEN ❌ No β€” Railway API token for graceful restarts
TAILSCALE_API_KEY ❌ No β€” Tailscale API key (for cleanup script only)

Access Methods

Method 1: Railway Public URL (Optional Password)

After deployment, Railway provides a public URL:

https://your-project-name.up.railway.app

If you configured OPENCODE_SERVER_PASSWORD, access requires authentication. If running in Tailscale-only mode (no password), the proxy allows direct access.

Method 2: Tailscale Direct (Private)

Connect via the Tailscale IP from any device in your tailnet:

  1. Get the Tailscale IP from logs:

    railway logs | grep "Tailscale IP"
  2. Access OpenCode UI:

    http://100.x.x.x:18080  # Replace with actual IP
    

Method 3: SSH Access

Connect directly via SSH:

# With provided keys (Option A)
ssh opencode@100.x.x.x

# With auto-generated keys (Option B)
ssh -i ~/.ssh/railway_opencode opencode@100.x.x.x

Note: SSH listens only on the Tailscale interface for security.

⚠️ Security Considerations

Session Persistence

Important: This deployment uses in-memory session storage. Sessions will be lost when:

  • The container restarts (redeploy, crash, or Railway sleep/wake cycle)
  • Railway scales horizontally (if multi-instance in future)

Workaround: Use Basic Auth for scripts/API access, or re-authenticate after redeploys.

Rate Limiting

The login endpoint has built-in rate limiting:

  • 5 attempts per 15-minute window per IP
  • 429 response with Retry-After header when exceeded

Password Security

  • Passwords are compared using timing-safe operations to prevent timing attacks
  • OPENCODE_SERVER_PASSWORD is optional β€” when running in Tailscale-only mode, no password is required
  • Use a strong, unique password if you want additional authentication layer
  • Consider using Basic Auth for automated access instead of session cookies

SSH Key Management

  • Never commit private keys to version control
  • Auto-generated keys are displayed once in logs β€” save them immediately
  • For production, provide keys via Railway secrets (not environment variables)

Network Security

  • OpenCode binds to 127.0.0.1 (localhost only) β€” not directly accessible
  • Proxy server adds authentication layer before forwarding to OpenCode
  • Tailscale provides mesh VPN encryption for all traffic
  • SSH is disabled by default β€” must explicitly enable with ENABLE_SSH=true

Secrets Management

Avoid setting these in plaintext environment variables in production:

  • TAILSCALE_SSH_PRIVKEY β€” use Railway's secret file mounting instead
  • Consider using Railway's built-in secrets management for all sensitive values

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Railway Container                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚                    OpenCode Server                      β”‚    β”‚
β”‚  β”‚                   (Port 18081, internal)                β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                              ↑                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚                 Authentication Proxy                     β”‚    β”‚
β”‚  β”‚         (Port 18080, public via Railway)              β”‚    β”‚
β”‚  β”‚         β€’ Password authentication                      β”‚    β”‚
β”‚  β”‚         β€’ Session management                           β”‚    β”‚
β”‚  β”‚         β€’ WebSocket proxying                           β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                              ↑                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚                  Tailscale Daemon                       β”‚    β”‚
β”‚  β”‚         (Userspace networking, no iptables)           β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                              ↑                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚                   SSH Server                            β”‚    β”‚
β”‚  β”‚         (Port 22, Tailscale interface only)           β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↑
                    Tailscale Network (Tailnet)
                              ↑
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚              Your Devices (iPhone, Laptop, etc.)     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pre-installed Tools

Development

  • Node.js: v20 (LTS) and v22 (latest) via FNM
  • Bun: Latest stable release
  • npm/npx: Via Node.js installations

Version Control

  • Git: Latest stable with LFS support
  • git-filter-repo: For repository history rewriting
  • GitHub CLI (gh): Pre-configured if GITHUB_TOKEN is set

System & Network

  • Tailscale: Mesh VPN client
  • OpenSSH Server: Secure remote access
  • curl: HTTP client
  • ca-certificates: SSL/TLS certificates

Media Processing

  • FFmpeg: Audio/video processing

Container Tools

  • Docker CLI: Client-only for remote Docker hosts
  • docker-compose: Via Docker CLI plugins

Utilities

  • build-essential: GCC, make, and build tools
  • procps: Process monitoring (ps, top, etc.)
  • iproute2: Network management tools
  • unzip: Archive extraction

Security Considerations

Network Security

  • Tailscale Only: SSH listens exclusively on the Tailscale interface (100.x.x.x), not 0.0.0.0
  • No Password Auth: SSH requires key-based authentication only
  • Mesh Encryption: All Tailscale traffic is encrypted with WireGuard
  • ACLs: Use Tailscale ACLs to restrict access within your tailnet

Container Security

  • Non-root User: OpenCode runs as opencode user (UID 1000)
  • Minimal Base: Built on debian:bookworm-slim
  • No Docker Daemon: Client-only tools (no privileged container needed)
  • Read-only Root: Where possible, filesystems are read-only

Authentication

  • Password Required: Web UI requires OPENCODE_SERVER_PASSWORD
  • Session Management: HTTP-only cookies with 30-day expiration
  • Basic Auth: Optional HTTP Basic Auth for API access

Secrets Management

  • Never Commit Keys: All authentication is via environment variables
  • Tagged Auth Keys: Use reusable, non-ephemeral auth keys with tags for persistent identity
  • Private Key Storage: If using auto-generated keys, save the private key immediately
  • State Persistence: Tailscale node state (/data/tailscale) persists across deployments β€” don't delete the Railway volume

Troubleshooting

Container Won't Start

# Check logs for errors
railway logs --tail 100

# Verify environment variables
railway variables

Common causes:

  • Invalid TAILSCALE_AUTH_KEY format
  • OpenCode server not starting (check for missing AI provider configuration)
  • Health check endpoint not responding (/global/health on port 18081)

Tailscale Not Connecting

# Check Tailscale status in container
railway ssh

tailscale status

# If stuck, try re-authenticating
tailscale down
tailscale up --authkey=$TAILSCALE_AUTH_KEY

Tailscale Node Identity Not Persisting

If your Tailscale IP changes on every deployment:

  1. Check state directory exists:

    railway ssh
    ls -la /data/tailscale/
    # Should show: tailscaled.state, other state files
  2. Verify volume mount:

    df -h | grep /data
    # Should show: /dev/... mounted on /data
  3. Check logs for state detection:

    railway logs
    # Should show: "Found existing Tailscale state" or "No existing Tailscale state found"
  4. Ensure auth key is NOT ephemeral: Ephemeral keys create new nodes each time. Use a reusable, non-ephemeral key with tags instead.

  5. If state is corrupted: Temporarily move the state directory to force re-authentication:

    railway ssh
    mv /data/tailscale /data/tailscale.backup
    # Redeploy to create fresh state

SSH Connection Refused

  1. Verify Tailscale is connected:

    tailscale ping 100.x.x.x
  2. Check SSH is enabled:

    railway variables get ENABLE_SSH
    # Should return: true
  3. Verify key permissions inside container:

    ls -la /var/lib/opencode/.ssh/
    # Should be: 600 for private key, 600 for authorized_keys

Out of Memory

If the container is restarting due to memory limits:

# Enable memory monitoring for auto-restart
railway variables set ENABLE_MONITOR=true
railway variables set MEMORY_THRESHOLD_MB=1500

Or upgrade your Railway plan for more memory.

Web UI Login Issues

  1. Clear browser cookies for the Railway domain

  2. Verify password is set correctly:

    railway variables set OPENCODE_SERVER_PASSWORD="new-password"
    railway up
  3. Check for typo in password (case-sensitive)

OpenCode Server Not Responding

# Check if OpenCode process is running
railway ssh

ps aux | grep opencode

# Check proxy logs
cat /var/log/opencode-monitor.log 2>/dev/null || echo "No monitor logs"

Volume Persistence Issues

Data should persist in /data. If lost:

# Verify volume is mounted
railway ssh
df -h | grep /data

# Check workspace contents
ls -la /data/

# Check Tailscale state
ls -la /data/tailscale/

Ensure railway.toml has the volume mount configured:

[deploy.volumeMounts]
mountPath = "/data"

The /data volume stores:

  • Workspace data (/data/workspace)
  • OpenCode config (/data/.config, /data/.local)
  • Tailscale node state (/data/tailscale) β€” for persistent identity across deployments

Development

Local Testing

Build and test locally with Docker:

# Build image
docker build -t opencode-railway .

# Run locally (for testing only β€” requires Railway env)
docker run -it \
  -e OPENCODE_SERVER_PASSWORD=test \
  -e TAILSCALE_AUTH_KEY=tskey-auth-... \
  -p 18080:18080 \
  opencode-railway

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Test on Railway
  5. Submit a pull request

License

MIT License β€” see LICENSE for details.

Acknowledgments

  • OpenCode β€” The AI coding agent
  • Railway β€” Deployment platform
  • Tailscale β€” Zero-trust networking
  • FNM β€” Fast Node.js manager

About

Secure OpenCode server on Railway with Tailscale networking and SSH access

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages