Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
# ============================================
# Backend Server Configuration
# ============================================
PORT=5001
PORT=5003
HOST=0.0.0.0
# Comma-separated browser origins allowed to call the API
# Leave empty for same-origin deployments. Set an explicit comma-separated allowlist
# only when a browser origin must call the API cross-origin.
CORS_ORIGIN=http://localhost:5173
# Required control-plane bearer token for all sensitive /api/* routes
AUTH_TOKEN=change-me
Expand Down Expand Up @@ -52,11 +53,11 @@ DEBUG=false
# ============================================
# Frontend Configuration (Vite)
# These are optional - frontend uses defaults if not set.
# If VITE_API_AUTH_TOKEN is omitted, the browser will prompt once and cache it in localStorage.
# If VITE_API_AUTH_TOKEN is omitted, the browser will prompt once, cache it in localStorage, and re-prompt if the cached token is rejected.
# ============================================
# VITE_API_URL=http://localhost:5001
# VITE_API_URL=http://localhost:5003
# VITE_API_AUTH_TOKEN=change-me
# VITE_SERVER_PORT=5001
# VITE_SERVER_PORT=5003
# VITE_OPENCODE_PORT=5551
# VITE_MAX_FILE_SIZE_MB=50
# VITE_MAX_UPLOAD_SIZE_MB=50
43 changes: 43 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Validate

on:
pull_request:
push:
branches:
- main

concurrency:
group: validate-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI pins Bun to 1.2.23 while Docker defaults/tooling pins 1.2.21 (Dockerfile/docker-compose/entrypoint). If the goal is runtime parity, align these versions (or explicitly document why CI uses a different patch) to avoid hard-to-reproduce differences between local/CI/container behavior.

Suggested change
bun-version: 1.2.23
bun-version: 1.2.21

Copilot uses AI. Check for mistakes.

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Validate workspace
run: pnpm validate
20 changes: 10 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

## Commands

- `npm run dev` - Start both backend (5001) and frontend (5173)
- `npm run dev:backend` - Backend only: `bun --watch backend/src/index.ts`
- `npm run dev:frontend` - Frontend only: `cd frontend && vite`
- `npm run build` - Build both backend and frontend
- `npm run test` - Run backend tests: `cd backend && bun test`
- `cd backend && bun test <filename>` - Run single test file
- `cd backend && vitest --ui` - Test UI with coverage
- `cd backend && vitest --coverage` - Coverage report
- `cd frontend && npm run lint` - Frontend linting
- `pnpm dev` - Start both backend (5003) and frontend (5173)
- `pnpm dev:backend` - Backend only: `bun --watch backend/src/index.ts`
- `pnpm dev:frontend` - Frontend only: `pnpm --filter frontend dev`
- `pnpm build` - Build both backend and frontend
- `pnpm test` - Run backend tests once in CI-safe mode
- `pnpm --filter backend test:watch` - Run backend tests in watch mode
- `pnpm --filter backend test:ui` - Open the Vitest UI
- `pnpm test:coverage` - Generate backend coverage for all backend source files
- `pnpm lint` - Frontend linting

## Code Style

Expand Down Expand Up @@ -38,4 +38,4 @@
- DRY principles, follow existing patterns
- ./opencode-src/ is reference only, never commit
- Use shared types from workspace package
- OpenCode server runs on port 5551, backend API on port 5001
- OpenCode server runs on port 5551, backend API on port 5003
108 changes: 59 additions & 49 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
FROM node:20 AS base
FROM node:20-bookworm-slim AS base

RUN apt-get update && apt-get install -y \
git \
curl \
lsof \
ripgrep \
ARG PNPM_VERSION=9.15.0
ARG BUN_VERSION=1.2.21

ENV BUN_INSTALL=/root/.bun
ENV BUN_VERSION=${BUN_VERSION}
ENV PATH=/root/.bun/bin:/root/.local/bin:${PATH}

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
grep \
gawk \
sed \
findutils \
coreutils \
procps \
curl \
file \
findutils \
gawk \
git \
grep \
jq \
less \
lsof \
procps \
ripgrep \
sed \
tree \
file \
&& rm -rf /var/lib/apt/lists/*

RUN corepack enable && corepack prepare pnpm@9.15.0 --activate

RUN curl -fsSL https://bun.sh/install | bash && \
ln -s $HOME/.bun/bin/bun /usr/local/bin/bun
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
RUN curl -fsSL https://bun.com/install | bash -s -- bun-v${BUN_VERSION}

WORKDIR /app

FROM base AS metadata
FROM base AS deps

COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY shared/package.json ./shared/
COPY backend/package.json ./backend/
COPY frontend/package.json ./frontend/

RUN pnpm install --frozen-lockfile

FROM base AS builder

ARG BUILD_TIME=unknown
ARG GIT_SHA=unknown
ARG GIT_SHORT_SHA=unknown
ARG GIT_DIRTY=false
ARG GIT_REF=local
ARG SOURCE_REPOSITORY=https://github.com/diamondplated/opencode-webui.git
ARG IMAGE_NAME=ghcr.io/diamondplated/opencode-webui
ARG PNPM_VERSION=9.15.0
ARG BUN_VERSION=1.2.21
ARG OPENCODE_VERSION=1.2.27

COPY --from=deps /app ./
COPY . .

RUN node <<'NODE'
Expand All @@ -49,62 +70,51 @@ const buildInfo = {
sha,
shortSha,
dirty,
ref: process.env.GIT_REF || 'unknown',
},
source: {
repository: process.env.SOURCE_REPOSITORY || 'unknown',
image: process.env.IMAGE_NAME || 'opencode-webui',
},
toolchain: {
node: process.version,
bun: process.env.BUN_VERSION || 'unknown',
pnpm: process.env.PNPM_VERSION || 'unknown',
opencode: process.env.OPENCODE_VERSION || 'unknown',
},
}

fs.writeFileSync('/tmp/build-info.json', JSON.stringify(buildInfo, null, 2))
fs.writeFileSync('/app/build-info.json', JSON.stringify(buildInfo, null, 2))
NODE

FROM base AS deps

COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY shared/package.json ./shared/
COPY backend/package.json ./backend/
COPY frontend/package.json ./frontend/

RUN pnpm install --frozen-lockfile

FROM base AS builder

COPY --from=deps /app ./
COPY shared ./shared
COPY backend ./backend
COPY frontend/src ./frontend/src
COPY frontend/public ./frontend/public
COPY frontend/index.html frontend/vite.config.ts frontend/tsconfig*.json frontend/components.json frontend/eslint.config.js ./frontend/

RUN pnpm --filter frontend build
RUN pnpm run build

FROM base AS runner

RUN curl -fsSL https://opencode.ai/install | bash && \
ln -s $HOME/.opencode/bin/opencode /usr/local/bin/opencode
ARG OPENCODE_VERSION=1.2.27

ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=5003
ENV OPENCODE_SERVER_PORT=5551
ENV DATABASE_PATH=/app/data/opencode.db
ENV WORKSPACE_PATH=/workspace
ENV OPENCODE_VERSION=${OPENCODE_VERSION}

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/shared ./shared
COPY --from=builder /app/backend ./backend
RUN npm install --global opencode-ai@${OPENCODE_VERSION}

COPY --from=builder /app/backend/dist ./backend/dist
COPY --from=builder /app/frontend/dist ./frontend/dist
COPY --from=metadata /tmp/build-info.json ./build-info.json
COPY --from=builder /app/build-info.json ./build-info.json
COPY package.json pnpm-workspace.yaml ./

RUN mkdir -p /app/backend/node_modules/@opencode-webui && \
ln -s /app/shared /app/backend/node_modules/@opencode-webui/shared

COPY --chmod=755 scripts/docker-entrypoint.sh /docker-entrypoint.sh

RUN mkdir -p /workspace /app/data

EXPOSE 5003

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5003/api/health || exit 1
CMD curl -fsS http://localhost:5003/api/health/ready || exit 1

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["bun", "backend/src/index.ts"]
CMD ["bun", "backend/dist/index.js"]
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,33 +85,33 @@ git clone https://github.com/yourusername/opencode-webui.git
cd opencode-webui

# Start with Docker Compose (single container)
docker-compose up -d
docker compose up -d

# Access the application at http://localhost:5001
# Access the application at http://127.0.0.1:5003
```

The Docker setup automatically:
- Installs OpenCode if not present
- Installs pinned Bun/OpenCode toolchains during image build
- Builds and serves frontend from backend
- Sets up persistent volumes for workspace and database
- Includes health checks and auto-restart
- Publishes the API to loopback by default and uses readiness-based health checks

**Docker Commands:**
```bash
# Start container
docker-compose up -d
docker compose up -d

# Stop and remove container
docker-compose down
docker compose down

# Rebuild image
docker-compose build
docker compose build

# View logs
docker-compose logs -f
docker compose logs -f

# Restart container
docker-compose restart
docker compose restart

# Access container shell
docker exec -it opencode-web sh
Expand All @@ -124,8 +124,8 @@ docker exec -it opencode-web sh
git clone https://github.com/yourusername/opencode-webui.git
cd opencode-webui

# Install dependencies (uses Bun workspaces)
bun install
# Install dependencies (uses pnpm workspaces)
pnpm install --frozen-lockfile

# Copy environment configuration
cp .env.example .env
Expand All @@ -134,15 +134,26 @@ cp .env.example .env
export AUTH_TOKEN=replace-this-with-a-long-random-token

# Start development servers (backend + frontend)
npm run dev
pnpm dev
```

Useful validation commands:

```bash
pnpm lint
pnpm test
pnpm build
pnpm validate
```

### Phase 0 Security Gate

Sensitive control-plane routes now require a shared bearer token, while health endpoints stay public for diagnostics.

- Set `AUTH_TOKEN` to a long random shared secret.
- Set `CORS_ORIGIN` to an explicit comma-separated allowlist of browser origins.
- Browser clients can either set `VITE_API_AUTH_TOKEN` ahead of time or enter the token once when prompted; the browser caches it in localStorage.
- API clients and automation can send `Authorization: Bearer <AUTH_TOKEN>`.

- Leave `CORS_ORIGIN` empty for same-origin production deployments, or set it to an explicit comma-separated allowlist when a browser must call the API cross-origin.
- Direct runtime defaults bind the backend to `127.0.0.1` in production. Development still defaults to `0.0.0.0` for easier local testing.
- The container publishes `127.0.0.1:5003` by default. Override `HOST_BIND_ADDRESS=0.0.0.0` only when you intentionally want LAN exposure.
- Browser clients can either set `VITE_API_AUTH_TOKEN` ahead of time or enter the token once when prompted; the browser caches it in localStorage and re-prompts automatically if a cached token is rejected.
- API clients and automation can send `Authorization: Bearer <AUTH_TOKEN>`. Headerless browser transports such as `EventSource` and raw file/media URLs automatically fall back to `?access_token=...`.
- `/api/health/live` is liveness-only, `/api/health/ready` is the deploy/readiness gate, and `/api/health/build` now returns build provenance including git ref plus pinned toolchain versions.
5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"build": "bun build src/index.ts --outdir=dist --target=bun",
"test": "vitest",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch"
},
"dependencies": {
Expand All @@ -22,6 +22,7 @@
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"vitest": "^3.2.4"
},
Expand Down
10 changes: 6 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,11 @@ async function syncDefaultConfigToDisk(): Promise<void> {

if (defaultConfig) {
const configPath = getOpenCodeConfigFilePath()
const configContent = JSON.stringify(defaultConfig.content, null, 2)
await writeFileContent(configPath, configContent)
logger.info(`Synced default config '${defaultConfig.name}' to: ${configPath}`)
const configContent = settingsService.getOpenCodeConfigContent(defaultConfig.name)
if (configContent) {
await writeFileContent(configPath, configContent)
logger.info(`Synced default config '${defaultConfig.name}' to: ${configPath}`)
}
} else {
logger.info('No default OpenCode config found in database')
}
Expand Down Expand Up @@ -155,7 +157,7 @@ app.route('/api/tts', createTTSRoutes(db))

app.all('/api/opencode/*', async (c) => {
const request = c.req.raw
return proxyRequest(request)
return proxyRequest(request, db)
})

const isProduction = ENV.SERVER.NODE_ENV === 'production'
Expand Down
Loading
Loading