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:
.localfor opencode’s internal state.config/opencodefor configurationprojectfor the actual code the agent works on
Architecture
Prerequisites
You need the Upsun CLI (docs.upsun.com/administration/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 GitHubhttp://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 GitHubcomposable: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 GitHubscripts/install_upsun_cli.sh
View source on GitHubscripts/create_opencode_project.sh
View source on GitHubDeployment
Initialize the project
Create Upsun project
Set API keys
Set at least one LLM provider:Deploy
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:
Security review agent
npm audit or pip-audit, scans code, generates a report.
Screenshot agent
Performance agent
Customizing agents
Add your own toopencode.json:
chrome_ prefix enables all chrome MCP tools.
Redeploy after changes:
Working with code
The/app/project directory is your workspace:
Troubleshooting
”No providers configured”
Check your API keys:CLAUDE_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY needs to be set.
Chrome MCP not connecting
First check if Chrome is running:Upsun CLI not working in the agent
You probably forgot the API token: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
- opencode Documentation
- MCP Specification
- chrome-devtools-mcp
- Upsun Chrome Headless Service
- Upsun Composable Images