Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ opencode-data/
*.swp
*.swo
*~

.sisyphus/
# OS files
.DS_Store
Thumbs.db
Expand Down
33 changes: 28 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
# 1. Switch to a Debian-based image for glibc compatibility
FROM node:24-bookworm-slim
FROM oven/bun:1 AS bun_runtime

FROM golang:1.24-bookworm AS go_runtime

FROM python:3.14-slim-bookworm

# 2. Install necessary system dependencies (some opencode tools need these)
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
bash \
gosu \
git \
gnupg \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*

# 3. Install opencode-ai globally
RUN npm i -g opencode-ai
COPY --from=go_runtime /usr/local/go /usr/local/go
COPY --from=bun_runtime /usr/local/bin/bun /usr/local/bin/bun
COPY --from=bun_runtime /usr/local/bin/bunx /usr/local/bin/bunx

ENV PATH="/usr/local/go/bin:${PATH}"

RUN npm i -g \
opencode-ai \
bash-language-server \
pyright \
vscode-langservers-extracted \
yaml-language-server

RUN GOBIN=/usr/local/bin go install golang.org/x/tools/gopls@latest

WORKDIR /app

Expand Down
46 changes: 35 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,24 @@ The image is pre-configured with an entrypoint that maps environment variables t

## Persistent Storage

The container supports persistent storage for configuration, credentials, and custom agents/commands.
The container persists OpenCode data through a single host mount: `./opencode-data:/data`. Inside the container, `/data/config` is the canonical OpenCode global config root, and `/root/.config/opencode` is symlinked to that location at startup.

### Volume Mount

Mount a host directory to `/root/.local/share/opencode` to persist data:
Mount a host directory to `/data` to persist config, credentials, and custom OpenCode resources:

```bash
docker run -d \
-p 4096:4096 \
-v ./opencode-data:/root/.local/share/opencode \
-v ./opencode-data:/data \
-v .:/workspace \
-e OPENCODE_SERVER_PASSWORD=superSecret \
-e OPENCODE_CONFIG_DIR=/data/config \
-e OPENCODE_CONFIG=/data/config/opencode.json \
ghcr.io/felixclements/opencode-server-docker:latest
```

Or with Docker Compose, see the `docker-compose.yml` file.
Or with Docker Compose, see `docker-compose.yml`.

### Project Directory

Expand All @@ -46,21 +48,23 @@ The `opencode-data/` directory contains user-specific configuration and should n

The mounted volume should contain:

```
```text
opencode-data/
├── auth.json # API credentials (created via /connect command)
├── config/
│ └── opencode.json # Global configuration file
└── .opencode/ # Custom agents, commands, modes, plugins, skills, tools, themes
├── auth.json
└── config/
├── opencode.json
├── agents/
├── commands/
├── modes/
├── plugins/
├── skills/
│ └── <name>/SKILL.md
├── tools/
└── themes/
```

Legacy `opencode-data/.opencode/*` content is treated as a compatibility source only. On startup, the entrypoint copies legacy directories into the canonical `opencode-data/config/*` location when the canonical destination does not already exist.

### Config Precedence

OpenCode loads config in this order (later sources override earlier ones):
Expand All @@ -69,7 +73,7 @@ OpenCode loads config in this order (later sources override earlier ones):
2. Global config (`~/.config/opencode/opencode.json`) - user preferences
3. Custom config (`OPENCODE_CONFIG` env var) - custom overrides
4. Project config (`opencode.json` in project) - project-specific settings
5. `.opencode/` directories - agents, commands, plugins, etc.
5. Global resource directories under `~/.config/opencode/` - agents, commands, plugins, skills, tools, themes, and related resources
6. Inline config (`OPENCODE_CONFIG_CONTENT` env var) - runtime overrides

### Environment Variables
Expand Down Expand Up @@ -104,9 +108,11 @@ OpenCode loads config in this order (later sources override earlier ones):
```bash
docker run -d \
-p 4096:4096 \
-v ./opencode-data:/root/.local/share/opencode \
-v ./opencode-data:/data \
-v .:/workspace \
-e OPENCODE_SERVER_PASSWORD=superSecret \
-e OPENCODE_CONFIG_DIR=/data/config \
-e OPENCODE_CONFIG=/data/config/opencode.json \
-e CORS=http://localhost:5173,https://app.example.com \
ghcr.io/felixclements/opencode-server-docker:latest
```
Expand All @@ -121,12 +127,30 @@ docker compose up -d

Edit `docker-compose.yml` to customize environment variables and mount volumes for persistent storage.

## Baked-In Tooling

The image ships with Python, Go, and the default LSP bundle already installed. Fresh containers have `python3`, `pip3`, `go`, `pyright-langserver`, `gopls`, `bash-language-server`, `yaml-language-server`, and `vscode-json-language-server` on `PATH` without any first-run install step.

## Building

```bash
docker build -t ghcr.io/felixclements/opencode-server-docker:latest .
```

## Smoke Checks

```bash
docker build -t opencode-local:test .
docker run --rm opencode-local:test bash -lc 'python3 --version && go version && command -v pyright-langserver && command -v gopls && command -v bash-language-server && command -v yaml-language-server && command -v vscode-json-language-server'
rm -rf ./opencode-data && mkdir -p ./opencode-data
docker compose up -d
docker exec opencode-server bash -lc 'test -d /data/config && test -d /data/config/skills && test -L /root/.config/opencode && [ "$(readlink -f /root/.config/opencode)" = "/data/config" ]'
rm -rf ./opencode-data && mkdir -p ./opencode-data/.opencode/skills/demo && printf '# Demo\n' > ./opencode-data/.opencode/skills/demo/SKILL.md
docker run --rm -v "$PWD/opencode-data:/data" opencode-local:test bash -lc 'test -f /data/config/skills/demo/SKILL.md'
docker run --rm -e PUID=1000 -e PGID=1000 -v "$PWD/opencode-data:/data" opencode-local:test bash -lc 'opencode --help >/dev/null && test -L /root/.config/opencode'
docker run --rm -e PUID=1000 -e PGID=1000 opencode-local:test bash -lc 'command -v pyright-langserver && command -v gopls && command -v bash-language-server && command -v yaml-language-server && command -v vscode-json-language-server'
```

## .gitignore

The repository includes a `.gitignore` file that excludes:
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ services:
- OPENCODE_CONFIG=/data/config/opencode.json

volumes:
# Persistent storage for config, auth, and .opencode directories
- ./opencode-data:/data

# Mount current project directory for OpenCode to access and modify
Expand Down
151 changes: 89 additions & 62 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,56 @@ PGID=${PGID:-0}
# Data directory configuration
DATA_DIR="/data"
CONFIG_DIR="$DATA_DIR/config"
OPENCODE_DIR="$DATA_DIR/.opencode"
LEGACY_OPENCODE_DIR="$DATA_DIR/.opencode"
CANONICAL_SUBDIRS=(agents commands modes plugins skills tools themes)

# Export environment variables for the running application
export DATA_DIR
export CONFIG_DIR
export OPENCODE_DIR
export LEGACY_OPENCODE_DIR
export HOME="/root"
export OPENCODE_CONFIG_DIR="${OPENCODE_CONFIG_DIR:-$CONFIG_DIR}"
export OPENCODE_CONFIG="${OPENCODE_CONFIG:-$CONFIG_DIR/opencode.json}"

migrate_legacy_directories() {
local legacy_dir
local canonical_dir
local canonical_contents

for dir in "${CANONICAL_SUBDIRS[@]}"; do
legacy_dir="$LEGACY_OPENCODE_DIR/$dir"
canonical_dir="$CONFIG_DIR/$dir"
canonical_contents=()

if [ -d "$canonical_dir" ]; then
shopt -s nullglob dotglob
canonical_contents=("$canonical_dir"/*)
shopt -u nullglob dotglob
fi

if [ -d "$legacy_dir" ] && [ ${#canonical_contents[@]} -eq 0 ]; then
mkdir -p "$canonical_dir"
cp -a "$legacy_dir/." "$canonical_dir/"
fi
done
}

# Create the complete directory structure
create_directories() {
echo "Creating OpenCode data directories..."

# Main directories
mkdir -p "$CONFIG_DIR"
mkdir -p "$OPENCODE_DIR"

# .opencode subdirectories for custom agents, commands, modes, plugins, skills, tools, themes
mkdir -p "$OPENCODE_DIR/agents"
mkdir -p "$OPENCODE_DIR/commands"
mkdir -p "$OPENCODE_DIR/modes"
mkdir -p "$OPENCODE_DIR/plugins"
mkdir -p "$OPENCODE_DIR/skills"
mkdir -p "$OPENCODE_DIR/tools"
mkdir -p "$OPENCODE_DIR/themes"

# Symlink for config directory to enable single-volume persistence

mkdir -p "$DATA_DIR" "$CONFIG_DIR"

migrate_legacy_directories

for dir in "${CANONICAL_SUBDIRS[@]}"; do
mkdir -p "$CONFIG_DIR/$dir"
done

mkdir -p /root/.config
if [ ! -L /root/.config/opencode ]; then
ln -sf "$CONFIG_DIR" /root/.config/opencode
fi

# Symlink for opencode data directory (auth.json, logs, etc.)
mkdir -p /home/node/.local/share
if [ ! -L /home/node/.local/share/opencode ]; then
ln -sf "$OPENCODE_DIR" /home/node/.local/share/opencode
fi

chmod 755 /root /root/.config
rm -rf /root/.config/opencode
ln -s "$CONFIG_DIR" /root/.config/opencode

echo "Directory structure created successfully."
}

Expand All @@ -53,7 +67,6 @@ set_ownership() {
echo "Setting ownership to PUID=$PUID, PGID=$PGID..."
chown -R "$PUID:$PGID" "$DATA_DIR"
chown -R "$PUID:$PGID" /root/.config
chown -R "$PUID:$PGID" /home/node/.local
fi
}

Expand All @@ -74,71 +87,85 @@ create_user() {

# Build the opencode serve command
build_command() {
# Start with the command "serve"
local cmd="serve"
local args=(serve)

# 1. Handle Port
if [ -n "$PORT" ]; then
cmd="$cmd --port $PORT"
args+=(--port "$PORT")
fi

# 2. Handle Hostname (default to 0.0.0.0 in Docker)
if [ -n "$HOSTNAME_OVERRIDE" ]; then
cmd="$cmd --hostname $HOSTNAME_OVERRIDE"
args+=(--hostname "$HOSTNAME_OVERRIDE")
else
cmd="$cmd --hostname 0.0.0.0"
args+=(--hostname 0.0.0.0)
fi

# 3. Handle mDNS
if [ "$MDNS" = "true" ]; then
cmd="$cmd --mdns"
args+=(--mdns)
fi

# 4. Handle mDNS Domain
if [ -n "$MDNS_DOMAIN" ]; then
cmd="$cmd --mdns-domain $MDNS_DOMAIN"
args+=(--mdns-domain "$MDNS_DOMAIN")
fi

# 5. Handle CORS (comma-separated list)
if [ -n "$CORS" ]; then
IFS=',' read -ra ORIGINS <<< "$CORS"
for origin in "${ORIGINS[@]}"; do
cmd="$cmd --cors $(echo $origin | xargs)"
args+=(--cors "$(echo "$origin" | xargs)")
done
fi

echo "$cmd"

printf '%s\n' "${args[@]}"
}

run_opencode_server() {
local command_args=()

while IFS= read -r line; do
command_args+=("$line")
done < <(build_command)

command_args+=("$@")

echo "Starting opencode server..."

if [ "$PUID" -ne 0 ] || [ "$PGID" -ne 0 ]; then
exec gosu "$PUID:$PGID" opencode "${command_args[@]}"
else
exec opencode "${command_args[@]}"
fi
}

# Main execution
main() {
# Create directory structure
create_directories

# Create user if needed
create_user

# Set ownership
set_ownership

# Build command
COMMAND=$(build_command)

echo "Starting opencode server..."

# Ensure OPENCODE_CONFIG is exported if set
if [ -n "$OPENCODE_CONFIG" ]; then
export OPENCODE_CONFIG
fi

# Execute as the appropriate user
if [ "$PUID" -ne 0 ] || [ "$PGID" -ne 0 ]; then
# Drop privileges using gosu
exec gosu "$PUID:$PGID" opencode $COMMAND "$@"
else
# Run as root (not recommended)
exec opencode $COMMAND "$@"

if [ "$#" -eq 0 ]; then
run_opencode_server
fi

case "$1" in
serve)
shift
run_opencode_server "$@"
;;
-*)
run_opencode_server "$@"
;;
*)
if [ "$PUID" -ne 0 ] || [ "$PGID" -ne 0 ]; then
exec gosu "$PUID:$PGID" "$@"
else
exec "$@"
fi
;;
esac
}

# Run main function
Expand Down
Loading