Skip to content

Commit ba03134

Browse files
author
opencode-bot
committed
feat(docker): add Docker server mode, auth sync, Dockerfile updates, local build flags, script/docker-build, and publish-docker workflow
1 parent f993541 commit ba03134

6 files changed

Lines changed: 411 additions & 12 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Publish Docker Image
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch: {}
7+
8+
jobs:
9+
docker:
10+
name: Build and push to Docker Hub
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Set up QEMU
19+
uses: docker/setup-qemu-action@v3
20+
21+
- name: Set up Docker Buildx
22+
uses: docker/setup-buildx-action@v3
23+
24+
- name: Log in to Docker Hub
25+
if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }}
26+
uses: docker/login-action@v3
27+
with:
28+
username: ${{ secrets.DOCKERHUB_USERNAME }}
29+
password: ${{ secrets.DOCKERHUB_TOKEN }}
30+
31+
- name: Compute tags
32+
id: meta
33+
run: |
34+
tag="${GITHUB_REF_NAME#v}"
35+
sha=$(echo "$GITHUB_SHA" | cut -c1-7)
36+
if [ -z "$tag" ]; then tag="$sha"; fi
37+
echo "version=$tag" >> $GITHUB_OUTPUT
38+
39+
- name: Build and push
40+
uses: docker/build-push-action@v6
41+
with:
42+
context: .
43+
file: ./Dockerfile
44+
push: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }}
45+
platforms: linux/amd64,linux/arm64
46+
tags: |
47+
opencodeai/opencode:server
48+
opencodeai/opencode:server-${{ steps.meta.outputs.version }}
49+
cache-from: type=gha
50+
cache-to: type=gha,mode=max

Dockerfile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
FROM oven/bun:latest AS base
2+
3+
# Core tools required by server features (downloads, unzip, etc.) and gopls support
4+
RUN apt-get update \
5+
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
6+
ca-certificates curl unzip tar git golang nodejs npm \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
# Create non-root user
10+
RUN groupadd -g 1001 opencode && \
11+
useradd -r -u 1001 -g opencode -m opencode
12+
13+
# Set working directory for the app layer
14+
WORKDIR /app
15+
16+
# Copy only the opencode package files for a minimal build
17+
COPY packages/opencode/package.json ./
18+
RUN sed -i 's/"@opencode-ai\/sdk": "workspace:\*"/"@opencode-ai\/sdk": "latest"/g' package.json && \
19+
sed -i 's/"@opencode-ai\/plugin": "workspace:\*"/"@opencode-ai\/plugin": "latest"/g' package.json
20+
21+
# Install dependencies (production preferred, fall back to full)
22+
RUN bun install --production || bun install
23+
24+
# Copy source code
25+
COPY packages/opencode/src ./src
26+
COPY packages/opencode/tsconfig.json ./
27+
28+
# Expose port
29+
EXPOSE 8080
30+
31+
# Switch to non-root user
32+
USER opencode
33+
34+
# Start the server
35+
CMD ["bun", "run", "/app/src/index.ts", "serve", "--hostname", "0.0.0.0", "--port", "8080"]

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,46 @@ $ bun install
8686
$ bun dev
8787
```
8888

89+
#### Docker Server Mode
90+
91+
You can optionally run the opencode server in a Docker container with the current directory mounted for isolation. When started with `--docker`, opencode securely syncs only its own provider credentials (from `auth.json`) into the container; no other local credentials or home directories are mounted.
92+
93+
```bash
94+
# TUI with server in Docker (mounts $PWD to /workspace)
95+
# Uses Docker Hub image by default: opencodeai/opencode:server
96+
opencode --docker
97+
98+
# Headless server in Docker
99+
opencode serve --docker --port 8080 --docker-image opencode:latest
100+
```
101+
102+
This maps a host port to the container’s server and mounts your current directory at `/workspace`.
103+
104+
Build from a local Dockerfile (handy for dev):
105+
106+
```bash
107+
# Build with the repo Dockerfile, then run
108+
opencode --docker --docker-build --dockerfile ./Dockerfile
109+
110+
# Or headless
111+
opencode serve --docker --docker-build --dockerfile ./Dockerfile --port 8080
112+
```
113+
114+
The default Docker image is `opencodeai/opencode:server`. The provided Dockerfile uses the `oven/bun` base image, adds essential tools (`curl`, `unzip`, `tar`, `git`, `nodejs`, `npm`) and Go (for optional `gopls`), installs the opencode server, and exposes port `8080`.
115+
116+
If you prefer to build the image manually:
117+
118+
```bash
119+
docker build -t opencode:latest .
120+
```
121+
122+
Or use the helper:
123+
124+
```bash
125+
# Tags both opencodeai/opencode:server and opencode:local
126+
./script/docker-build [Dockerfile] [context]
127+
```
128+
89129
#### Development Notes
90130

91131
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
1+
import { Provider } from "../../provider/provider"
12
import { Server } from "../../server/server"
3+
import { bootstrap } from "../bootstrap"
24
import { cmd } from "./cmd"
5+
import { Auth } from "../../auth"
6+
import path from "path"
7+
import { ModelsDev } from "../../provider/models"
38

49
export const ServeCommand = cmd({
510
command: "serve",
611
builder: (yargs) =>
712
yargs
13+
.option("docker", {
14+
type: "boolean",
15+
describe: "run server in docker with current dir mounted",
16+
})
17+
.option("docker-image", {
18+
type: "string",
19+
describe: "docker image for server",
20+
default: "opencodeai/opencode:server",
21+
alias: ["dockerImage"],
22+
})
23+
.option("dockerfile", {
24+
type: "string",
25+
describe: "path to a local Dockerfile to build before running",
26+
})
27+
.option("docker-context", {
28+
type: "string",
29+
describe: "docker build context directory (defaults to Dockerfile's dir)",
30+
alias: ["dockerContext"],
31+
})
32+
.option("docker-build", {
33+
type: "boolean",
34+
describe: "force build the docker image before running",
35+
alias: ["dockerBuild"],
36+
})
837
.option("port", {
938
alias: ["p"],
1039
type: "number",
@@ -19,14 +48,111 @@ export const ServeCommand = cmd({
1948
}),
2049
describe: "starts a headless opencode server",
2150
handler: async (args) => {
22-
const hostname = args.hostname
23-
const port = args.port
24-
const server = Server.listen({
25-
port,
26-
hostname,
51+
const cwd = process.cwd()
52+
await bootstrap(cwd, async () => {
53+
const providers = await Provider.list()
54+
if (Object.keys(providers).length === 0) {
55+
return "needs_provider"
56+
}
57+
58+
const srv = await (async () => {
59+
if (!args.docker) return Server.listen({ port: args.port, hostname: args.hostname })
60+
const docker = Bun.which("docker")
61+
if (!docker) return Server.listen({ port: args.port, hostname: args.hostname })
62+
const df = (args as { dockerfile?: string }).dockerfile
63+
const needBuild = !!df || (args as { dockerBuild?: boolean }).dockerBuild === true
64+
const img = await (async () => {
65+
const defaultImg = "opencodeai/opencode:server"
66+
if (!needBuild) return (args as { dockerImage?: string }).dockerImage ?? defaultImg
67+
const f = df ?? "Dockerfile"
68+
const ctx = (args as { dockerContext?: string }).dockerContext ?? path.dirname(path.resolve(f))
69+
const base = (args as { dockerImage?: string }).dockerImage ?? defaultImg
70+
const tag = base === defaultImg ? "opencode:local" : base
71+
const b = Bun.spawn({ cmd: [docker, "build", "-t", tag, "-f", f, ctx], stdout: "inherit", stderr: "inherit" })
72+
const code = await b.exited
73+
if (code !== 0) return base
74+
return tag
75+
})()
76+
const alloc = () => {
77+
const s = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("ok") })
78+
const p = s.port
79+
s.stop()
80+
return p
81+
}
82+
const port = args.port && args.port > 0 ? args.port : alloc()
83+
const host = args.hostname ?? "127.0.0.1"
84+
const cport = 8080
85+
const vol = process.cwd() + ":/workspace"
86+
const db = await ModelsDev.get()
87+
const envlist: string[] = []
88+
for (const p of Object.values(db)) {
89+
for (const k of p.env) {
90+
const v = process.env[k]
91+
if (v) envlist.push(`${k}=${v}`)
92+
}
93+
}
94+
const cmd = [
95+
docker,
96+
"run",
97+
"--rm",
98+
"-d",
99+
"-p",
100+
`${port}:${cport}`,
101+
"-v",
102+
vol,
103+
"-w",
104+
"/workspace",
105+
...envlist.flatMap((e) => ["-e", e]),
106+
img,
107+
"bun",
108+
"run",
109+
"/app/src/index.ts",
110+
"serve",
111+
"--hostname",
112+
"0.0.0.0",
113+
"--port",
114+
String(cport),
115+
]
116+
const p = Bun.spawn({ cmd, stdout: "pipe", stderr: "pipe" })
117+
const code = await p.exited
118+
const id = await new Response(p.stdout).text().then((x) => x.trim())
119+
if (code !== 0 || !id) return Server.listen({ port: args.port, hostname: args.hostname })
120+
const url = new URL("http://" + host + ":" + String(port))
121+
const until = Date.now() + 20_000
122+
while (Date.now() < until) {
123+
const ok = await fetch(new URL("/doc", url)).then((r) => r.ok).catch(() => false)
124+
if (ok) break
125+
await Bun.sleep(200)
126+
}
127+
return {
128+
hostname: host,
129+
port,
130+
url,
131+
stop: async () => {
132+
const stop = Bun.spawn({ cmd: [docker, "stop", id], stdout: "ignore", stderr: "inherit" })
133+
await stop.exited
134+
},
135+
}
136+
})()
137+
138+
if (args.docker) {
139+
const auth = await Auth.all()
140+
await Promise.all(
141+
Object.entries(auth).map(([id, info]) =>
142+
fetch(new URL("/auth/" + encodeURIComponent(id), srv.url), {
143+
method: "PUT",
144+
headers: { "content-type": "application/json" },
145+
body: JSON.stringify(info),
146+
}).catch(() => {}),
147+
),
148+
)
149+
}
150+
151+
console.log(`opencode server listening on http://${srv.hostname}:${srv.port}`)
152+
153+
await new Promise(() => {})
154+
155+
srv.stop()
27156
})
28-
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
29-
await new Promise(() => {})
30-
server.stop()
31157
},
32158
})

0 commit comments

Comments
 (0)