Compare commits

..

53 Commits

Author SHA1 Message Date
joakimp c05ec7503c Bump opencode to 1.14.20 and clarify versioning convention
Publish Docker Image / build-omos (push) Successful in 44m59s
Publish Docker Image / build-base (push) Successful in 45m10s
Publish Docker Image / update-description (push) Successful in 16s
Bump OPENCODE_VERSION ARG from 1.14.19 to 1.14.20 to track the new
upstream release on npm.

Clarify the tagging convention in AGENTS.md: the first build on a new
opencode version uses the bare 'v{opencode_version}' tag (no letter
suffix). Letter suffixes (a, b, c, ...) are reserved for container-
level rebuilds on the same opencode version (CVE fixes, doc changes,
entrypoint bugs). The previous wording implied a letter was always
required, which was never the actual behaviour.
2026-04-21 21:16:47 +02:00
joakimp 84b5ed4412 Fix PROMPT_COMMAND collision with zoxide causing ';;' parse error
Publish Docker Image / update-description (push) Has been cancelled
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / build-omos (push) Has been cancelled
v1.14.19c installed 'history -a; ' at the start of PROMPT_COMMAND
before zoxide's init ran. Zoxide's init uses ';' as its separator
when prepending __zoxide_hook, producing 'history -a;;__zoxide_hook'.
Every interactive prompt then emitted:

  bash: PROMPT_COMMAND: syntax error near unexpected token ';;'

History flushing still worked (the 'history -a' half parsed fine),
but the error spam made the shell feel broken.

Fix by moving the history-flush PROMPT_COMMAND assignment AFTER
zoxide's init, and using a newline separator (via ${PROMPT_COMMAND:+...}
parameter expansion) so there's no semicolon involved at all. Each
PROMPT_COMMAND line runs as its own statement, no parsing contention.

Known upstream issue: https://github.com/ajeetdsouza/zoxide/issues/722
2026-04-21 21:05:20 +02:00
joakimp 8535f73ad3 Ship shell defaults via /etc/skel-devbox so user files are preserved
Publish Docker Image / build-base (push) Successful in 40m28s
Publish Docker Image / build-omos (push) Successful in 50m37s
Publish Docker Image / update-description (push) Successful in 15s
Previous behaviour (e4063b5) COPY'd .bash_aliases and .inputrc
directly into /home/developer/ during image build. That silently
shadowed any host bind-mount or in-container customization for users
upgrading from v1.14.19b — if you'd written your own .bash_aliases
and rebuilt the container, our baked version would overwrite it
without warning.

Ship the files to /etc/skel-devbox/ instead. The entrypoint copies
them to $HOME only if the target file does not already exist, so:

- Fresh containers get the defaults automatically (unchanged)
- Host bind-mounts win (they materialize before the entrypoint runs)
- Existing in-container customizations survive upgrades
- Defaults remain discoverable at /etc/skel-devbox/ for anyone who
  wants to copy, diff, or reset back to upstream

Docs (README.md, DOCKER_HUB.md, deploy/README.md) describe the new
skel layout and the restore/diff commands.
2026-04-21 19:44:29 +02:00
joakimp e4063b5559 Persist bash history and bake shell quality-of-life defaults
Two changes that address a longstanding frustration: bash history is
lost on every container recreate, and the container's ~/.bashrc and
~/.inputrc are stock Debian (no history tuning, no prefix search on
arrow keys, no integrations).

Added a named volume 'devbox-shell-history' mounted at ~/.cache/bash
with HISTFILE pointing there; history now survives 'docker compose up
--force-recreate'. The volume is added to both docker-compose.yml and
docker-compose.shared.yml, and ~/.cache/bash is registered in the
entrypoint ownership-fix loop per the AGENTS.md convention.

Baked rootfs/home/developer/.bash_aliases (sourced automatically by
Debian's default ~/.bashrc) and rootfs/home/developer/.inputrc into
the image. They give new containers: 100k-entry timestamped dedup
history with per-prompt flush, Up/Down arrow prefix history search,
case-insensitive coloured completion, aliases that prefer eza and
bat when present, git shortcuts, interactive rm/mv/cp, zoxide and
fzf (via 'fzf --bash') integration, and a [devbox] prompt marker.
The fzf integration uses 'fzf --bash' because we install fzf from
GitHub releases, not apt — the apt-path key-bindings aren't present.

Users who prefer their host's own shell config can uncomment two
commented bind-mount lines in docker-compose.yml to shadow the
baked defaults.
2026-04-21 19:30:22 +02:00
joakimp cb4971b4a6 Document SSH banner-timeout workaround for residential CGNAT users
Add a Troubleshooting subsection to deploy/README.md describing the
ISP-CGNAT per-destination flow-table exhaustion that manifests as
'Connection timed out during banner exchange' or pure TCP connect
timeouts after the first 3-4 SSH connects.

The fix is SSH ControlMaster/ControlPersist on the client side, which
multiplexes all SSH sessions over one TCP flow and stays within the
CGNAT cap. sync-to-vm.sh already uses this pattern internally; this
note makes it discoverable for users hitting the issue in interactive
or scripted SSH use outside the deploy/ scripts.
2026-04-21 09:04:59 +02:00
joakimp 3d632ef02f Detect workspace UID and GID independently in entrypoint
The auto-detection block was gated on UID mismatch alone: if the host
user already had UID 1000 (the container's default) but a non-default
GID, the GID was never adopted. That left host-written files with a
numeric GID the container couldn't resolve to a name (e.g. '1001' on
Debian VMs where useradd assigns a dedicated group starting at 1001).

Split UID and GID detection into independent conditions. Each dimension
is adopted from /workspace if its env var is unset and the workspace
value differs from the container default, regardless of the other
dimension's state. Preserves existing USER_UID / USER_GID overrides.
2026-04-20 23:59:32 +02:00
joakimp 3669bec8ff Stop leaking host GIDs into VM via rsync -a
Publish Docker Image / build-base (push) Successful in 39m15s
Publish Docker Image / build-omos (push) Successful in 50m20s
Publish Docker Image / update-description (push) Successful in 16s
Replace 'rsync -az' with 'rsync -rlptDz' (archive minus owner/group
preservation). Running as a non-root user on the VM, rsync can't
preserve UID anyway, but it was successfully preserving GID whenever
the numeric GID happened to exist on the target. That caused synced
dirs (~/.aws, ~/.config/opencode, ~/.config/nvim, ~/.agents/skills,
~/.ssh) to end up with group 1001 on the VM, which was confusing
and, for group-writable mode, potentially insecure.

With -o and -g dropped, received files get the receiving user's
UID:GID (devbox:devbox), which is what you want.
2026-04-20 22:12:19 +02:00
joakimp f210d533eb Fix root-owned parent dirs left behind by nested volume mounts
When a named volume is mounted at a nested path like
/home/developer/.local/state/opencode, Docker creates the parent
directory (.local/state) as root:root. The existing chown loop only
fixed the leaf mount points, leaving parents unwritable by the
developer user.

Add a non-recursive pre-pass that fixes ownership of the common
parent dirs (.local, .local/share, .local/state, .config) so that
anything creating new subdirs beneath them works correctly after a
fresh container recreate. Regression introduced by commit 967ce7d
(devbox-state volume) and only partially addressed by a06dc5f.
2026-04-20 22:12:14 +02:00
joakimp 00d4f1596d Ignore personal deploy/my-cloud-init.yml override 2026-04-20 21:27:05 +02:00
joakimp 3c19b836cf Clarify OMOS-only features and host-mount portability in docs
Two related documentation fixes for users mounting ~/.config/opencode
from the host:

1. Gate oh-my-opencode-slim references (file and agents) to the OMOS
   variant in the Custom opencode config sections and data persistence
   tables. Base-variant users no longer see oh-my-opencode-slim.json
   listed as if it were always present.

2. Add a portability note warning that host-absolute paths in
   opencode.json (e.g. file:///usr/local/lib/node_modules/... or
   file:///opt/homebrew/...) will not resolve inside the Linux
   container, and to prefer bare package specifiers that work on
   both macOS and Linux hosts.
2026-04-20 21:25:44 +02:00
joakimp fffaeffb7a Refresh default model IDs for current providers
Update auto-generated opencode.json defaults to model IDs that are
valid as of April 2026:

- anthropic: claude-sonnet-4-5 -> claude-sonnet-4-6
- openai:    gpt-4o (retired Apr 3 2026) -> gpt-5.4
- bedrock:   anthropic.claude-sonnet-4-5-v1 (invalid) ->
             global.anthropic.claude-sonnet-4-5-20250929-v1:0

The Bedrock ID now uses the global inference profile (no regional
10% premium) and includes the required date stamp and :0 suffix.
2026-04-20 21:25:37 +02:00
joakimp b4d2f09e77 Document rsync in README and DOCKER_HUB tool lists 2026-04-20 20:27:09 +02:00
joakimp d74adc14dc Add rsync to base image 2026-04-20 20:26:24 +02:00
joakimp 9fa8b5c1e3 Fix misleading --rm wording: data loss happens on any container recreation 2026-04-20 15:03:00 +02:00
joakimp 3724519402 Document devbox-state volume for TUI settings persistence 2026-04-20 14:53:07 +02:00
joakimp a06dc5f47c Add state volume to entrypoint ownership fix loop 2026-04-20 14:48:12 +02:00
joakimp 967ce7df49 Add devbox-state volume to persist TUI settings across container recreations 2026-04-20 14:37:58 +02:00
joakimp c209d873ba Bump opencode to v1.14.19
Publish Docker Image / build-base (push) Successful in 39m5s
Publish Docker Image / build-omos (push) Successful in 50m52s
Publish Docker Image / update-description (push) Successful in 14s
2026-04-20 12:26:23 +02:00
joakimp e52ac46237 Document gcc and g++ in README and DOCKER_HUB tool lists 2026-04-20 10:26:52 +02:00
joakimp 83fb3d6de5 Add gcc and g++ to base image for C/C++ compilation support 2026-04-20 10:25:44 +02:00
joakimp d9d3a4c1d2 Fix Bun download URL: remove non-existent LATEST file fetch
Publish Docker Image / build-base (push) Successful in 36m8s
Publish Docker Image / build-omos (push) Successful in 47m45s
Publish Docker Image / update-description (push) Successful in 14s
2026-04-19 23:05:31 +02:00
joakimp 7b8c74852e Add fzf and ripgrep to VM provisioning packages 2026-04-19 23:03:21 +02:00
joakimp c32d50b364 Use Bun baseline build for AVX2-less CPU compatibility (Sandy Bridge)
Publish Docker Image / build-omos (push) Failing after 14m30s
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
2026-04-19 22:35:45 +02:00
joakimp dd63607a3f Ensure WORKSPACE_PATH from remote .env exists on VM 2026-04-19 20:15:22 +02:00
joakimp 3852d3b1ad Exclude AWS CLI and SSO cache from sync-to-vm.sh 2026-04-19 20:07:36 +02:00
joakimp ddea23e80a Exclude node_modules and other generated files from sync-to-vm.sh 2026-04-19 19:58:47 +02:00
joakimp 466383b546 Add rsync to installed packages for sync-to-vm.sh support 2026-04-19 19:54:56 +02:00
joakimp f21cf87881 Fix rsync flag for macOS compatibility 2026-04-19 19:30:31 +02:00
joakimp 3c7df3f888 Add sync-to-vm.sh to copy local config directories to remote VM 2026-04-19 19:25:18 +02:00
joakimp 6fc74b1f19 Add bind mount pre-creation note to deploy post-setup instructions 2026-04-19 19:11:36 +02:00
joakimp 05998bd6a2 Add Bedrock setup notes to deploy docs and cloud-init final message 2026-04-19 19:04:15 +02:00
joakimp b1e25a45b2 Default docker-compose.yml to pull from Docker Hub, sync with DOCKER_HUB.md 2026-04-19 18:50:12 +02:00
joakimp 16ff29101e Bump opencode to v1.14.18
Publish Docker Image / build-omos (push) Successful in 41m30s
Publish Docker Image / build-base (push) Successful in 43m45s
Publish Docker Image / update-description (push) Successful in 15s
2026-04-19 18:28:39 +02:00
joakimp 81100fd5bb Add caveats and two-step fallback for inline boot-from-volume command 2026-04-19 18:15:53 +02:00
joakimp 4893be4133 Add locale customization instructions to cloud-init template 2026-04-19 18:09:30 +02:00
joakimp 9ebff2e037 Fix --block-device syntax to match current OpenStack CLI key names 2026-04-19 16:49:26 +02:00
joakimp 5bac08dd03 Fix image name casing to match OpenStack: Debian-13-Trixie 2026-04-19 16:47:10 +02:00
joakimp addccd4a82 Remove --key-name from OpenStack examples, clarify SSH key is in cloud-init 2026-04-19 16:36:15 +02:00
joakimp 7b0f6ed880 Add floating IP instructions to OpenStack deploy docs 2026-04-19 16:22:52 +02:00
joakimp fa3bb12d44 Skip ufw on OpenStack in cloud-init, matching setup-host.sh behavior 2026-04-19 13:22:07 +02:00
joakimp d091b6b50f Add optional console password (chpasswd) to cloud-init and deploy docs 2026-04-19 13:10:12 +02:00
joakimp fb9629db2b Add NVMe performance volume example to OpenStack deploy docs 2026-04-19 11:33:55 +02:00
joakimp 265cbdb14c Document full OpenStack server create command with flavor, image, network 2026-04-19 11:18:31 +02:00
joakimp 68204f573b Skip ufw on OpenStack (auto-detected), add security group setup script
setup-host.sh now detects OpenStack via metadata endpoint and skips ufw.
New setup-openstack-secgroup.sh creates the required security group with
SSH, mosh, and ICMP rules via the OpenStack CLI.
2026-04-19 11:04:09 +02:00
joakimp e0258a928e Add VM host deployment scripts (cloud-init + post-install)
Recommended base: Debian 13 Trixie (matches opencode-devbox base image).
- cloud-init.yml: automated VM provisioning for Proxmox/OpenStack/cloud providers
- setup-host.sh: interactive post-install script for manually-created VMs
- README.md: documents both paths and VM sizing recommendations

Installs Docker (official repo), Compose v2, ufw firewall, mosh support,
and the IPv4 DNS preference workaround for Docker Hub IPv6 issues.
2026-04-19 10:43:41 +02:00
joakimp 4bd543050a Bump opencode to v1.4.17, add file utility to base image
Publish Docker Image / build-omos (push) Successful in 41m7s
Publish Docker Image / build-base (push) Successful in 43m7s
Publish Docker Image / update-description (push) Successful in 15s
2026-04-19 09:31:21 +02:00
joakimp b164c1b2f9 Bump opencode to v1.4.12
Publish Docker Image / build-omos (push) Successful in 42m1s
Publish Docker Image / build-base (push) Successful in 42m19s
Publish Docker Image / update-description (push) Successful in 14s
2026-04-18 23:11:46 +02:00
joakimp c59c66087a Limit locales to 16 common languages, document how to add more
Reduces locale generation from 200+ to 16 targeted locales (major world
languages + Nordic + key European). Saves build time and image size.
Users can add more at runtime via locale-gen.
2026-04-18 23:10:23 +02:00
joakimp e679fa06e6 Add check-versions.sh to compare pinned versions against latest releases
Run before tagging a release to see what tools have newer versions.
Reports only — does not modify files. Human decides what to bump.
2026-04-18 16:50:01 +02:00
joakimp d90dd76a46 Bump bat 0.26.1, uv 0.11.7, Go 1.26.2 2026-04-18 16:47:15 +02:00
joakimp 2153aa5659 Bump opencode to v1.4.11
Publish Docker Image / build-base (push) Successful in 1h15m31s
Publish Docker Image / build-omos (push) Failing after 1h29m23s
Publish Docker Image / update-description (push) Has been skipped
2026-04-18 16:43:38 +02:00
joakimp 0e4525ca53 Add git-crypt and age to base image for encrypted repo support 2026-04-18 16:40:52 +02:00
joakimp 43cecab0f7 Add shared-machine multi-user setup with per-user isolation via SIGNUM
For machines where multiple users share one OS account. Each user gets
isolated containers, config, and named volumes by running docker compose
from their own directory with a unique SIGNUM in .env.
2026-04-17 13:53:51 +02:00
19 changed files with 1230 additions and 43 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
OPENCODE_PROVIDER=anthropic OPENCODE_PROVIDER=anthropic
# Model override (optional, defaults per provider) # Model override (optional, defaults per provider)
# OPENCODE_MODEL=anthropic/claude-sonnet-4-5 # OPENCODE_MODEL=anthropic/claude-sonnet-4-6
# ── API Keys (set the one matching your provider) ──────────────────── # ── API Keys (set the one matching your provider) ────────────────────
# ANTHROPIC_API_KEY= # ANTHROPIC_API_KEY=
+27
View File
@@ -0,0 +1,27 @@
# ── Shared machine setup ─────────────────────────────────────────────
# Your corporate signum / username (REQUIRED)
# This isolates your container, config, and data from other users.
SIGNUM=your-signum-here
# ── Provider ─────────────────────────────────────────────────────────
OPENCODE_PROVIDER=amazon-bedrock
OPENCODE_MODEL=amazon-bedrock/eu.anthropic.claude-opus-4-6-v1
AWS_REGION=eu-west-1
AWS_PROFILE=default
# ── Git ──────────────────────────────────────────────────────────────
GIT_USER_NAME=Your Name
GIT_USER_EMAIL=your.name@example.com
# ── Paths (adjust to your layout) ───────────────────────────────────
# Default: ~/src mounted as /workspace
# WORKSPACE_PATH=~/src
# SSH keys — defaults to shared ~/.ssh
# If you have per-user keys: SSH_KEY_PATH=~/<signum>/.ssh
# SSH_KEY_PATH=~/.ssh
# ── Locale (defaults to en_US.UTF-8) ────────────────────────────────
# LANG=sv_SE.UTF-8
# LANGUAGE=sv_SE:sv
# LC_ALL=sv_SE.UTF-8
+3
View File
@@ -3,3 +3,6 @@
*.swo *.swo
*~ *~
.DS_Store .DS_Store
# Personal cloud-init overrides (not shared)
deploy/my-cloud-init.yml
+9 -1
View File
@@ -15,7 +15,15 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
## Versioning scheme ## Versioning scheme
Tags follow `v{opencode_version}{letter}` — e.g. `v1.4.3k`. The number matches the opencode npm version. The letter suffix increments for container-level changes (tooling, docs, CVE fixes) on the same opencode version. CI produces four Docker Hub tags per release: `vX.Y.Zn`, `latest`, `vX.Y.Zn-omos`, `latest-omos`. Tags follow `v{opencode_version}[letter]` — e.g. `v1.14.20` for the first build on a new opencode release, and `v1.14.20a`, `v1.14.20b`, … for subsequent rebuilds on the same opencode version.
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile`).
- **No letter suffix** on the first build of a new opencode version — the bare `v{opencode_version}` tag is the canonical release.
- **Letter suffix** (`a`, `b`, `c`, …) increments for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.
CI produces four Docker Hub tags per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`.
When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile` and update the comment in `.env.example` if it names a specific model/version for context.
## Critical conventions ## Critical conventions
+42 -10
View File
@@ -125,7 +125,9 @@ The container defaults to English (`en_US.UTF-8`) and neovim as the editor. Over
| `LC_ALL` | Override all locale settings | `en_US.UTF-8` | | `LC_ALL` | Override all locale settings | `en_US.UTF-8` |
| `EDITOR` | Default text editor | `nvim` | | `EDITOR` | Default text editor | `nvim` |
All common UTF-8 locales are pre-generated in the image. Example for Swedish: Pre-generated locales: `en_US`, `en_GB`, `sv_SE`, `da_DK`, `nb_NO`, `fi_FI`, `de_DE`, `fr_FR`, `es_ES`, `it_IT`, `pt_BR`, `nl_NL`, `pl_PL`, `ja_JP`, `ko_KR`, `zh_CN` (all UTF-8).
Example for Swedish:
```bash ```bash
LANG=sv_SE.UTF-8 LANG=sv_SE.UTF-8
@@ -133,6 +135,15 @@ LANGUAGE=sv_SE:sv
LC_ALL=sv_SE.UTF-8 LC_ALL=sv_SE.UTF-8
``` ```
To add a locale not in the list, run inside the container:
```bash
sudo sed -i '/xx_XX.UTF-8/s/^# //g' /etc/locale.gen
sudo locale-gen
```
Replace `xx_XX` with the desired locale (e.g. `ru_RU`, `tr_TR`). This change does not persist across container restarts — for permanent additions, build from source and modify the Dockerfile.
## Initial Setup ## Initial Setup
### 1. Create host directories ### 1. Create host directories
@@ -215,24 +226,28 @@ Understanding what survives container restarts and what doesn't:
| `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes — lives on host | SSH keys | | `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes — lives on host | SSH keys |
| `/home/developer/.aws` | Host bind mount | ✅ Yes — lives on host | AWS credentials/SSO cache | | `/home/developer/.aws` | Host bind mount | ✅ Yes — lives on host | AWS credentials/SSO cache |
| `/home/developer/.local/share/opencode` | Named volume (if configured) | ✅ Yes — Docker volume | Session history, memory, auth tokens | | `/home/developer/.local/share/opencode` | Named volume (if configured) | ✅ Yes — Docker volume | Session history, memory, auth tokens |
| `/home/developer/.local/state/opencode` | Named volume (if configured) | ✅ Yes — Docker volume | TUI settings (theme, toggles) |
| `/home/developer/.cache/bash` | Named volume `devbox-shell-history` | ✅ Yes — Docker volume | Bash history (`$HISTFILE`) — survives container recreate |
| `/home/developer/.local/share/uv` | Named volume (if configured) | ✅ Yes — Docker volume | Python installs, uv tool installs | | `/home/developer/.local/share/uv` | Named volume (if configured) | ✅ Yes — Docker volume | Python installs, uv tool installs |
| `/home/developer/.rustup` | Named volume (if configured) | ✅ Yes — Docker volume | Rust toolchains | | `/home/developer/.rustup` | Named volume (if configured) | ✅ Yes — Docker volume | Rust toolchains |
| `/home/developer/.cargo` | Named volume (if configured) | ✅ Yes — Docker volume | Cargo binaries, registry cache | | `/home/developer/.cargo` | Named volume (if configured) | ✅ Yes — Docker volume | Cargo binaries, registry cache |
| `/home/developer/.vscode-server` | Named volume (if configured) | ✅ Yes — Docker volume | VS Code server and extensions | | `/home/developer/.vscode-server` | Named volume (if configured) | ✅ Yes — Docker volume | VS Code server and extensions |
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes — lives on host | opencode.json, oh-my-opencode-slim.json, skills | | `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes — lives on host | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
### Key points ### Key points
- **Project files** (`/workspace`) are always safe — they're your host filesystem. - **Project files** (`/workspace`) are always safe — they're your host filesystem.
- **opencode config** is auto-generated from `OPENCODE_PROVIDER` env var on each start if no existing config is found. To persist config changes, mount the config directory from the host (see Custom opencode Config below). - **opencode config** is auto-generated from `OPENCODE_PROVIDER` env var on each start if no existing config is found. To persist config changes, mount the config directory from the host (see Custom opencode Config below).
- **opencode data** (session history, memory) is lost with `--rm` unless you add a named volume. - **opencode data** (session history, memory) is lost on container recreation unless you add a named volume.
- **Python installs** via `uv python install` are lost unless you add the `devbox-uv` named volume. - **TUI settings** (theme, toggles) are lost on container recreation unless you add the `devbox-state` named volume.
- **Rust toolchains** via `rustup-init` are lost unless you add the `devbox-rustup` and `devbox-cargo` named volumes. - **Bash history** persists via the `devbox-shell-history` volume mounted at `~/.cache/bash`. `HISTFILE` is pre-configured; no setup required.
- **Python installs** via `uv python install` are lost on container recreation unless you add the `devbox-uv` named volume.
- **Rust toolchains** via `rustup-init` are lost on container recreation unless you add the `devbox-rustup` and `devbox-cargo` named volumes.
- **AWS SSO tokens** persist across restarts when `~/.aws` is mounted (recommended for Bedrock users). - **AWS SSO tokens** persist across restarts when `~/.aws` is mounted (recommended for Bedrock users).
## Custom opencode Config ## Custom opencode Config
For full control over opencode settings (MCP servers, custom models, oh-my-opencode-slim agents, etc.), mount the entire config directory from the host: For full control over opencode settings (MCP servers, custom models, and — on the OMOS variant — oh-my-opencode-slim agents), mount the entire config directory from the host:
```bash ```bash
docker run -it --rm \ docker run -it --rm \
@@ -243,6 +258,8 @@ docker run -it --rm \
This persists all configuration changes across container restarts. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped. This persists all configuration changes across container restarts. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped.
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
## Neovim Configuration ## Neovim Configuration
The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host: The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host:
@@ -389,6 +406,7 @@ services:
image: joakimp/opencode-devbox:latest image: joakimp/opencode-devbox:latest
# For multi-agent orchestration, use the omos variant instead: # For multi-agent orchestration, use the omos variant instead:
# image: joakimp/opencode-devbox:latest-omos # image: joakimp/opencode-devbox:latest-omos
container_name: opencode-devbox
stdin_open: true stdin_open: true
tty: true tty: true
env_file: env_file:
@@ -399,8 +417,8 @@ services:
- ~/projects:/workspace - ~/projects:/workspace
- ~/.ssh:/home/developer/.ssh:ro - ~/.ssh:/home/developer/.ssh:ro
- devbox-data:/home/developer/.local/share/opencode - devbox-data:/home/developer/.local/share/opencode
# Optional: persist Python/uv installs across restarts - devbox-state:/home/developer/.local/state/opencode
# - devbox-uv:/home/developer/.local/share/uv - devbox-uv:/home/developer/.local/share/uv
# Optional: persist Rust toolchains and cargo data # Optional: persist Rust toolchains and cargo data
# - devbox-rustup:/home/developer/.rustup # - devbox-rustup:/home/developer/.rustup
# - devbox-cargo:/home/developer/.cargo # - devbox-cargo:/home/developer/.cargo
@@ -417,7 +435,8 @@ services:
volumes: volumes:
devbox-data: devbox-data:
# devbox-uv: devbox-state:
devbox-uv:
# devbox-rustup: # devbox-rustup:
# devbox-cargo: # devbox-cargo:
# devbox-vscode: # devbox-vscode:
@@ -446,6 +465,19 @@ docker compose run --rm devbox # direct to opencode
docker compose run --rm devbox bash # interactive shell docker compose run --rm devbox bash # interactive shell
``` ```
## Shell defaults
The image ships baked `.bash_aliases` and `.inputrc` in `/etc/skel-devbox/`. On first container start the entrypoint copies them to `/home/developer/` **only if the target file does not already exist**, so your host bind-mounts or any in-container customization are preserved across upgrades.
- **Prefix history search** on Up/Down arrows (type `git `, press Up, walk back through prior `git ...` commands only). Ctrl-Up / Ctrl-Down still step through full history.
- **Persistent history** via `$HISTFILE=~/.cache/bash/history`, backed by the `devbox-shell-history` named volume. Survives container recreate. 100 000 entries, time-stamped, dedup.
- **Case-insensitive tab completion** and coloured completion lists.
- **Aliases** — `ls`/`ll`/`la``eza`, `cat``bat`, `gs`/`gd`/`gl` for git, interactive `rm`/`mv`/`cp`.
- **Integrations** — `zoxide` (`z <fragment>`), `fzf` key bindings (`Ctrl-R`, `Ctrl-T`).
- **`[devbox]` prompt prefix** so you always know you're in the container.
To override with your host's own files, uncomment the matching bind-mount lines in `docker-compose.yml`. To restore the baked defaults any time: `cp /etc/skel-devbox/.bash_aliases ~/` (or delete the file and recreate the container).
## What's Included ## What's Included
### Base image (`latest`) ### Base image (`latest`)
@@ -454,7 +486,7 @@ docker compose run --rm devbox bash # interactive shell
- **opencode** — AI coding assistant - **opencode** — AI coding assistant
- **Node.js 22** — for npx-based MCP servers - **Node.js 22** — for npx-based MCP servers
- **AWS CLI v2** — SSO and Bedrock authentication - **AWS CLI v2** — SSO and Bedrock authentication
- **Dev tools** — git, git-lfs, ssh, ripgrep, fd, fzf, bat, eza, zoxide, uv, rustup, jq, make, curl, wget, neovim 0.12, tmux, htop, tree - **Dev tools** — git, git-lfs, git-crypt, age, ssh, ripgrep, fd, fzf, bat, eza, zoxide, uv, rustup, jq, make, gcc, g++, curl, wget, neovim 0.12, tmux, htop, tree, rsync
- **Non-root user** — runs as `developer` with UID auto-matched to workspace owner (sudo available) - **Non-root user** — runs as `developer` with UID auto-matched to workspace owner (sudo available)
### OMOS image (`latest-omos`) ### OMOS image (`latest-omos`)
+39 -6
View File
@@ -5,7 +5,7 @@ ARG DEBIAN_VERSION=trixie-slim
FROM debian:${DEBIAN_VERSION} AS base FROM debian:${DEBIAN_VERSION} AS base
ARG TARGETARCH ARG TARGETARCH
ARG OPENCODE_VERSION=1.4.7 ARG OPENCODE_VERSION=1.14.20
LABEL maintainer="joakimp" LABEL maintainer="joakimp"
LABEL description="Portable opencode developer container" LABEL description="Portable opencode developer container"
@@ -32,10 +32,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
make \ make \
patch \ patch \
diffutils \ diffutils \
git-crypt \
age \
file \
sudo \ sudo \
locales \ locales \
procps \ procps \
unzip \ unzip \
gcc \
g++ \
rsync \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -71,7 +77,7 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;
nvim --version | head -1 nvim --version | head -1
# bat — syntax-highlighted cat replacement # bat — syntax-highlighted cat replacement
ARG BAT_VERSION=0.25.0 ARG BAT_VERSION=0.26.1
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
curl -fsSL "https://github.com/sharkdp/bat/releases/download/v${BAT_VERSION}/bat-v${BAT_VERSION}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ curl -fsSL "https://github.com/sharkdp/bat/releases/download/v${BAT_VERSION}/bat-v${BAT_VERSION}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
install /tmp/bat-v${BAT_VERSION}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \ install /tmp/bat-v${BAT_VERSION}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
@@ -91,7 +97,7 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
zoxide --version zoxide --version
# uv — fast Python package manager (replaces pip, venv, pyenv) # uv — fast Python package manager (replaces pip, venv, pyenv)
ARG UV_VERSION=0.11.6 ARG UV_VERSION=0.11.7
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \ install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
@@ -108,7 +114,8 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
chmod +x /usr/local/bin/rustup-init chmod +x /usr/local/bin/rustup-init
# Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars) # Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars)
RUN sed -i '/\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen # To add more locales, run: sudo sed -i '/<locale>.UTF-8/s/^# //g' /etc/locale.gen && sudo locale-gen
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG=en_US.UTF-8 ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8
@@ -148,7 +155,7 @@ RUN if [ "${INSTALL_PYTHON}" = "true" ]; then \
# ── Optional: Go ───────────────────────────────────────────────────── # ── Optional: Go ─────────────────────────────────────────────────────
ARG INSTALL_GO=false ARG INSTALL_GO=false
ARG GO_VERSION=1.23.4 ARG GO_VERSION=1.26.2
RUN if [ "${INSTALL_GO}" = "true" ]; then \ RUN if [ "${INSTALL_GO}" = "true" ]; then \
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \ curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
@@ -159,10 +166,22 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \
# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ──────── # ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
# Installs Bun runtime and the oh-my-opencode-slim npm package. # Installs Bun runtime and the oh-my-opencode-slim npm package.
# Runtime activation is controlled by ENABLE_OMOS env var in entrypoint. # Runtime activation is controlled by ENABLE_OMOS env var in entrypoint.
# Uses the baseline Bun build (SSE4.2 only) for compatibility with older
# CPUs that lack AVX2 (e.g. Sandy Bridge on OpenStack).
ARG INSTALL_OMOS=false ARG INSTALL_OMOS=false
ARG OMOS_VERSION=latest ARG OMOS_VERSION=latest
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \ RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash && \ ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
BUN_ARCH="x64-baseline"; \
elif [ "$ARCH" = "aarch64" ]; then \
BUN_ARCH="aarch64"; \
fi && \
curl -fsSL "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
unzip -o /tmp/bun.zip -d /tmp/bun && \
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
chmod +x /usr/local/bin/bun && \
rm -rf /tmp/bun /tmp/bun.zip && \
bun --version && \ bun --version && \
npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \ npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
fi fi
@@ -181,9 +200,23 @@ RUN mkdir -p /workspace \
/home/${USER_NAME}/.config/opencode/skills \ /home/${USER_NAME}/.config/opencode/skills \
/home/${USER_NAME}/.agents/skills \ /home/${USER_NAME}/.agents/skills \
/home/${USER_NAME}/.local/share/opencode \ /home/${USER_NAME}/.local/share/opencode \
/home/${USER_NAME}/.cache/bash \
/home/${USER_NAME}/.ssh && \ /home/${USER_NAME}/.ssh && \
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME} chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
# ── Shell defaults (bash history, aliases, readline) ─────────────────
# Shipped under /etc/skel-devbox/ rather than copied directly to the
# user's home. The entrypoint copies them to /home/developer/ only if
# the target file does not already exist, so host bind-mounts and
# previously-customized files are never overwritten. Users can restore
# the baked defaults anytime via:
# cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
# History itself persists via the devbox-shell-history named volume
# mounted at ~/.cache/bash (HISTFILE points there).
RUN mkdir -p /etc/skel-devbox
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
# ── Entrypoint ──────────────────────────────────────────────────────── # ── Entrypoint ────────────────────────────────────────────────────────
COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
+85 -5
View File
@@ -128,14 +128,16 @@ docker compose exec -u developer devbox aws --version
### Custom opencode config ### Custom opencode config
For full control over opencode settings (MCP servers, custom models, oh-my-opencode-slim agents, etc.), mount the entire config directory from the host: For full control over opencode settings (MCP servers, custom models, and — on the OMOS variant — oh-my-opencode-slim agents), mount the entire config directory from the host:
```yaml ```yaml
volumes: volumes:
- ~/.config/opencode:/home/developer/.config/opencode - ~/.config/opencode:/home/developer/.config/opencode
``` ```
This persists all configuration changes across container restarts, including `opencode.json`, `oh-my-opencode-slim.json`, and skills. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped. This persists all configuration changes across container restarts, including `opencode.json`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json`. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped.
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
### Custom skills ### Custom skills
@@ -271,6 +273,39 @@ volumes:
- devbox-vscode:/home/developer/.vscode-server - devbox-vscode:/home/developer/.vscode-server
``` ```
### Shared machine setup (multiple users, single OS account)
For machines where multiple users share one OS account (e.g. a common `garage` user), a separate compose file isolates each user's config and data using a `SIGNUM` variable.
Each user creates their own directory and setup:
```bash
# Replace <signum> with your username/identifier
mkdir -p ~/<signum>/opencode-devbox
cd ~/<signum>/opencode-devbox
# Copy the shared-machine compose and env files
cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml
cp /path/to/opencode-devbox/.env.shared.example .env
# Create per-user config directory
mkdir -p ~/<signum>/.config/opencode
# Edit .env with your signum, provider, keys, etc.
vim .env
# Start
docker compose up -d
docker compose exec -u developer devbox-<signum> opencode
```
Each user's container, config, and named volumes are fully isolated:
- Container name: `devbox-<signum>` (no collisions)
- Named volumes: prefixed with the project directory name (automatic per-user isolation)
- Opencode config: `~/<signum>/.config/opencode/` (per-user settings, OMOS config, etc.)
See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration.
### Rebuilding the Image ### Rebuilding the Image
`docker compose run` and `docker compose up` use the existing image — they **do not rebuild** when you change the Dockerfile or build args (e.g. updating `OPENCODE_VERSION`). Rebuild explicitly: `docker compose run` and `docker compose up` use the existing image — they **do not rebuild** when you change the Dockerfile or build args (e.g. updating `OPENCODE_VERSION`). Rebuild explicitly:
@@ -403,6 +438,49 @@ The `--use-device-code` flag outputs a URL and short code instead of trying to o
SSO sessions typically last 812 hours before requiring re-authentication. Since `~/.aws` is mounted from the host, tokens persist across container restarts. SSO sessions typically last 812 hours before requiring re-authentication. Since `~/.aws` is mounted from the host, tokens persist across container restarts.
## Shell defaults
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
Defaults you get out of the box:
- **Prefix history search** on Up/Down arrows (type `git `, press Up, walk back through prior `git ...` commands only). Ctrl-Up / Ctrl-Down still step through full history.
- **Persistent history** — `$HISTFILE` points at `~/.cache/bash/history`, backed by the `devbox-shell-history` named volume so history survives container recreation. Timestamps, 100 000 entries, dedup.
- **Case-insensitive tab completion**, coloured completion lists, `show-all-if-ambiguous`.
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
### Overriding the defaults
**Option A — bind-mount host files.** Uncomment the bind-mount lines in `docker-compose.yml`:
```yaml
- ~/.bash_aliases:/home/developer/.bash_aliases:ro
- ~/.inputrc:/home/developer/.inputrc:ro
```
**Option B — customize inside the container.** Just edit `~/.bash_aliases` or `~/.inputrc` as normal. Pair this with a bind-mount or named volume on the home dir if you want the edits to survive container recreation.
### Restoring or diffing defaults
The skel files remain available inside every container at `/etc/skel-devbox/`. Useful commands:
```bash
# See what the image currently ships
cat /etc/skel-devbox/.bash_aliases
# Diff your current config against the upstream defaults
diff ~/.bash_aliases /etc/skel-devbox/.bash_aliases
# Reset to the baked defaults
cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
# …or delete the file and recreate the container — the entrypoint
# copies from /etc/skel-devbox/ on next start if the target is absent
rm ~/.bash_aliases
```
## Secret Scanning ## Secret Scanning
A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets. A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets.
@@ -444,8 +522,8 @@ Container (Debian trixie)
├── opencode binary ├── opencode binary
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun) ├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
├── AWS CLI v2 (SSO + Bedrock auth) ├── AWS CLI v2 (SSO + Bedrock auth)
├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make ├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
├── git, ssh, ripgrep, fd, fzf, jq, curl, tree ├── git, git-crypt, age, ssh, ripgrep, fd, fzf, jq, curl, tree
├── Node.js (for MCP servers) ├── Node.js (for MCP servers)
├── Bun (optional — included with oh-my-opencode-slim) ├── Bun (optional — included with oh-my-opencode-slim)
├── entrypoint.sh (UID adjustment, git config, provider setup) ├── entrypoint.sh (UID adjustment, git config, provider setup)
@@ -460,11 +538,13 @@ Container (Debian trixie)
| `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes | SSH keys | | `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes | SSH keys |
| `/home/developer/.aws` | Host bind mount (if configured) | ✅ Yes | AWS credentials/SSO cache | | `/home/developer/.aws` | Host bind mount (if configured) | ✅ Yes | AWS credentials/SSO cache |
| `/home/developer/.local/share/opencode` | Named volume `devbox-data` | ✅ Yes | Session history, memory | | `/home/developer/.local/share/opencode` | Named volume `devbox-data` | ✅ Yes | Session history, memory |
| `/home/developer/.local/state/opencode` | Named volume `devbox-state` | ✅ Yes | TUI settings (theme, toggles) |
| `/home/developer/.cache/bash` | Named volume `devbox-shell-history` | ✅ Yes | Bash history (`$HISTFILE`), survives container recreate |
| `/home/developer/.local/share/uv` | Named volume `devbox-uv` (if configured) | ✅ Yes | Python installs, uv tool installs | | `/home/developer/.local/share/uv` | Named volume `devbox-uv` (if configured) | ✅ Yes | Python installs, uv tool installs |
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains | | `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache | | `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions | | `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, oh-my-opencode-slim.json, skills | | `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
**opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above). **opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above).
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# check-versions.sh — Compare pinned versions in Dockerfile against latest releases
# Run before tagging a release to see what can be bumped.
set -euo pipefail
BOLD="\033[1m"; DIM="\033[2m"; GREEN="\033[32m"; YELLOW="\033[33m"; RESET="\033[0m"
DOCKERFILE="${1:-Dockerfile}"
if [[ ! -f "$DOCKERFILE" ]]; then
echo "Usage: $0 [Dockerfile]"
exit 1
fi
get_pinned() {
grep "^ARG $1=" "$DOCKERFILE" | head -1 | cut -d= -f2
}
get_latest_github() {
local repo="$1"
local tag
tag=$(curl -s "https://api.github.com/repos/${repo}/releases/latest" | jq -r '.tag_name // empty')
# Strip leading 'v' if present
echo "${tag#v}"
}
get_latest_go() {
curl -s "https://go.dev/dl/?mode=json" | jq -r '.[0].version' | sed 's/^go//'
}
get_latest_npm() {
npm view "$1" version 2>/dev/null
}
check() {
local name="$1" current="$2" latest="$3"
if [[ -z "$latest" ]]; then
printf " ${DIM}%-20s %-12s (could not check)${RESET}\n" "$name" "$current"
elif [[ "$current" == "$latest" ]]; then
printf " ${GREEN}%-20s %-12s ✓ up to date${RESET}\n" "$name" "$current"
else
printf " ${YELLOW}${BOLD}%-20s %-12s → %s available${RESET}\n" "$name" "$current" "$latest"
fi
}
echo ""
echo -e "${BOLD}Version check for $DOCKERFILE${RESET}"
echo ""
# GitHub-sourced binaries
check "opencode" "$(get_pinned OPENCODE_VERSION)" "$(get_latest_npm opencode-ai)"
check "gosu" "$(get_pinned GOSU_VERSION)" "$(get_latest_github tianon/gosu)"
check "fzf" "$(get_pinned FZF_VERSION)" "$(get_latest_github junegunn/fzf)"
check "git-lfs" "$(get_pinned GIT_LFS_VERSION)" "$(get_latest_github git-lfs/git-lfs)"
check "neovim" "$(get_pinned NVIM_VERSION)" "$(get_latest_github neovim/neovim)"
check "bat" "$(get_pinned BAT_VERSION)" "$(get_latest_github sharkdp/bat)"
check "eza" "$(get_pinned EZA_VERSION)" "$(get_latest_github eza-community/eza)"
check "zoxide" "$(get_pinned ZOXIDE_VERSION)" "$(get_latest_github ajeetdsouza/zoxide)"
check "uv" "$(get_pinned UV_VERSION)" "$(get_latest_github astral-sh/uv)"
check "Go (opt)" "$(get_pinned GO_VERSION)" "$(get_latest_go)"
echo ""
echo -e "${DIM}Node.js uses major version ($(get_pinned NODE_VERSION)) — auto-updates via nodesource.${RESET}"
echo -e "${DIM}rustup-init uses latest from static.rust-lang.org — no pinned version.${RESET}"
echo -e "${DIM}Debian apt packages update on each build via apt-get update.${RESET}"
echo ""
+254
View File
@@ -0,0 +1,254 @@
# Deploy — Host VM setup
Scripts for setting up a fresh Linux VM to host opencode-devbox.
## Files
- **`cloud-init.yml`** — cloud-init user-data template for automated VM provisioning on OpenStack, Proxmox, or any cloud with cloud-init support
- **`setup-host.sh`** — interactive post-install script for VMs that weren't provisioned with cloud-init
- **`setup-openstack-secgroup.sh`** — creates an OpenStack security group with the right rules (SSH, mosh, ICMP)
- **`sync-to-vm.sh`** — syncs local config directories (`~/.aws`, `~/.config/opencode`, etc.) to a remote VM based on which bind mounts are active in its `docker-compose.yml`
## Supported distributions
- **Debian 13 (Trixie)** — recommended (matches opencode-devbox base image)
- **Ubuntu 24.04 LTS** — also works
Other distributions will need manual adaptation.
## Quick start
### Option 1: Cloud-init (automated)
Customize `cloud-init.yml` — replace the SSH public key and optionally the hostname/timezone. Then use it during VM creation:
- **Proxmox**: attach as cloud-init user-data
- **OpenStack**: pass via `--user-data` flag (see full example below)
- **AWS/DigitalOcean/etc**: paste into the "user data" field
#### Full OpenStack example
Cloud-init only handles guest configuration — flavor, image, network, and security group must be specified explicitly at creation time.
> **Note:** Do not use `--key-name` — the SSH key is configured in `cloud-init.yml` under `ssh_authorized_keys` for the `devbox` user. The `--key-name` flag injects into the image's default user (e.g. `debian`), not the `devbox` user created by cloud-init.
```bash
# List available flavors to choose appropriate sizing
openstack flavor list
# Create the security group first (one-time, see below)
./setup-openstack-secgroup.sh
# Basic — boot from default storage
openstack server create \
--flavor c4m8 \
--image Debian-13-Trixie \
--network my-network \
--security-group opencode-devbox \
--user-data cloud-init.yml \
devbox-vm
```
If your cloud offers NVMe-backed (performance) volumes, boot from one for faster Docker and build I/O:
```bash
# Performance — boot from NVMe volume (40GB, preserved on instance deletion)
openstack server create \
--flavor c4m8 \
--network my-network \
--security-group opencode-devbox \
--user-data cloud-init.yml \
--block-device source_type=image,uuid=$(openstack image show Debian-13-Trixie -f value -c id),destination_type=volume,volume_size=40,delete_on_termination=false,boot_index=0,volume_type=performance \
devbox-vm
```
> **Note:** The inline `volume_type` parameter requires API microversion 2.67+. If the server goes to ERROR state, check your volume quota (`openstack quota show`) and try creating the volume separately:
> ```bash
> openstack volume create --image Debian-13-Trixie --size 40 --type performance --bootable devbox-boot-volume
> openstack server create --flavor c4m8 --volume devbox-boot-volume --network my-network --security-group opencode-devbox --user-data cloud-init.yml devbox-vm
> ```
#### Floating IP
OpenStack doesn't support assigning a floating IP at instance creation time — it's a separate step after the VM is active:
```bash
# Allocate a new floating IP from the external network
openstack floating ip create <external-network>
# Assign it to the VM
openstack server add floating ip devbox-vm <floating-ip>
```
To find your external network name: `openstack network list --external`. If you already have an unassigned floating IP, skip the create step.
The VM boots with Docker installed, firewall configured (or skipped on OpenStack), and your SSH key authorized. Log in as the `devbox` user.
### Console password (optional)
The cloud-init template uses SSH key authentication only — no password is set by default. This is sufficient for normal use since the `devbox` user has passwordless `sudo`.
A password is only needed for:
- **Emergency console access** — logging in via OpenStack Horizon console (noVNC) or Proxmox VNC when SSH is unreachable
- **`su - devbox`** — switching to the devbox user from another account
To enable console access, uncomment the `chpasswd` block in `cloud-init.yml` before deploying:
```yaml
chpasswd:
expire: false
users:
- name: devbox
password: your-password-here
type: text
```
For an already-running VM, set a password via SSH:
```bash
sudo passwd devbox
```
### Option 2: Post-install script (manual)
On a fresh Debian/Ubuntu VM:
```bash
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/deploy/setup-host.sh | bash
```
Or clone and run:
```bash
git clone https://gitea.jordbo.se/joakimp/opencode-devbox
cd opencode-devbox/deploy
./setup-host.sh
```
## What gets installed
- Docker Engine (from Docker's official apt repo, not distro's `docker.io`)
- Docker Compose plugin (v2)
- `tmux`, `mosh`, `git`
- `ufw` firewall with SSH (22) and mosh (UDP 60000-61000) allowed — **skipped on OpenStack** (detected automatically; use security groups instead)
- IPv4 DNS preference (works around Docker Hub IPv6 connectivity issues)
## OpenStack security groups
On OpenStack, firewalling is handled by security groups rather than ufw. The `setup-host.sh` script detects OpenStack automatically and skips ufw configuration.
To create the required security group:
```bash
./setup-openstack-secgroup.sh
```
This creates a security group named `opencode-devbox` with rules for SSH (TCP 22), mosh (UDP 60000-61000), and ICMP. Apply it to your instance:
```bash
# New instance
openstack server create --security-group opencode-devbox ...
# Existing instance
openstack server add security group <instance-name> opencode-devbox
```
## VM sizing recommendations
| Use case | vCPU | RAM | Disk |
|---|---|---|---|
| Minimum | 2 | 4 GB | 20 GB |
| Recommended | 4 | 8 GB | 40 GB |
| Heavy use (Rust/Python builds, multi-project) | 8 | 16 GB | 80 GB |
## After VM setup
If you uncomment any bind mounts in `docker-compose.yml` (e.g. `~/.aws`, `~/.config/opencode`), create the directories first — Docker creates missing bind mount paths as root-owned, which causes permission issues:
```bash
# Only create directories for mounts you uncomment
mkdir -p ~/.aws # AWS Bedrock SSO
mkdir -p ~/.config/opencode # persistent opencode config
mkdir -p ~/.config/nvim # custom neovim config
mkdir -p ~/.agents/skills # opencode agent skills
```
Named volumes (`devbox-data`, `devbox-uv`, etc.) are managed by Docker and need no pre-creation.
```bash
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml -o docker-compose.yml
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
vim .env # configure provider and keys
vim docker-compose.yml # uncomment optional volume mounts
docker compose up -d
docker compose exec -u developer devbox opencode
```
> **AWS Bedrock users:** Uncomment the `~/.aws` volume mount in `docker-compose.yml` before starting. You'll also need to copy your `~/.aws/config` from a machine where SSO is already configured, then authenticate inside the container with `aws sso login`.
### Syncing local config to the VM
After editing `docker-compose.yml` on the VM to uncomment the bind mounts you need, run `sync-to-vm.sh` from your local machine to copy the corresponding directories:
```bash
./deploy/sync-to-vm.sh devbox-affection
```
The script reads `docker-compose.yml` on the remote VM, detects which bind mounts are active, and syncs only those directories from your local machine. It also creates the remote directories if they don't exist.
### Shell defaults inside the container
The image ships baked `.bash_aliases` and `.inputrc` in `/etc/skel-devbox/` — quality-of-life defaults (prefix history search on Up/Down arrows, persistent history across container recreates via the `devbox-shell-history` named volume, `[devbox]` prompt marker, sensible aliases). On first container start the entrypoint copies them to `/home/developer/` **only if the target file does not already exist**.
This means:
- Fresh containers get the defaults automatically.
- If you bind-mount your host's `~/.bash_aliases` / `~/.inputrc` (see the commented lines in `docker-compose.yml`), your host versions win.
- If you edit the files inside a running container and store them via a home-dir bind-mount or equivalent, subsequent upgrades never overwrite them.
- To restore the baked defaults any time: `cp /etc/skel-devbox/.bash_aliases ~/` (or delete the file and recreate the container).
- To diff your current config against what the image ships: `diff ~/.bash_aliases /etc/skel-devbox/.bash_aliases`.
### Troubleshooting: SSH hangs or "banner exchange" timeouts
If SSH to the VM intermittently fails with `Connection timed out during banner exchange` or pure TCP connect timeouts — especially after the first few successful connects in a short window — the cause is almost certainly your ISP's CGNAT (Carrier-Grade NAT), not the VM.
**Symptoms**
- First 34 SSH connects succeed, then subsequent ones fail hard for 2030 minutes
- `ping` to the VM works perfectly throughout (ICMP isn't tracked the same way)
- `mosh` sessions stay stable once established (UDP, different flow table)
- Happens on residential ISPs (Tele2, Comhem, Telia, most European consumer broadband)
- VM-side logs show SSH is idle — the SYNs never reach it
**Cause**
Residential CGNAT boxes keep a per-subscriber TCP flow table with a small concurrent-flow cap (~4) per destination IP. Once exhausted, new SYNs to that destination are silently dropped until old flows age out (typically 2030 min after TCP close).
**Fix**
Add SSH connection multiplexing on your client so all SSH sessions (interactive, `scp`, `rsync`, scripts) share a single TCP connection to the VM:
```ssh-config
# ~/.ssh/config
Host <vm-alias>
HostName <vm-ip>
User devbox
IdentityFile ~/.ssh/id_ed25519
ControlMaster auto
ControlPath ~/.ssh/cm/%r@%h:%p
ControlPersist 4h
ServerAliveInterval 30
ServerAliveCountMax 6
```
Then create the socket directory:
```bash
mkdir -p ~/.ssh/cm && chmod 700 ~/.ssh/cm
```
All SSH to the VM now multiplexes over a single flow slot, regardless of how many parallel sessions you open. `sync-to-vm.sh` already does this internally for its own rsync/scp calls.
For a more robust long-term fix (especially if you access the VM from multiple hosts), run a WireGuard tunnel on the VM and route SSH through that — UDP bypasses the TCP flow table entirely.
+110
View File
@@ -0,0 +1,110 @@
#cloud-config
# cloud-init template for opencode-devbox host VM
# Tested on Debian 13 (Trixie) and Ubuntu 24.04
#
# Usage:
# - Proxmox: attach this file as cloud-init user-data in VM config
# - OpenStack: pass as --user-data when creating the instance
# - Cloud providers: paste into "user data" field
#
# Customize the marked sections before use.
# ── Hostname ─────────────────────────────────────────────────────────
hostname: devbox
manage_etc_hosts: true
# ── User ─────────────────────────────────────────────────────────────
users:
- name: devbox
groups: sudo, docker
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
# CUSTOMIZE: replace with your public SSH key.
# This is the only SSH key config needed — do NOT use --key-name with
# openstack server create, as that injects into the image's default
# user (e.g. debian), not the devbox user defined here.
- ssh-ed25519 AAAA... your-key-here
# ── Optional: console password ───────────────────────────────────────
# Uncomment to set a password for the devbox user. Only needed for
# emergency access via the OpenStack/Proxmox console (VNC/noVNC).
# SSH key authentication is used for normal access.
#
# chpasswd:
# expire: false
# users:
# - name: devbox
# password: your-password-here
# type: text
# ── Locale and timezone ──────────────────────────────────────────────
# en_US.UTF-8 is pre-generated on Debian/Ubuntu and works out of the box.
# To use a different locale (e.g. sv_SE.UTF-8), add it to the runcmd
# section before the locale is applied:
# - locale-gen sv_SE.UTF-8
# Then change the locale line below to match.
locale: en_US.UTF-8
timezone: Europe/Stockholm
# ── Package installation ─────────────────────────────────────────────
package_update: true
package_upgrade: true
packages:
- ca-certificates
- curl
- gnupg
- git
- tmux
- mosh
- rsync
- fzf
- ripgrep
- ufw
# ── Commands to run at first boot ────────────────────────────────────
runcmd:
# Install Docker from official repository
- install -m 0755 -d /etc/apt/keyrings
- curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
- chmod a+r /etc/apt/keyrings/docker.asc
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo \"$ID\") $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" > /etc/apt/sources.list.d/docker.list
- apt-get update
- apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- usermod -aG docker devbox
# Firewall — skip on OpenStack (use security groups instead)
- |
if curl -s --connect-timeout 2 http://169.254.169.254/openstack/ >/dev/null 2>&1; then
echo "OpenStack detected — skipping ufw (use security groups instead)"
else
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 60000:61000/udp
ufw --force enable
fi
# Disable IPv6 preference for Docker (avoids intermittent Docker Hub connectivity issues)
- echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
# Create projects directory for the user
- mkdir -p /home/devbox/projects
- chown devbox:devbox /home/devbox/projects
# ── Final message ───────────────────────────────────────────────────
final_message: |
opencode-devbox host VM ready.
Next steps:
1. SSH in: ssh devbox@<this-host>
2. Clone your opencode-devbox compose config, or:
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml -o docker-compose.yml
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
3. Edit .env with your provider and keys
4. Edit docker-compose.yml to uncomment optional mounts (e.g. ~/.aws for Bedrock)
5. docker compose up -d
6. docker compose exec -u developer devbox opencode
Cloud-init run completed in $UPTIME seconds.
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
# setup-host.sh — Post-install script for opencode-devbox host VM
#
# Run this on a fresh Debian 13 or Ubuntu 24.04 VM to set up everything
# needed to run opencode-devbox containers.
#
# Usage:
# curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/deploy/setup-host.sh | bash
#
# Or clone and run:
# git clone https://gitea.jordbo.se/joakimp/opencode-devbox
# cd opencode-devbox/deploy
# ./setup-host.sh
set -euo pipefail
# ── Colors ──────────────────────────────────────────────────────────
BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
info() { echo -e "${BOLD}==>${RESET} $*"; }
ok() { echo -e "${GREEN}${BOLD}${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}!${RESET} $*"; }
err() { echo -e "${RED}${BOLD}${RESET} $*" >&2; }
# ── Detect distro ──────────────────────────────────────────────────
if [[ ! -f /etc/os-release ]]; then
err "Cannot detect Linux distribution — /etc/os-release missing"
exit 1
fi
. /etc/os-release
case "$ID" in
debian|ubuntu)
info "Detected $PRETTY_NAME"
;;
*)
err "Unsupported distribution: $ID — this script only supports Debian and Ubuntu"
exit 1
;;
esac
# ── Require sudo ────────────────────────────────────────────────────
if [[ $EUID -eq 0 ]]; then
err "Do not run as root — use a regular user with sudo"
exit 1
fi
if ! sudo -n true 2>/dev/null; then
warn "This script needs sudo access. You may be prompted for your password."
fi
# ── Update packages ─────────────────────────────────────────────────
info "Updating package index..."
sudo apt-get update -qq
info "Installing base packages..."
sudo apt-get install -y --no-install-recommends \
ca-certificates curl gnupg git tmux mosh rsync fzf ripgrep ufw
# ── Docker ──────────────────────────────────────────────────────────
if command -v docker &>/dev/null; then
ok "Docker already installed ($(docker --version))"
else
info "Installing Docker from official repository..."
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
ok "Docker installed: $(docker --version)"
fi
# ── Add user to docker group ────────────────────────────────────────
if groups | grep -q docker; then
ok "User already in docker group"
else
info "Adding $USER to docker group..."
sudo usermod -aG docker "$USER"
warn "You must log out and back in for docker group to take effect"
warn "Or run: newgrp docker"
fi
# ── Firewall ────────────────────────────────────────────────────────
# Detect OpenStack — if running on OpenStack, skip ufw (security groups handle firewalling)
SKIP_UFW=false
if curl -s --connect-timeout 2 http://169.254.169.254/openstack/ &>/dev/null; then
SKIP_UFW=true
warn "OpenStack detected — skipping ufw (use security groups instead)"
warn "Ensure your security group allows: SSH (22/tcp), mosh (60000-61000/udp)"
fi
if [[ "$SKIP_UFW" == "false" ]]; then
info "Configuring firewall (ufw)..."
sudo ufw default deny incoming >/dev/null
sudo ufw default allow outgoing >/dev/null
sudo ufw allow ssh >/dev/null
sudo ufw allow 60000:61000/udp comment 'mosh' >/dev/null
if ! sudo ufw status | grep -q "Status: active"; then
sudo ufw --force enable
fi
ok "Firewall active — SSH and mosh allowed"
fi
# ── IPv4 preference for Docker Hub ──────────────────────────────────
if ! grep -q 'precedence ::ffff:0:0/96' /etc/gai.conf 2>/dev/null; then
info "Setting IPv4 preference in /etc/gai.conf..."
echo 'precedence ::ffff:0:0/96 100' | sudo tee -a /etc/gai.conf > /dev/null
ok "IPv4 preferred for DNS resolution"
fi
# ── Create projects directory ───────────────────────────────────────
if [[ ! -d "$HOME/projects" ]]; then
mkdir -p "$HOME/projects"
ok "Created ~/projects"
fi
# ── Done ────────────────────────────────────────────────────────────
echo ""
ok "Host setup complete"
echo ""
cat <<EOF
${BOLD}Next steps:${RESET}
1. If you weren't already in the docker group, log out and back in:
exit
ssh <your-user>@<this-host>
2. Set up opencode-devbox:
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml -o docker-compose.yml
curl -sL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
3. Edit .env with your provider and API keys:
vim .env
4. Start and connect:
docker compose up -d
docker compose exec -u developer devbox opencode
EOF
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# setup-openstack-secgroup.sh — Create an OpenStack security group for opencode-devbox
#
# Prerequisites:
# - OpenStack CLI installed (pip install python-openstackclient)
# - Authenticated (source your openrc.sh or clouds.yaml configured)
#
# Usage:
# ./setup-openstack-secgroup.sh [group-name]
#
# Default group name: opencode-devbox
set -euo pipefail
GROUP_NAME="${1:-opencode-devbox}"
BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RESET="\033[0m"
info() { echo -e "${BOLD}==>${RESET} $*"; }
ok() { echo -e "${GREEN}${BOLD}${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}!${RESET} $*"; }
if ! command -v openstack &>/dev/null; then
echo "Error: openstack CLI not found. Install with: pip install python-openstackclient"
exit 1
fi
# Check if group already exists
if openstack security group show "$GROUP_NAME" &>/dev/null; then
warn "Security group '$GROUP_NAME' already exists — updating rules"
else
info "Creating security group '$GROUP_NAME'..."
openstack security group create "$GROUP_NAME" \
--description "opencode-devbox: SSH, mosh, HTTPS"
ok "Security group created"
fi
# Add rules (idempotent — OpenStack ignores duplicates)
info "Adding rules..."
# SSH (TCP 22)
openstack security group rule create "$GROUP_NAME" \
--protocol tcp --dst-port 22 --remote-ip 0.0.0.0/0 \
--description "SSH" 2>/dev/null && ok "SSH (TCP 22)" || warn "SSH rule already exists"
# Mosh (UDP 60000-61000)
openstack security group rule create "$GROUP_NAME" \
--protocol udp --dst-port 60000:61000 --remote-ip 0.0.0.0/0 \
--description "mosh" 2>/dev/null && ok "mosh (UDP 60000-61000)" || warn "mosh rule already exists"
# ICMP (ping — useful for diagnostics)
openstack security group rule create "$GROUP_NAME" \
--protocol icmp --remote-ip 0.0.0.0/0 \
--description "ICMP ping" 2>/dev/null && ok "ICMP ping" || warn "ICMP rule already exists"
echo ""
ok "Security group '$GROUP_NAME' ready"
echo ""
echo -e "${BOLD}Apply to a new instance:${RESET}"
echo " openstack server create --security-group $GROUP_NAME ..."
echo ""
echo -e "${BOLD}Apply to an existing instance:${RESET}"
echo " openstack server add security group <instance-name> $GROUP_NAME"
echo ""
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
# sync-to-vm.sh — Copy local config to an opencode-devbox VM
#
# Reads docker-compose.yml on the remote VM to detect which bind mounts
# are active, then syncs the corresponding directories from this machine.
#
# Usage:
# ./sync-to-vm.sh <ssh-host>
#
# Examples:
# ./sync-to-vm.sh devbox-affection
# ./sync-to-vm.sh devbox@129.192.68.184
set -euo pipefail
# ── Colors ──────────────────────────────────────────────────────────
BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
info() { echo -e "${BOLD}==>${RESET} $*"; }
ok() { echo -e "${GREEN}${BOLD}${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}!${RESET} $*"; }
err() { echo -e "${RED}${BOLD}${RESET} $*" >&2; }
# ── Args ────────────────────────────────────────────────────────────
if [[ $# -lt 1 ]]; then
err "Usage: $0 <ssh-host>"
echo " Example: $0 devbox-affection"
exit 1
fi
SSH_HOST="$1"
REMOTE_COMPOSE="~/opencode-devbox/docker-compose.yml"
# ── SSH multiplexing (reuse one connection for all operations) ──────
CTRL_SOCKET=$(mktemp -u /tmp/sync-to-vm-XXXXXX)
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${CTRL_SOCKET} -o ControlPersist=120 -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
cleanup() {
ssh ${SSH_OPTS} -O exit "$SSH_HOST" 2>/dev/null || true
rm -f "$CTRL_SOCKET"
}
trap cleanup EXIT
ssh_cmd() {
ssh ${SSH_OPTS} "$SSH_HOST" "$@"
}
# ── Bind mount patterns to detect ──────────────────────────────────
# Maps: grep pattern → local source → remote destination
declare -a MOUNT_PATTERNS=(
"~/.aws:/home/developer/.aws|$HOME/.aws|~/.aws"
"~/.config/opencode:/home/developer/.config/opencode|$HOME/.config/opencode|~/.config/opencode"
"~/.config/nvim:/home/developer/.config/nvim|$HOME/.config/nvim|~/.config/nvim"
"~/.agents/skills:/home/developer/.agents/skills|$HOME/.agents/skills|~/.agents/skills"
)
# ── Establish persistent SSH connection ─────────────────────────────
info "Connecting to ${SSH_HOST}..."
if ! ssh_cmd true 2>/dev/null; then
err "Cannot connect to ${SSH_HOST}"
exit 1
fi
ok "Connected to ${SSH_HOST}"
# ── Fetch remote docker-compose.yml ─────────────────────────────────
info "Reading docker-compose.yml from ${SSH_HOST}..."
REMOTE_COMPOSE_CONTENT=$(ssh_cmd "cat $REMOTE_COMPOSE 2>/dev/null") || {
err "Could not read ${REMOTE_COMPOSE} on ${SSH_HOST}"
err "Has the VM been set up? Run the post-setup steps first."
exit 1
}
# ── Ensure workspace directory exists on remote ─────────────────────
REMOTE_ENV="~/opencode-devbox/.env"
WORKSPACE_PATH=$(ssh_cmd "grep -E '^\s*WORKSPACE_PATH=' $REMOTE_ENV 2>/dev/null | cut -d= -f2- | tr -d '\"'" 2>/dev/null || true)
if [[ -n "$WORKSPACE_PATH" ]]; then
info "Ensuring WORKSPACE_PATH (${WORKSPACE_PATH}) exists on ${SSH_HOST}..."
ssh_cmd "mkdir -p ${WORKSPACE_PATH}"
ok "Workspace directory ready"
else
# Default from docker-compose.yml is ~/projects or current dir
WORKSPACE_PATH=$(echo "$REMOTE_COMPOSE_CONTENT" | grep -oP 'WORKSPACE_PATH:-[^}]+' | sed 's/WORKSPACE_PATH:-//' || true)
if [[ -n "$WORKSPACE_PATH" && "$WORKSPACE_PATH" != "." ]]; then
info "Ensuring default WORKSPACE_PATH (${WORKSPACE_PATH}) exists on ${SSH_HOST}..."
ssh_cmd "mkdir -p ${WORKSPACE_PATH}"
ok "Workspace directory ready"
fi
fi
# ── Detect active bind mounts ──────────────────────────────────────
SYNCED=0
for entry in "${MOUNT_PATTERNS[@]}"; do
IFS='|' read -r pattern local_path remote_path <<< "$entry"
# Check if the mount is uncommented (active) in docker-compose.yml
# Match lines that start with optional whitespace and a dash, NOT preceded by #
if echo "$REMOTE_COMPOSE_CONTENT" | grep -qE "^\s*-\s+['\"]?${pattern}" 2>/dev/null; then
# Mount is active — check if local source exists
if [[ ! -d "$local_path" ]]; then
warn "Mount active for ${pattern} but ${local_path} does not exist locally — skipping"
continue
fi
# Check if directory has content
if [[ -z "$(ls -A "$local_path" 2>/dev/null)" ]]; then
warn "${local_path} is empty — skipping"
continue
fi
info "Syncing ${local_path}${SSH_HOST}:${remote_path}"
# Ensure remote directory exists
ssh_cmd "mkdir -p ${remote_path}"
# Sync with rsync (fall back to scp if rsync unavailable)
# Exclude generated/cached content that gets recreated on the remote.
# Use -rlptD (archive minus -o -g) so ownership on the remote is set
# by the receiving user (devbox). Preserving host UID/GID with -a
# tagged files with the pusher's numeric GID, which leaked through
# whenever the VM happened to have a matching group (see #group-1001).
if command -v rsync &>/dev/null; then
rsync -rlptDz --progress \
--exclude='node_modules' \
--exclude='__pycache__' \
--exclude='.venv' \
--exclude='*.pyc' \
--exclude='cli/cache' \
--exclude='sso/cache' \
-e "ssh ${SSH_OPTS}" "${local_path}/" "${SSH_HOST}:${remote_path}/"
else
scp -o "ControlPath=${CTRL_SOCKET}" -r "${local_path}/." "${SSH_HOST}:${remote_path}/"
fi
ok "Synced ${local_path}"
SYNCED=$((SYNCED + 1))
fi
done
# ── Summary ─────────────────────────────────────────────────────────
echo ""
if [[ $SYNCED -eq 0 ]]; then
warn "No active bind mounts detected in remote docker-compose.yml"
warn "Uncomment the mounts you need in ${REMOTE_COMPOSE} on the VM, then re-run this script"
else
ok "Synced ${SYNCED} director$([ $SYNCED -eq 1 ] && echo 'y' || echo 'ies') to ${SSH_HOST}"
fi
+54
View File
@@ -0,0 +1,54 @@
# opencode-devbox docker-compose for shared machines
#
# For machines where multiple users share one OS account (e.g. 'garage').
# Each user gets isolated config, data, and named volumes by setting
# SIGNUM in their .env file.
#
# Setup per user:
# 1. mkdir -p ~/<signum>/opencode-devbox && cd ~/<signum>/opencode-devbox
# 2. cp docker-compose.shared.yml docker-compose.yml
# 3. cp .env.shared.example .env
# 4. Edit .env with your signum, provider, keys, etc.
# 5. mkdir -p ~/<signum>/.config/opencode
# 6. docker compose up -d
#
# Named volumes are automatically isolated per user because Docker Compose
# prefixes them with the project directory name (e.g. opencode-devbox_devbox-data).
# Since each user runs from ~/<signum>/opencode-devbox/, volumes don't collide.
services:
devbox:
image: joakimp/opencode-devbox:latest
container_name: devbox-${SIGNUM:?Set SIGNUM in .env}
stdin_open: true
tty: true
env_file:
- .env
environment:
- TERM=xterm-256color
volumes:
# Host workspace — user's project directory
- ${WORKSPACE_PATH:-~/src}:/workspace
# SSH keys — user-specific if available, else shared
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
# Opencode config — per-user (persists settings across restarts)
- ${HOME}/${SIGNUM}/.config/opencode:/home/developer/.config/opencode
# Persist opencode data (auth, memory, session history)
- devbox-data:/home/developer/.local/share/opencode
# Persist bash history across container recreations
- devbox-shell-history:/home/developer/.cache/bash
# Persist uv data (Python installs)
- devbox-uv:/home/developer/.local/share/uv
# Optional: AWS credentials (per-user if available)
# - ${HOME}/${SIGNUM}/.aws:/home/developer/.aws
volumes:
devbox-data:
devbox-shell-history:
devbox-uv:
+26 -7
View File
@@ -10,13 +10,17 @@
services: services:
devbox: devbox:
build: image: joakimp/opencode-devbox:latest
context: . # For multi-agent orchestration, use the omos variant instead:
args: # image: joakimp/opencode-devbox:latest-omos
INSTALL_PYTHON: "false" #
INSTALL_GO: "false" # To build from source instead of pulling from Docker Hub, uncomment:
INSTALL_OMOS: "false" # build:
image: opencode-devbox:latest # context: .
# args:
# INSTALL_PYTHON: "false"
# INSTALL_GO: "false"
# INSTALL_OMOS: "false"
container_name: opencode-devbox container_name: opencode-devbox
stdin_open: true stdin_open: true
tty: true tty: true
@@ -45,6 +49,19 @@ services:
# Optional: persist opencode data (auth, memory, etc.) # Optional: persist opencode data (auth, memory, etc.)
- devbox-data:/home/developer/.local/share/opencode - devbox-data:/home/developer/.local/share/opencode
# Optional: persist opencode TUI settings (theme, toggles, etc.)
- devbox-state:/home/developer/.local/state/opencode
# Persist bash history across container recreations.
# Without this, ~/.bash_history is lost on 'docker compose up --force-recreate'.
- devbox-shell-history:/home/developer/.cache/bash
# Optional: override baked shell defaults with your host's rc files.
# The image ships sensible defaults (history tuning, prefix-search on
# Up/Down arrows, fzf/zoxide integration). Uncomment to use your own:
# - ~/.bash_aliases:/home/developer/.bash_aliases:ro
# - ~/.inputrc:/home/developer/.inputrc:ro
# Optional: persist uv data (Python installs, tool installs) # Optional: persist uv data (Python installs, tool installs)
# Without this, 'uv python install' must be re-run after container removal. # Without this, 'uv python install' must be re-run after container removal.
- devbox-uv:/home/developer/.local/share/uv - devbox-uv:/home/developer/.local/share/uv
@@ -62,6 +79,8 @@ services:
volumes: volumes:
devbox-data: devbox-data:
devbox-state:
devbox-shell-history:
devbox-uv: devbox-uv:
# devbox-rustup: # devbox-rustup:
# devbox-cargo: # devbox-cargo:
+18 -4
View File
@@ -1,6 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ── Shell defaults: copy baked files from /etc/skel-devbox/ if absent
# Respects host bind-mounts and user customizations — existing files
# are never overwritten. To restore defaults: rm ~/.bash_aliases (or
# .inputrc) and recreate the container, or cp from /etc/skel-devbox/
# directly.
SKEL_DIR="/etc/skel-devbox"
if [ -d "$SKEL_DIR" ]; then
for f in .bash_aliases .inputrc; do
if [ -f "$SKEL_DIR/$f" ] && [ ! -e "$HOME/$f" ]; then
cp "$SKEL_DIR/$f" "$HOME/$f"
fi
done
fi
# ── Git config defaults ────────────────────────────────────────────── # ── Git config defaults ──────────────────────────────────────────────
if [ -n "${GIT_USER_NAME:-}" ] && ! git config --global user.name &>/dev/null; then if [ -n "${GIT_USER_NAME:-}" ] && ! git config --global user.name &>/dev/null; then
git config --global user.name "$GIT_USER_NAME" git config --global user.name "$GIT_USER_NAME"
@@ -22,7 +36,7 @@ if [ ! -f "$CONFIG_FILE" ] && [ -n "${OPENCODE_PROVIDER:-}" ]; then
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
{ {
"\$schema": "https://opencode.ai/config.json", "\$schema": "https://opencode.ai/config.json",
"model": "${OPENCODE_MODEL:-anthropic/claude-sonnet-4-5}", "model": "${OPENCODE_MODEL:-anthropic/claude-sonnet-4-6}",
"share": "disabled", "share": "disabled",
"autoupdate": false "autoupdate": false
} }
@@ -32,7 +46,7 @@ EOF
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
{ {
"\$schema": "https://opencode.ai/config.json", "\$schema": "https://opencode.ai/config.json",
"model": "${OPENCODE_MODEL:-openai/gpt-4o}", "model": "${OPENCODE_MODEL:-openai/gpt-5.4}",
"share": "disabled", "share": "disabled",
"autoupdate": false "autoupdate": false
} }
@@ -42,7 +56,7 @@ EOF
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
{ {
"\$schema": "https://opencode.ai/config.json", "\$schema": "https://opencode.ai/config.json",
"model": "${OPENCODE_MODEL:-amazon-bedrock/anthropic.claude-sonnet-4-5-v1}", "model": "${OPENCODE_MODEL:-amazon-bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0}",
"share": "disabled", "share": "disabled",
"autoupdate": false, "autoupdate": false,
"provider": { "provider": {
@@ -60,7 +74,7 @@ EOF
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
{ {
"\$schema": "https://opencode.ai/config.json", "\$schema": "https://opencode.ai/config.json",
"model": "${OPENCODE_MODEL:-anthropic/claude-sonnet-4-5}", "model": "${OPENCODE_MODEL:-anthropic/claude-sonnet-4-6}",
"share": "disabled", "share": "disabled",
"autoupdate": false "autoupdate": false
} }
+33 -9
View File
@@ -6,18 +6,22 @@ CURRENT_UID=$(id -u "$USER_NAME")
CURRENT_GID=$(id -g "$USER_NAME") CURRENT_GID=$(id -g "$USER_NAME")
# ── UID/GID adjustment ─────────────────────────────────────────────── # ── UID/GID adjustment ───────────────────────────────────────────────
# Priority: env vars > auto-detect from /workspace > default (1000) # Priority per dimension: env var > auto-detect from /workspace > no-op
# UID and GID are detected independently so a GID-only mismatch (e.g. host
# user has UID 1000 but primary group at GID 1001) is still corrected.
TARGET_UID="${USER_UID:-}" TARGET_UID="${USER_UID:-}"
TARGET_GID="${USER_GID:-}" TARGET_GID="${USER_GID:-}"
# Auto-detect from /workspace owner if env vars not set if [ -d /workspace ]; then
if [ -z "$TARGET_UID" ] && [ -d /workspace ]; then WORKSPACE_UID=$(stat -c '%u' /workspace 2>/dev/null || stat -f '%u' /workspace 2>/dev/null || echo "")
WORKSPACE_UID=$(stat -c '%u' /workspace 2>/dev/null || stat -f '%u' /workspace 2>/dev/null) WORKSPACE_GID=$(stat -c '%g' /workspace 2>/dev/null || stat -f '%g' /workspace 2>/dev/null || echo "")
WORKSPACE_GID=$(stat -c '%g' /workspace 2>/dev/null || stat -f '%g' /workspace 2>/dev/null) # Adopt workspace UID if env var not set and workspace is non-root-owned
# Only adjust if workspace is owned by a non-root user if [ -z "$TARGET_UID" ] && [ -n "$WORKSPACE_UID" ] && [ "$WORKSPACE_UID" != "0" ] && [ "$WORKSPACE_UID" != "$CURRENT_UID" ]; then
if [ "$WORKSPACE_UID" != "0" ] && [ "$WORKSPACE_UID" != "$CURRENT_UID" ]; then
TARGET_UID="$WORKSPACE_UID" TARGET_UID="$WORKSPACE_UID"
TARGET_GID="${TARGET_GID:-$WORKSPACE_GID}" fi
# Adopt workspace GID if env var not set and workspace group differs
if [ -z "$TARGET_GID" ] && [ -n "$WORKSPACE_GID" ] && [ "$WORKSPACE_GID" != "0" ] && [ "$WORKSPACE_GID" != "$CURRENT_GID" ]; then
TARGET_GID="$WORKSPACE_GID"
fi fi
fi fi
@@ -25,12 +29,13 @@ fi
if [ -n "$TARGET_GID" ] && [ "$TARGET_GID" != "$CURRENT_GID" ]; then if [ -n "$TARGET_GID" ] && [ "$TARGET_GID" != "$CURRENT_GID" ]; then
groupmod -g "$TARGET_GID" "$USER_NAME" 2>/dev/null || true groupmod -g "$TARGET_GID" "$USER_NAME" 2>/dev/null || true
find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -group "$CURRENT_GID" -exec chgrp "$TARGET_GID" {} + 2>/dev/null || true find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -group "$CURRENT_GID" -exec chgrp "$TARGET_GID" {} + 2>/dev/null || true
echo "Adjusted developer GID to $TARGET_GID"
fi fi
if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "$CURRENT_UID" ]; then if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "$CURRENT_UID" ]; then
usermod -u "$TARGET_UID" "$USER_NAME" 2>/dev/null || true usermod -u "$TARGET_UID" "$USER_NAME" 2>/dev/null || true
find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -user "$CURRENT_UID" -exec chown "$TARGET_UID" {} + 2>/dev/null || true find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -user "$CURRENT_UID" -exec chown "$TARGET_UID" {} + 2>/dev/null || true
echo "Adjusted developer UID:GID to $TARGET_UID:${TARGET_GID:-$CURRENT_GID}" echo "Adjusted developer UID to $TARGET_UID"
fi fi
# ── SSH key permissions ────────────────────────────────────────────── # ── SSH key permissions ──────────────────────────────────────────────
@@ -51,9 +56,28 @@ fi
# developer user can write to them. # developer user can write to them.
FINAL_UID="${TARGET_UID:-$CURRENT_UID}" FINAL_UID="${TARGET_UID:-$CURRENT_UID}"
FINAL_GID="${TARGET_GID:-$CURRENT_GID}" FINAL_GID="${TARGET_GID:-$CURRENT_GID}"
# First, fix parent dirs that Docker auto-creates as root:root when it
# materializes nested mount points (e.g. mounting a volume at
# .local/state/opencode creates .local/state as root). Non-recursive —
# we only need the dir node itself; children are handled below or were
# created by the user.
for parent in \
/home/"$USER_NAME"/.local \
/home/"$USER_NAME"/.local/share \
/home/"$USER_NAME"/.local/state \
/home/"$USER_NAME"/.cache \
/home/"$USER_NAME"/.config; do
if [ -d "$parent" ] && [ "$(stat -c '%u' "$parent" 2>/dev/null)" != "$FINAL_UID" ]; then
chown "$FINAL_UID":"$FINAL_GID" "$parent" 2>/dev/null || true
fi
done
for dir in \ for dir in \
/home/"$USER_NAME"/.local/share/opencode \ /home/"$USER_NAME"/.local/share/opencode \
/home/"$USER_NAME"/.local/state/opencode \
/home/"$USER_NAME"/.local/share/uv \ /home/"$USER_NAME"/.local/share/uv \
/home/"$USER_NAME"/.cache/bash \
/home/"$USER_NAME"/.rustup \ /home/"$USER_NAME"/.rustup \
/home/"$USER_NAME"/.cargo \ /home/"$USER_NAME"/.cargo \
/home/"$USER_NAME"/.vscode-server \ /home/"$USER_NAME"/.vscode-server \
+82
View File
@@ -0,0 +1,82 @@
# opencode-devbox bash aliases and customizations
# Sourced by the Debian-default ~/.bashrc on shell startup.
# To override, bind-mount your host's ~/.bash_aliases over this file
# via docker-compose.yml.
# ── History persistence and quality ──────────────────────────────────
# The named volume devbox-shell-history is mounted at ~/.cache/bash
# so history survives container recreation.
export HISTFILE="${HOME}/.cache/bash/history"
mkdir -p "$(dirname "$HISTFILE")" 2>/dev/null || true
# Large, time-stamped, deduplicated history. Append rather than overwrite.
export HISTSIZE=100000
export HISTFILESIZE=200000
export HISTCONTROL=ignoreboth:erasedups
export HISTTIMEFORMAT='%F %T '
shopt -s histappend 2>/dev/null
shopt -s cmdhist 2>/dev/null
# Note: PROMPT_COMMAND="history -a" is installed LATER in this file,
# after zoxide's init runs. Installing it here would create a
# "history -a;;__zoxide_hook" chain because zoxide's init uses ';'
# as its separator and prepends itself; two adjacent ';' breaks the
# parser. See https://github.com/ajeetdsouza/zoxide/issues/722.
# ── Common aliases ───────────────────────────────────────────────────
# Prefer eza (modern ls) when available
if command -v eza >/dev/null 2>&1; then
alias ls='eza --group-directories-first'
alias ll='eza -lh --group-directories-first --git'
alias la='eza -lha --group-directories-first --git'
alias tree='eza --tree'
else
alias ll='ls -lh'
alias la='ls -lha'
fi
# Prefer bat (syntax-highlighted cat) when available
if command -v bat >/dev/null 2>&1; then
alias cat='bat --style=plain --paging=never'
alias less='bat --paging=always'
fi
# Git shortcuts
alias gs='git status'
alias gd='git diff'
alias gl='git log --oneline --graph --decorate -20'
# Safety: confirm before destructive ops
alias rm='rm -i'
alias mv='mv -i'
alias cp='cp -i'
# ── Shell integrations ───────────────────────────────────────────────
# zoxide — smarter cd. Use 'z <fragment>' to jump to previously-visited dirs.
if command -v zoxide >/dev/null 2>&1; then
eval "$(zoxide init bash)"
fi
# fzf — fuzzy finder key bindings (Ctrl-R for history, Ctrl-T for files).
# We install fzf from GitHub releases (not apt), so sourcing from the
# apt-path /usr/share/doc/fzf/examples/* would find nothing. Use the
# binary's own --bash flag (available since fzf 0.48) for setup.
if command -v fzf >/dev/null 2>&1; then
eval "$(fzf --bash)" 2>/dev/null || true
fi
# ── PROMPT_COMMAND: flush history every prompt ───────────────────────
# Installed AFTER zoxide init so zoxide's hook is already in place;
# we append with a newline separator to avoid the ';;' parse error
# described at the top of this file. Guarded so repeated sourcing
# (e.g. `exec bash`) doesn't stack duplicates.
if [ -z "${DEVBOX_HIST_SET:-}" ]; then
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}history -a"
export DEVBOX_HIST_SET=1
fi
# ── Prompt: show [opencode-devbox] tag so it's obvious you're in the container
# Preserves the default Debian PS1 logic but prefixes with a container marker.
if [ -n "${PS1:-}" ] && [ -z "${DEVBOX_PS1_SET:-}" ]; then
PS1='\[\e[38;5;39m\][devbox]\[\e[0m\] '"${PS1}"
export DEVBOX_PS1_SET=1
fi
+27
View File
@@ -0,0 +1,27 @@
# opencode-devbox readline defaults
# To override, bind-mount your host's ~/.inputrc over this file
# via docker-compose.yml.
# Inherit system-wide defaults (colour, 8-bit input, …) if present
$include /etc/inputrc
# ── History search on Up/Down ────────────────────────────────────────
# Type a prefix, press Up, and walk through previous commands starting
# with that prefix. Ctrl-Up / Ctrl-Down keep the unconditional stepper.
"\e[A": history-search-backward
"\e[B": history-search-forward
"\e[1;5A": previous-history
"\e[1;5B": next-history
# ── Completion quality ───────────────────────────────────────────────
set show-all-if-ambiguous on # single Tab shows matches on ambiguity
set completion-ignore-case on # case-insensitive file/dir completion
set colored-stats on # colour ls-style completion list entries
set colored-completion-prefix on # highlight the matched prefix
set visible-stats on # append /*@ type indicators in completion
set mark-symlinked-directories on # add trailing / to symlinks to dirs
set skip-completed-text on # don't re-insert already-typed text
# Treat hyphens and underscores as equivalent when completing (e.g.
# typing `foo-` matches both `foo-bar` and `foo_bar`).
set completion-map-case on