98 lines
8.1 KiB
Markdown
98 lines
8.1 KiB
Markdown
# OpenCode Sandbox
|
|
|
|
A rootless Podman container for running [OpenCode](https://github.com/anomalyco/opencode) with a pinned toolchain — Java, Node, pnpm, git — without touching your host machine's environment.
|
|
|
|
## What this gives you
|
|
|
|
- A hash-pinned, integrity-verified build of OpenCode (not "latest" — an exact version, checked against a SHA256 captured at build time).
|
|
- JDK 25 (LTS), Node 24 (LTS), Go, Rust, pnpm, git, curl, openssh-client.
|
|
- Language servers for Go, Rust, TypeScript/JavaScript, Java, and Kotlin, ready for OpenCode's built-in LSP client (see [LSP support](#lsp-support) below).
|
|
- The current directory mounted read-write as `/workspace`.
|
|
- OpenCode's config, data, and session state shared across every workdir/container, so logging in once carries over everywhere.
|
|
- Every container joined to a shared Podman network so it can reach other dev containers by name.
|
|
|
|
## Requirements
|
|
|
|
- [Podman](https://podman.io/) (rootless).
|
|
- On macOS: a running `podman machine` — `make` will start a stopped one automatically, but if you've never run Podman before you need to initialize one first:
|
|
```
|
|
podman machine init
|
|
```
|
|
- GNU Make (macOS's bundled Make 3.81 works fine; the Makefile deliberately avoids features added in 3.82+).
|
|
|
|
## Usage
|
|
|
|
```
|
|
make build # build (or rebuild) the container image
|
|
make opencode # launch the OpenCode TUI in the current directory
|
|
make shell # open a bash shell in the container instead
|
|
make install # build, then install a global `opencode-sandbox` shortcut (see below)
|
|
```
|
|
|
|
Run `make opencode` or `make shell` from any project directory — the image is shared, so there's nothing per-project to set up.
|
|
|
|
### `make install`
|
|
|
|
Installs a shim at `~/.local/bin/opencode-sandbox` that runs `make opencode`
|
|
against this repo's Makefile, using whatever directory you're standing in —
|
|
not this repo — as `/workspace`. Once installed, `opencode-sandbox` works as
|
|
a global command from any directory, no `cd` into this repo required. (Only
|
|
`opencode` gets a shortcut; `make shell` still has to be run from here.)
|
|
|
|
It also makes sure `~/.local/bin` is on `PATH` for future shells, based on
|
|
your login shell (`$SHELL`):
|
|
|
|
- zsh → appended to `~/.zshrc` (or `~/.zshenv` if that's the only one of the two that already exists; `~/.zshrc` is created if neither does)
|
|
- bash → `~/.bash_profile` on macOS, `~/.bashrc` on Linux (created if missing)
|
|
- fish → added via `fish_add_path` (no config file edit needed)
|
|
- anything else → appended to `~/.profile`
|
|
|
|
The edit is wrapped in `# >>> opencode-sandbox >>>` markers so re-running
|
|
`make install` never duplicates it, and is skipped entirely if
|
|
`~/.local/bin` is already on `PATH`.
|
|
|
|
## LSP support
|
|
|
|
OpenCode's built-in LSP client is off by default — turn it on per-project with `"lsp": true` in that project's `opencode.json` (or configure individual servers there; see OpenCode's own docs for the object form). Once enabled, this image is ready for it:
|
|
|
|
| Language | Server | How it gets there |
|
|
|---|---|---|
|
|
| Go | `gopls` | Built from source at image-build time (`go install`), pinned via `GOPLS_VERSION` in the Containerfile. |
|
|
| Rust | `rust-analyzer` | Installed as a `rustup` component at image-build time — OpenCode has no auto-download fallback for this one, so it has to already be on `PATH`. |
|
|
| TypeScript / JavaScript | `typescript` + `typescript-language-server` | Installed as global npm packages, pinned via `TYPESCRIPT_VERSION`/`TS_LS_VERSION`. Pinned to TypeScript's last JavaScript-based release (6.0.x) — TypeScript 7 is a native Go rewrite that dropped `tsserver.js`, which `typescript-language-server` needs to attach to. `NODE_PATH` is set so the global `typescript` package resolves even from a project with no local `node_modules`. |
|
|
| Java | `jdtls` | Pre-baked — OpenCode has no auto-download fallback for Java at all, so this has to be on `PATH`. Fetched straight from Eclipse's own download server (not GitHub Releases; architecture-independent, pure JVM). No `bin/jdtls` ships upstream, so `scripts/jdtls` is a hand-written launcher wrapper — the same convention every other jdtls consumer (nvim-jdtls, mason.nvim, coc-java) uses — which also manages a persistent, per-project `-data` workspace dir under OpenCode's host-persisted data dir. |
|
|
| Kotlin | `kotlin-lsp` | Pre-baked — JetBrains' official server (`github.com/Kotlin/kotlin-lsp`), fetched per-arch and hash-verified by hand (JetBrains publishes no checksum). Ships its own bundled JetBrains Runtime, so it needs none of this image's JDK. Symlinked as both `kotlin-lsp` and `kotlin-language-server`, since it's unconfirmed which name OpenCode's Kotlin integration expects. |
|
|
|
|
Bumping a pinned LSP version means editing the corresponding `ARG`/`FROM` tag in the Containerfile and rebuilding (`make build`).
|
|
|
|
## How it's put together
|
|
|
|
**Containerfile** — multi-stage build on `debian:trixie-slim`:
|
|
- JDK and Node are copied in from their official upstream images (`eclipse-temurin:25-jdk-noble`, `node:24-trixie-slim`) rather than apt-installed, keeping the final image slim while staying on a mainstream glibc distro (Alpine/musl is avoided — it breaks JVM and native Node addon compatibility).
|
|
- pnpm is installed via its own standalone installer script, pinned to a specific version.
|
|
- OpenCode is downloaded directly from its canonical GitHub releases (`github.com/anomalyco/opencode`) for the container's architecture, and its SHA256 is checked against a hash pinned in the Containerfile before it's installed — there's no maintainer-published signature for OpenCode releases, so this hash is captured by hand and is the actual root of trust here. Bump `OPENCODE_VERSION` and re-run the `sha256sum` steps documented in the Containerfile's comments whenever you upgrade it.
|
|
- A few build-time smoke tests (`ldd`, `java -version`, `node --version`, etc.) fail the build immediately if anything is broken, rather than surfacing at first `make opencode`.
|
|
|
|
**Makefile** — four targets:
|
|
- `build` builds the image.
|
|
- `opencode` and `shell` both run the container with the same flags: `--userns=keep-id` (so files created in the container are owned by your real host user, not root — verified: rootless Podman's user-namespace remapping means `id` shows `root` *inside* the container's own namespace view, but the file lands on the host owned by your actual UID/GID), the current directory bind-mounted to `/workspace`, and OpenCode's three XDG directories bind-mounted from your home directory:
|
|
|
|
| Host path | Container path | Contents |
|
|
|---|---|---|
|
|
| `~/.config/opencode` | `/home/dev/.config/opencode` | settings, plugins, themes |
|
|
| `~/.local/share/opencode` | `/home/dev/.local/share/opencode` | auth, database, snapshots |
|
|
| `~/.local/state/opencode` | `/home/dev/.local/state/opencode` | session/model history, locks |
|
|
|
|
Because these are real host paths shared by every container regardless of workdir, logging into OpenCode once carries over to every project you use the sandbox in.
|
|
|
|
- Both run targets join a shared, auto-created Podman network (`opencode-sandbox-net`) under a `--network-alias` derived from the current directory's name, so a container started from another dev-container setup on the same network can reach this one by that name (and vice versa).
|
|
|
|
## macOS-specific notes
|
|
|
|
- Podman on macOS runs everything inside a Linux VM (`podman machine`, backend `libkrun`/`applehv`, virtiofs file sharing). The Makefile detects this and starts a stopped machine automatically before `build`, `opencode`, or `shell`.
|
|
- SELinux volume-relabeling suffixes (`:Z`/`:z`) are intentionally omitted — macOS's host filesystem has no SELinux concept, so they'd be a no-op.
|
|
- Known virtiofs limitation: inotify/file-watch events from the host don't propagate into the container. Not a blocker for normal use, just worth knowing if a tool inside the container tries to watch `/workspace` for live host-side changes.
|
|
|
|
## Connecting to other dev containers
|
|
|
|
Any other container you attach to the `opencode-sandbox-net` network (`podman network connect opencode-sandbox-net <container>`, or by starting it with `--network opencode-sandbox-net`) can resolve and reach this sandbox by its workdir-derived alias, and vice versa.
|