Skip to main content
This tutorial covers deploying opencode, a self-hosted AI coding agent, on Upsun. The setup includes a headless Chrome service that agents can use for web automation, performance testing, and screenshots. You’ll learn how to work around read-only containers using mounts, connect services via MCP (Model Context Protocol), and configure specialized agents.

What you’re building

A web-accessible coding agent that runs in your browser. You open the web UI, and you have an AI assistant that can read files, run commands, write code, and talk to a headless browser. Four agents come pre-configured: one for managing Upsun deployments, one for security reviews, one for screenshots, and one for performance analysis. The Chrome integration is where it gets interesting. opencode supports MCP servers, and chrome-devtools-mcp exposes browser automation as tools. Your agent can navigate pages, take screenshots, record performance traces without you writing puppeteer scripts. The agent calls the tool; the MCP server handles the browser stuff.

Why the setup looks like this

Upsun containers are read-only. You can’t write to /app at runtime. This is a problem because opencode needs to write files: configuration, auth tokens, project state. And the whole point of a coding agent is writing code. The solution is mounts. We carve out writable directories backed by persistent storage:
  • .local for opencode’s internal state
  • .config/opencode for configuration
  • project for the actual code the agent works on
Without these mounts, opencode crashes on first write. I spent a while debugging this before realizing the issue was the read-only filesystem. The error messages don’t make it obvious.

Architecture

opencode connects to Chrome via MCP. The chrome-devtools-mcp package talks to Chrome’s remote debugging protocol. Upsun exposes the Chrome service URL through environment variables, which we inject into the MCP config at deploy time.

Prerequisites

You need the Upsun CLI, Git, and at least one LLM API key (OpenAI, Anthropic, or Google).

Project structure

The configuration files

opencode.json

This file defines the MCP servers and custom agents: View source on GitHub
The http://CHROME placeholder gets replaced at deploy time with the actual Chrome service URL. Each agent has restricted tools: the upsun agent only gets bash (to run CLI commands), the security agent gets bash and read (to scan files), the screenshot agent gets bash and write (to save images), and the performance agent only gets chrome tools. Why restrict tools? So agents stay in their lane. You don’t want the security scanner accidentally modifying files. Least privilege.

.upsun/config.yaml

View source on GitHub
We use composable:25.11 instead of a fixed runtime type because it lets us pick nodejs@24 from the stack. HIGH_MEMORY profile because AI coding agents are surprisingly memory-hungry. The models aren’t running locally, but the tools and Chrome connections add up. The relationships.chrome entry exposes CHROME_SCHEME, CHROME_IP, and CHROME_PORT as environment variables. The deploy hook uses sed to inject these into the opencode config. It’s a bit hacky, yes. But it works, and I couldn’t find a cleaner way to get dynamic service URLs into a static JSON config. The deploy hook does a lot: create directories, generate auth.json, copy and patch config, init git, create project file. All of this runs before the app starts. Verbose, but necessary because containers are read-only and opencode expects certain files to exist.

The helper scripts

scripts/setup_auth.sh

View source on GitHub
This builds auth.json from environment variables. Set whichever API keys you have, and opencode uses them. You can set multiple and switch in the UI.

scripts/install_upsun_cli.sh

View source on GitHub
The upsun agent needs the CLI. We download it from GitHub releases.

scripts/create_opencode_project.sh

View source on GitHub
opencode expects a project file pointing to the working directory. This creates one if missing.

Deployment

Initialize the project

Create Upsun project

Pick a region close to you since you’ll be interacting with the web UI.

Set API keys

Set at least one LLM provider:
You can set multiple. opencode lets you switch. If you want the upsun agent to manage projects, you also need an API token:
Generate one at console.upsun.com under Account Settings > API Tokens.

Deploy

Watch the build. It installs opencode, puppeteer, the Upsun CLI, and sets up directories. When done:
Opens the opencode web UI.

Using the agents

The main interface

opencode runs in your browser. Type and the agent responds. It reads and writes files, runs commands, uses MCP tools.

Upsun agent

Switch by typing @upsun:
It has bash and the CLI installed. Configured with safe defaults.

Security review agent

Runs npm audit or pip-audit, scans code, generates a report.

Screenshot agent

Writes a puppeteer script, runs it, saves screenshots.

Performance agent

Uses chrome-mcp tools directly. Navigates, records traces, reports on Web Vitals. No puppeteer scripts needed.

Customizing agents

Add your own to opencode.json:
Restrict tools based on what the agent needs. The chrome_ prefix enables all chrome MCP tools. Redeploy after changes:

Working with code

The /app/project directory is your workspace:
Or SSH in:
The mount persists across deploys.

Troubleshooting

”No providers configured”

Check your API keys:
One of CLAUDE_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY needs to be set.

Chrome MCP not connecting

First check if Chrome is running:
If that fails, look at service logs:

Upsun CLI not working in the agent

You probably forgot the API token:
Test it:

Out of memory

Try a bigger container:

Files disappearing after deploy

You wrote somewhere outside the mounts. Only .local, .config/opencode, and project persist. Everything else in /app resets on deploy. This trips people up a lot.

Why this setup

Chrome runs as a separate service because putting it in the app container would be a mess. Resource conflicts, bloated container, harder to debug. A dedicated service keeps things clean. MCP instead of puppeteer directly because MCP is how opencode extends itself. Agents call tools. chrome-mcp translates those into browser actions. If you used puppeteer directly, the agent would have to write and run scripts. MCP is cleaner. Multiple agents with restricted permissions because you don’t want one agent doing everything. The security scanner shouldn’t write files. The upsun agent shouldn’t edit code. Keeps things focused. The deploy hook is verbose because containers are read-only. We have to set up writable directories and populate config during deploy. Annoying but necessary.

Resources

For questions, check the Upsun Community Forum or open an issue in this repo.
Last modified on May 13, 2026