diff --git a/.gitignore b/.gitignore index 648de3e..44426d7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ opencode-data/ *.swp *.swo *~ - +.sisyphus/ # OS files .DS_Store Thumbs.db diff --git a/Dockerfile b/Dockerfile index 440a5e2..a051ad2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index ddfcb5e..1b6236a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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/ + │ └── /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): @@ -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 @@ -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 ``` @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index f00b746..ae576c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 558a953..06bd575 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -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." } @@ -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 } @@ -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