Skip to content

opencode web shows ENOENT: xdg-open error in container environments #31815

Closed as not planned

Description

@ZipiDan

Description

Running opencode web inside a container (Docker/Podman) without a desktop environment prints an ugly ENOENT error because xdg-open is not present in the image. The full error:

662 |     var file = this.spawnfile = options.file, spawnargs;
663 |     if (options.args === @undefined)
664 |       spawnargs = this.spawnargs = [];
665 |     else
666 |       validateArray(options.args, "options.args"), spawnargs = this.spawnargs = options.args;
667 |       if (this.#handle = Bun.spawn({

Error: Executable not found in $PATH: "xdg-open"
  path: "xdg-open",
 errno: -2,
syscall: "spawn xdg-open",
code: "ENOENT"

at spawn (node:child_process:667:35)
at spawn (node:child_process:14:39)
at <anonymous> (/$bunfs/root/chunk-8mgmcy90.js:3:5152)

Update (1.17.3)

Since 1.17.3 the HTTP server does start even when xdg-open is missing — the crash in 1.17.1 was fixed on 1.17.3. However, the error is still printed to stderr on startup, which is noisy and confusing for users running in containers.

The error has no useful meaning in a container environment: there is no desktop session, no $DISPLAY, and no browser to open. The server works fine, but the user sees a scary stack trace before any output.

Root cause

When opencode web starts, it spawns xdg-open to open the user's default browser. In a container environment (no desktop session, no $DISPLAY, no xdg-utils installed), this call fails with ENOENT. In 1.17.1 the exception killed the entire process. In 1.17.3 the crash is fixed, but the error is still logged to stderr before the server starts.

The server and the browser-opening logic should be decoupled: a missing or failed browser launch should be silent (or at most a warning) in headless environments.

Why this matters

1. Containers are a first-class deployment target

OpenCode's own install page documents docker run -it --rm ghcr.io/anomalyco/opencode as an official installation method. Containers are also used for:

  • CI/CD pipelines — running opencode in automated workflows
  • Remote development — SSH into a host that runs opencode in a container, access the web UI from the browser on the local machine
  • Multi-tenant setups — isolating projects in separate containers on the same host
  • Development environments — using Dev Containers, Podman, or Docker Compose for reproducible workspaces

In all these scenarios, the browser runs on the host, not inside the container. The container should only serve the web UI; the user opens it manually from their host browser.

2. The web server and the browser launch are separate concerns

opencode web serves an HTTP interface. Opening a browser is a convenience feature for local desktop usage, but it should not be a hard requirement for the server to start. In headless or containerized contexts, this convenience becomes a blocker.

3. No graceful fallback exists

There is currently no flag like --no-open or --no-browser to skip the browser launch. The environment variable BROWSER is not checked. The only workaround is either:

  • Installing xdg-utils inside the container (pointless — there is no browser to open)
  • Creating a fake /usr/local/bin/xdg-open no-op script (as done in Containerfile in this directory)

Neither is a proper solution.

Suggested fix

The browser launch error should be suppressed or downgraded to a debug-level log when xdg-open is not found:

try {
  spawn('xdg-open', [url]);
} catch (err) {
  // Container/headless environment — not an error
}

Additionally, consider adding:

  • A --no-open / --no-browser CLI flag for opencode web
  • Support for the BROWSER environment variable as an alternative to xdg-open
  • A server.openBrowser config option in opencode.json (default: true)

Workaround (immediate)

When you create a custom container and add the standard installation method:

Add this to your Containerfile before the ENTRYPOINT:

# Create a fake xdg-open
RUN printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/xdg-open && chmod +x /usr/local/bin/xdg-open

This creates a no-op xdg-open that satisfies the executable check without actually trying to open a browser.

Environment

  • OpenCode version: 1.17.1+ (error persists in 1.17.3, though the crash was fixed)
  • OS: Any Linux container (Debian, Alpine, etc.)
  • Runtime: Docker, Podman, or any OCI-compatible container runtime

Plugins

standard opencode plugin as it comes from the factory

OpenCode version

1.17.3 but from 1.17.1 exist the issue

Steps to reproduce

Simply pull the standard image: podman pull ghcr.io/anomalyco/opencode:1.17.3 and run with the web parameter and the error will be displayed on the screen.

But this is my Containerfile

FROM docker.io/debian:stable-backports

ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_USER=true

# Pre-configurar tshark para que no requiera setuid (ejecutamos como root en el contenedor)
RUN echo "wireshark-common wireshark-common/install-setuid boolean false" | debconf-set-selections

# Instalar todas las herramientas: esenciales, build, python, red y utilidades varias
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    git \
    jq \
    bash \
    file \
    unzip \
    zip \
    gcc \
    g++ \
    make \
    && rm -rf /var/lib/apt/lists/*

# Crear la estructura de directorios usada por opencode.sh para XDG y PYTHONUSERBASE
RUN mkdir -p /home/opencode/.config \
             /home/opencode/.local/state \
             /home/opencode/.local/share \
             /home/opencode/.pythonuser

# Instalar opencode mediante el instalador oficial
RUN curl -fsSL https://opencode.ai/install | bash \
    && ln -sf /root/.opencode/bin/opencode /usr/local/bin/opencode

# Copiar y preparar el entrypoint que decide entre modo web y TUI
COPY entrypoint.sh /usr/local/bin/opencode-entrypoint.sh
RUN chmod +x /usr/local/bin/opencode-entrypoint.sh

# Base para paquetes Python instalados por el usuario (persistente mediante volumen)
ENV PYTHONUSERBASE=/home/opencode/.pythonuser

# Directorio de trabajo por defecto: coincide con el volumen montado en opencode.sh
WORKDIR /workspace

# Puerto por defecto del servidor web de opencode
EXPOSE 4096

# Por defecto se arranca el servidor web en 0.0.0.0:4096.
# El modo TUI se activa con OPENCODE_MODE=tui (lo hace opencode.sh con --tui/--no-web).
ENTRYPOINT ["/usr/local/bin/opencode-entrypoint.sh"]
CMD ["web", "--port", "4096", "--hostname", "0.0.0.0"]

Entrypoint.sh

#!/bin/bash
set -e

# opencode container entrypoint.
# El modo se selecciona mediante la variable de entorno OPENCODE_MODE:
#   - web (por defecto): ejecuta `opencode` con los argumentos recibidos
#                         (típicamente los del CMD: web --port N --hostname ...)
#   - tui:                ejecuta `opencode` sin argumentos (arranca la TUI)

MODE="${OPENCODE_MODE:-web}"

case "$MODE" in
  tui)
    exec opencode
    ;;
  web)
    exec opencode "$@"
    ;;
  *)
    echo "[ERROR] OPENCODE_MODE desconocido: '$MODE' (valores válidos: 'web', 'tui')" >&2
    exit 1
    ;;
esac

Screenshot and/or share link

                                ▄     

█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀

Local access: http://localhost:4097
Network access: http://192.168.0.113:4097
662 | var file = this.spawnfile = options.file, spawnargs;
663 | if (options.args === @undefined)
664 | spawnargs = this.spawnargs = [];
665 | else
666 | validateArray(options.args, "options.args"), spawnargs = this.spawnargs = options.args;
667 | if (this.#handle = Bun.spawn({
^
error: Executable not found in $PATH: "xdg-open"
path: "xdg-open",
errno: -2,
syscall: "spawn xdg-open",
spawnargs: [ "http://localhost:4097" ],
code: "ENOENT"

  at spawn (node:child_process:667:35)
  at spawn (node:child_process:14:39)
  at <anonymous> (/$bunfs/root/chunk-j1dejnx8.js:3:5152)

Operating System

Host Debian 13 , podman 5.4.2, inside the container: docker.io/debian:stable-backports

Terminal

terminator but this not matter

Activity

  1. kagura-chen commented on Jun 11, 2026

    @kagura-chen
    Contributor

    I'd like to work on this. The fix is straightforward — check if xdg-open (or equivalent) is available before calling open() in web.ts, so the ENOENT error is never spawned in container/headless environments.

  2. disarticulate commented on Jun 13, 2026

    @disarticulate

    same here; unwanted action that should have been a flag. who runs this on baremetal?

  3. garbelini commented on Jun 15, 2026

    @garbelini

    quick workaround: add a dummy xdg-open

    RUN touch /usr/local/bin/xdg-open; chmod +x /usr/local/bin/xdg-open

  4. ZipiDan commented on Jun 19, 2026

    @ZipiDan
    Author

    same here; unwanted action that should have been a flag. who runs this on baremetal?

    Heh I think 90% of the user, hahaha.... My God, 90% irresponsibility.

    I don't consider any other option than using podman or similar alternative

  5. added a commit that references this issue on Jun 25, 2026
    9c40c60
  6. github-actions commented on Aug 19, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  7. ef-anrainer commented on Aug 19, 2026

    @ef-anrainer

    Still using the workaround from @garbelini - so what's wrong with the fix PR?

  8. kagura-chen commented on Aug 19, 2026

    @kagura-chen
    Contributor

    Quick note on the current state: the opencode web handler in web.ts already wraps its browser-open calls with .catch(() => {}) (present since commit 1409a07, May 2026), so the ENOENT no longer crashes opencode web in container/headless environments — the issue was auto-closed for inactivity rather than because it was ignored. PR #36875 by @tarcisiomiranda is the open fix awaiting maintainer review; I had also offered a fix back in June but there is no remaining gap that needs a second PR. Thanks for checking in!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions