Model or dataset
lehhair/OpenCodeUI avatar
lehhair/OpenCodeUI

OpenCodeUI: a third-party web and desktop frontend for the OpenCode agent server

https://lehhair.github.io/OpenCodeUI/

575 stars89 forksTypeScriptGPL-3.0

At a glance

What is it?
OpenCodeUI is a React and TypeScript frontend for the OpenCode agent, shipped as a static site, a Docker stack with a Caddy gateway and a Python port router, and a Tauri desktop client. It suits someone who wants a graphical chat, terminal and diff view over an opencode serve backend, and it is an early GPL-3.0 project built with AI assistance.
Who is it for?
OpenCodeUI fits a developer who already runs opencode serve and wants a browser or desktop interface over it, or a team that wants to expose the agent through a controlled gateway with authentication.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A frontend for a server that already exists

OpenCodeUI is a third-party web frontend for OpenCode, and the whole design follows from that positioning. The project description says it plainly: A third-party web frontend for OpenCode.

That means the interesting part of the system is not in this repository. The agent server is OpenCode's own, and OpenCodeUI is the client that talks to it. The quickest way to see the arrangement is to start the OpenCode backend locally and point a hosted frontend at it.

bash
opencode serve --cors "https://lehhair.github.io"

The cors argument is the whole contract between the two halves. The frontend is served from a different origin than the backend, so the browser will refuse cross-origin requests unless the backend is told to allow that origin. Opening the hosted frontend afterwards is enough to get a working session against your local server.

The feature list explains what a frontend for an agent is actually expected to do. A full chat interface with a message stream, Markdown rendering and Shiki code highlighting. A built-in terminal based on xterm.js with WebGL rendering. File browsing and diff, so you can view workspace files and compare multi-file changes. A theme system with three built-in themes, Eucalyptus, Claude and Breeze, light and dark switching, and custom CSS. PWA support so it can be installed as a desktop or mobile application. Mobile adaptation with safe areas, touch optimisation and a responsive layout. Browser notifications when an AI reply completes. At-mentions and slash commands for referencing files and running commands in the conversation. Configurable keyboard shortcuts.

The last three items on the list are the ones that make it a deployment product rather than a demo: Docker deployment with separated front and back end containers, a Tauri-based native desktop client for macOS, Linux and Windows, and dynamic port routing that discovers development services running inside the container and generates preview links for them.

The primary language is TypeScript, the licence is GPL-3.0, and the recent releases are v0.7.21, v0.7.20 and v0.7.19 in October 2026, with the last push on 2026-10-20.

The stack, and why the terminal and the diff view are the hard parts

The technology table is short and each choice is load-bearing.

React 19 with TypeScript for the framework, Vite 7 for the build, Tailwind CSS v4 for styling, Shiki for code highlighting, xterm.js with WebGL for the terminal, react-markdown with remark-gfm for Markdown, Tauri 2 for the desktop build, and Docker with Caddy plus a Python router for deployment.

The two dependencies that reveal the difficulty are @xterm/addon-webgl and the diff package. A terminal embedded in a web page is not an iframe of a shell, it is a terminal emulator rendering a byte stream that arrives over a channel, and the WebGL addon means the renderer is not the DOM, which is a different set of performance and character-handling problems. The xterm addons for fit, serialize and web links are all present, and fit and serialize matter because a terminal has to resize correctly and can be reattached to a session.

The diff package is there for the multi-file diff view, and the presence of the CodeMirror packages alongside it is more telling. @codemirror/state, view, commands and search are the building blocks of an in-browser code editor, and @opencode/client is the generated client for the OpenCode API at version 2.0.21. So the file browser is not a plain list with a read-only pane: it is a code editor with search, which is what you need when an agent has just edited a file and you want to read it properly.

Other dependencies point at the features. dompurify sanitises rendered Markdown. katex renders maths. i18next with the browser language detector provides translations. lucide-react and @tanstack/react-virtual handle icons and list virtualisation, which is what keeps a long chat or a large file list responsive. @tauri-apps/plugin-http, plugin-notification, plugin-dialog, plugin-fs and plugin-opener are the desktop-specific capabilities, and they are the boundary between the web build and the native build: browser notifications, file dialogs and filesystem access all come from a different source depending on how the app is running.

The scripts in package.json confirm the shape of the project. dev runs vite, build runs tsc -b and then vite build, and validate chains typecheck, lint, test:run and build, so the same four checks gate a release. The test runner is vitest, and there is a separate vitest.config.ts alongside the three tsconfig files for the app, the node tooling and the base configuration.

Deploying with the prebuilt images, without cloning anything

The default deployment is deliberately minimal: two files and a compose command, using prebuilt images from GitHub Container Registry.

bash
mkdir OpenCodeUI && cd OpenCodeUI

# 下载默认运行配置
curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/docker-compose.yml
curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/.env.example

# 复制并编辑环境变量,至少填写一个 LLM API Key
cp .env.example .env

# 启动
docker compose up -d

The readme notes that if your environment has no curl you can download docker-compose.yml and .env.example from GitHub into the same directory instead. Either way, the entry point is http://localhost:6658.

The architecture is four services behind a gateway. The gateway is Caddy on port 6658 as the single entry point that reverse proxies all requests, and on 6659 as a separate preview port for development services. The frontend serves static files on port 3000 internally. The backend runs opencode serve on port 4096 internally. The router handles dynamic port routing on 7070 internally and is built into the gateway.

The routing rules on 6658 are simple. Requests to /api/* go to the backend on 4096 and support SSE. Requests to /routes go to the router for the dynamic routing management panel. Requests to /preview/* go to the router for the preview port switching API. Everything else goes to the frontend as static assets.

Port 6659 is the interesting one. The router automatically scans ports 3000 to 9999 and generates preview links of the form /p/{token}/ for development services found inside the container. That is what makes the frontend useful for work on the agent itself, since a dev server started inside the backend container becomes reachable through the gateway without exposing it on a host port.

The environment file drives most of this. The ports are GATEWAY_PORT and PREVIEW_PORT, the workspace is WORKSPACE mounted at /workspace inside the container, the router interval and port range are ROUTER_SCAN_INTERVAL and ROUTER_PORT_RANGE with 4096 excluded so the backend itself is not previewed, and the token length is ROUTER_TOKEN_LENGTH.

The compose file binds both published ports to 127.0.0.1, which is the safe default for a machine you are sitting at.

The standalone frontend container, and authentication

If you already have an opencode serve running, you do not need the full stack. The standalone compose file gives you one frontend container that connects to an existing backend.

bash
git clone https://github.com/lehhair/OpenCodeUI.git
cd OpenCodeUI

# 启动(默认连接宿主机的 opencode serve :4096)
docker compose -f docker-compose.standalone.yml up -d

The frontend is then on http://localhost:3000. The default backend address is host.docker.internal:4096, and the port the frontend listens on is set with the PORT variable, so pointing at a remote backend is a one-line change.

bash
BACKEND_URL=your-server.com:4096 PORT=8080 docker compose -f docker-compose.standalone.yml up -d

That matters for the second thing the deployment does, which is authentication. When the backend has password authentication enabled with OPENCODE_SERVER_PASSWORD, the frontend asks for a username and password on the connection screen and then attaches an Authorization: Basic header to every request. The Caddy image shipped in the stack already forwards that header, so nothing needs configuring for the common case.

If you supply your own Caddyfile, or if you hit a 401, you have to add the header forwarding yourself in the reverse_proxy block. The readme gives the exact block, with the upstream host header, the Authorization header pass-through and flush_interval set to -1 so that streaming responses are not buffered.

yaml
handle_path /api/* {
	reverse_proxy your-opencode-serve:4096 {
		header_up Host {upstream_hostport}
		header_up Authorization {http.request.header.Authorization}
		flush_interval -1
	}
}

The flush_interval -1 is the detail that matters most for an agent interface. Without it, the proxy buffers the server-sent event stream and the client sees nothing until a response completes, which for a streaming agent means no visible output for the whole turn.

The custom configuration is mounted with a compose volume, and the file is read-only so the container cannot rewrite it.

The host Docker override, and the deliberate default of not having it

One piece of the deployment deserves attention because it is opt-in, and the reasoning behind that is stated.

By default the deployment does not give the backend container access to the host Docker daemon and does not mount the host root directory. The readme gives the reason as being closer to the older behaviour, so that existing users upgrading do not suddenly gain extra permissions.

If you need to build Docker images inside the OpenCode backend container, run docker compose in it, or reach host paths, you download a second override file and add it to the compose invocation.

bash
curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/docker-compose.host.yml
docker compose -f docker-compose.yml -f docker-compose.host.yml up -d

The override adds three things to the backend service: privileged: true, the host Docker socket mounted at /var/run/docker.sock, and the host root at /host. The readme says to enable it only when those capabilities are clearly needed, and that a public deployment must set OPENCODE_SERVER_PASSWORD.

Those three together are the strongest privilege escalation in the whole stack. A container with privileged mode and the host Docker socket is effectively the host, and the readme is right to keep it behind a separate file rather than in the default compose. The same warning applies to the built-from-source path, which has its own host override.

bash
git clone https://github.com/lehhair/OpenCodeUI.git
cd OpenCodeUI

# 复制并编辑环境变量,至少填写一个 LLM API Key
cp .env.example .env

# 构建并启动全部服务
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build

Building from source is only needed when you want to change the image contents, debug a Dockerfile, or avoid the prebuilt images, and the build override composes with the host override in the same way.

Persistence is deliberately simple. The backend keeps a single core volume, opencode-home, mounted at /root, and the entry script validates and installs opencode and mise at startup and writes the npm and pip mirror configuration on first launch. Language runtimes are not preinstalled and are installed on demand with mise into the persisted /root. The gateway keeps a separate volume, opencode-router-data, for the dynamic routing state.

So the OpenCode configuration and session cache, the userland caches for npm, cargo and pip, and the Node and Python versions installed through mise all survive a container rebuild, without the volume sprawl of many small mounts.

The reverse proxy requirements, and where this project stops

The readme is explicit about what a public deployment needs, and it is a short list that will save time.

Docker listens on 127.0.0.1 by default, so a public deployment needs a reverse proxy in front. The readme gives an Nginx configuration, and two of its directives are load-bearing rather than stylistic. proxy_set_header Connection '' together with proxy_http_version 1.1 and proxy_buffering off is required for SSE, and the comment marks it as mandatory. proxy_cache off goes with it. The WebSocket side needs the standard Upgrade header handling.

For the preview port, the solution is a separate domain. The environment file has a PREVIEW_DOMAIN entry with the example preview.example.com, and the note says Nginx forwards that domain to PREVIEW_PORT. The reason is a path rather than a port problem: the preview links are generated as /p/{token}/ paths, and a separate domain keeps the asset paths on the preview origin working instead of being relative to the main application path.

The dynamic routing has two more settings worth knowing. PUBLIC_BASE_URL is empty by default and is presumably what you set so that generated links point at the domain the browser actually uses, and PREVIEW_DOMAIN handles the preview origin separately. The router also has an authentication side: the gateway receives ROUTER_USERNAME and ROUTER_PASSWORD from the same OPENCODE_SERVER_USERNAME and OPENCODE_SERVER_PASSWORD values, so the preview port is not an open door when the main one is protected.

As for where this project stops, the answer is that it is a client, and the backend is OpenCode's own. Everything here is presentation, transport and deployment. The agent behaviour, the model providers and the session logic live elsewhere, and the dependency on @opencode/client at version 2.0.21 is the concrete link to that API, versioned separately from this frontend's own 0.7.x line.

The repository also carries three documents about that API version boundary, which is where the project's real complexity sits: docs-v1-to-v2-api-map.md, docs-v2-migration-contract.md and docs-v2-types.md, along with docs-v2-sdk-coverage.md and a docs-ui-api-usage.md. A frontend that tracks a moving server API needs to record which endpoints it has covered and which it has not, and that is what these files are for.

The project is also upfront about its own construction. The readme states that it was completed entirely through AI-assisted programming, from the first line of code to the final release, with all features developed through conversation with an AI. It carries a disclaimer saying the project is for study and exchange only, that it is at an early stage, and that bugs and instability are possible. The GPL-3.0 licence and the 0.7.x version line fit that description.

Editorial conclusion

OpenCodeUI fits a developer who already runs opencode serve and wants a browser or desktop interface over it, or a team that wants to expose the agent through a controlled gateway with authentication. It does not fit someone who wants the agent itself, since the server is OpenCode's own, and it does not fit a public deployment without reading the security section, because the compose file binds the gateway to 127.0.0.1 and the readme says a public deployment must set OPENCODE_SERVER_PASSWORD. Before deploying, decide between the prebuilt images and the build override, and note that the host Docker capability is deliberately off by default.

Frequently asked questions

How do I run OpenCodeUI against a local OpenCode server?

Start the backend with opencode serve --cors "https://lehhair.github.io" and open the hosted frontend at https://lehhair.github.io/OpenCodeUI/. The cors option is what allows the browser to reach a backend on a different origin.

What do the OpenCodeUI docker compose services do?

The gateway is Caddy on port 6658 as the single entry point and on 6659 for previews, the frontend serves static files on port 3000 internally, the backend runs opencode serve on 4096, and the router on 7070 is built into the gateway and scans ports 3000 to 9999 to generate preview links.

Does the OpenCodeUI deployment give the container access to the host Docker daemon?

Not by default. The readme states that the default deployment does not expose the host Docker daemon or mount the host root, so existing users do not gain extra permissions on upgrade. Enabling docker-compose.host.yml adds privileged mode, the Docker socket and a /host mount.

How do I set up a reverse proxy for OpenCodeUI in public?

Docker listens on 127.0.0.1, so a public deployment needs a proxy in front of port 6658. The readme gives an Nginx configuration where proxy_set_header Connection '' with proxy_http_version 1.1 and proxy_buffering off is required for SSE, and the preview port is served on a separate domain via PREVIEW_DOMAIN.

Official sources

  1. Issues
  2. lehhair/OpenCodeUI on GitHub
  3. License: GPL-3.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lehhair-opencodeui.svg)](https://hysenlabs.com/projects/lehhair-opencodeui)

Was this useful?