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
124 changes: 124 additions & 0 deletions .github/workflows/build-docker-opencode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: 'build opencode Docker image'

on:

workflow_dispatch: # Allow manual triggering from GitHub UI

schedule:
- cron: '0 2,14 * * *' # runs at least twice every day: 2am and 2pm

push:
paths:
- 'Dockerfile'
- '.github/workflows/build-docker-opencode.yaml'
- '.hadolint.yaml'

jobs:

build_docker_opencode:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
attestations: write
id-token: write
env:
DOCKER_FILE: Dockerfile
REGISTRY: ghcr.io
IMAGE_NAME: ghcr.io/${{ github.repository }}
OPENCODE_CONFIG: opencode.jsonc

steps:

- name: 'check environment'
run: |
docker --version
echo "npm version: $(npm --version)"
OPENCODE_VERSION=$(npm view opencode-ai version)
echo "opencode version: $OPENCODE_VERSION"
echo "OPENCODE_VERSION=$OPENCODE_VERSION" >> $GITHUB_ENV
echo "=== env vars ==="
printenv

- name: 'checkout git code'
uses: actions/checkout@v5

- name: 'validate config files'
run: |
# check Dockerfile
docker run --rm -i hadolint/hadolint < $DOCKER_FILE
# check opencode config if it exists
if [[ -f $OPENCODE_CONFIG ]]
then
pipx install check-jsonschema
check-jsonschema --verbose --schemafile https://opencode.ai/config.json $OPENCODE_CONFIG
fi

- name: 'login to GitHub Container Registry'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value={{ date 'YYYY-MM-DD-HH-mm-ss' tz='Europe/Paris' }}
type=raw,latest
type=raw,opencode-${{ env.OPENCODE_VERSION }}
type=sha

- name: 'set up Docker Buildx'
uses: docker/setup-buildx-action@v3
env:
# to avoid unknown/unknown arch in container registry
BUILDX_NO_DEFAULT_ATTESTATIONS: 1
with:
provenance: false # Disable provenance to avoid unknown/unknown
sbom: false # Disable sbom to avoid unknown/unknown

- name: 'build and push to Github Container Registry'
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: ${{env.DOCKER_FILE}}
tags: |
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: 'run built image to validate'
run: |
set -x
CONTAINER_NAME="opencode-vibe"
# start detached container
docker run --detach --name "$CONTAINER_NAME" "$IMAGE_NAME:latest"
# validate user in container
WHOAMI=$(docker exec "$CONTAINER_NAME" whoami)
echo "opencode container whoami: $WHOAMI"
if [[ "$WHOAMI" != "opencode" ]];
then
exit 1
fi
# run version check in container
wget --quiet -O /usr/local/bin/semver https://raw.githubusercontent.com/fsaintjacques/semver-tool/master/src/semver
chmod +x /usr/local/bin/semver
OC_VERSION=$(docker exec "$CONTAINER_NAME" npm view opencode-ai version)
echo "opencode version: $OC_VERSION"
SEMVER_COMPARE=$(semver compare "$OC_VERSION" "$OPENCODE_VERSION")
if [[ "$SEMVER_COMPARE" -lt 0 ]];
then
exit 1
fi
# invoke opencode with free OpenRouter model
docker exec "$CONTAINER_NAME" opencode run --log-level INFO --model 'opencode/glm-4.7-free' 'who are you?' >> opencode.log
echo "opencode container invocation: $(cat opencode.log)"
grep --ignore-case 'opencode' opencode.log
# explicit successful exit to kill container
exit

3 changes: 3 additions & 0 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://github.com/hadolint/hadolint
ignored:
- DL3013 # warning: Pin versions in pip installs.`
90 changes: 90 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
FROM debian:trixie
SHELL ["/bin/bash", "-c"]
# escape=\

ARG PYTHON_VERSION="3.13"
ARG OTEL_VERSION="0.143.0"

ENV USER="opencode"
ENV HOME="/home/$USER"
WORKDIR "$HOME"

# hadolint ignore=DL3008
RUN apt-get update -y \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends ca-certificates procps jq curl wget unzip git gh nodejs npm \
&& apt-get install -y --no-install-recommends python${PYTHON_VERSION} python${PYTHON_VERSION}-venv python${PYTHON_VERSION} python3-pip \
&& python3 --version \
&& pip3 --version \
# clean up \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# OpenTelemetry install
ARG TARGETPLATFORM
# need to be explicitly set for docker build locally on MacOS (automatic on GitHub buildx)
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/arm64}
ENV OTEL_PLATFORM=${TARGETPLATFORM#*/}
# contrib package is required to have filelog extension
# ARG OTEL_COLL_DEB="otelcol_${OTEL_VERSION}_linux_${OTEL_PLATFORM}.deb"
ARG OTEL_COLL_DEB="otelcol-contrib_${OTEL_VERSION}_linux_${OTEL_PLATFORM}.deb"
RUN curl --location --output "$OTEL_COLL_DEB" "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v$OTEL_VERSION/$OTEL_COLL_DEB" \
&& dpkg -i "$OTEL_COLL_DEB" \
&& rm "$OTEL_COLL_DEB"

# OTEL_CONFIG="/etc/otelcol/config.yaml"
ENV OTEL_CONFIG="$HOME/otel-config.yaml"
COPY otel-config.yaml "$OTEL_CONFIG"

# install last version of uv
ENV PATH="$PATH:/$HOME/.local/bin"
# set to avoid issue describd in https://github.com/hadolint/hadolint/wiki/DL4006
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& source /$HOME/.local/bin/env \
&& uv --version

# install last version of opencode
# hadolint ignore=DL3016
RUN npm install -g opencode-ai
ENV PATH="$PATH:/$HOME/.opencode/bin/"

ENV OPENCODE_CONFIG="$HOME/.config/opencode/opencode.jsonc"
# use * pattern to avoid copy failure if files don't exist
COPY .opencode/* .opencode/

# user 'opencode' allows to have all user Opencode data & config on mounted volume
# see https://opencode.ai/docs/troubleshooting/ for content of –/.local
RUN groupadd --system $USER \
&& useradd --system $USER --gid $USER \
&& chown -R "$USER:$USER" $HOME
USER "$USER"

# for otel extensions - for debugging but should be removed for prod
# http://localhost:55679/debug/servicez
# http://localhost:1777/debug/pprof/
# http://localhost:13133
EXPOSE 1777
EXPOSE 13133
EXPOSE 55679

# expose otel grpc and http endpoints to make them usable from outside of the container
# for debugging but should be removed for prod
EXPOSE 4317
EXPOSE 4318

# no OpenTelemetry by default
ENV WITH_OTEL="false"

COPY <<EOF start-opencode.sh
echo "Opencode agent - version: $() - user= $(whoami) - started: $(TZ=$TZ date)"
export GH_TOKEN=$GITHUB_OPENCODE_TOKEN
if [[ "\$WITH_OTEL" == "true" ]]
then
echo "starting OpenTelemetry collector with backend $OTEL_BACKEND_ENDPOINT ..."
nohup otelcol-contrib --config=$OTEL_CONFIG
fi
sleep infinity
EOF

CMD ["bash", "-c", "bash ./start-opencode.sh | tee -a opencode.log"]
74 changes: 74 additions & 0 deletions otel-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

# https://oneuptime.com/blog/post/2025-08-28-how-to-structure-logs-properly-in-opentelemetry/view
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
zpages:
expvar:
enabled: true
endpoint: 0.0.0.0:55679

receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
filelog:
# default location of oopencode log files
include: [ "${env:HOME}/.local/share/opencode/log/*.log"]
operators:
- type: regex_parser
id: matching_regex
regex: '^(?P<sev>[A-Z]*) *(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) *(?P<msg>.*)$'
severity:
parse_from: attributes.sev
- type: regex_parser
id: catch_all
regex: '(?P<log>.*)'
# Collector for own metrics
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 10s
static_configs:
- targets: ['0.0.0.0:8888']

exporters:
# export to console
debug:
verbosity: detailed

# Export to OTEL backed
otlp/backend:
endpoint: "${env:OTEL_BACKEND_ENDPOINT}"
headers:
#"Authorization": "Bearer ${env:DASH0_TOKEN}"
"Authorization": "${env:OTEL_BACKEND_HEADER}"

processors:
batch:

service:
extensions: [health_check, pprof, zpages]
telemetry:
resource:
"service.name": "otelcol-own-telemetry"
pipelines:
# use 'debug' exporter to display Otel activity on local console
traces:
receivers: [otlp]
# exporters: [debug,otlp/backend]
exporters: [otlp/backend]
metrics:
receivers: [otlp,prometheus]
# exporters: [debug,otlp/backend]
exporters: [otlp/backend]
logs:
receivers: [otlp,filelog]
# exporters: [debug,otlp/backend]
exporters: [otlp/backend]