Compare commits

..

73 Commits

Author SHA1 Message Date
joakimp 4de0bc9993 Document CI runner Docker pruning setup in deploy/README.md
Publish Docker Image / build-base (push) Successful in 44m32s
Publish Docker Image / build-omos (push) Successful in 54m49s
Publish Docker Image / update-description (push) Successful in 20s
Gitea Actions runners accumulate buildkit cache, stale containers,
and unused images. Without periodic cleanup the disk fills and builds
stall during image push (observed: build-omos hung at 'pushing layers'
for 1.5h on a 77%-full disk).

Add a 'CI runner maintenance' section to deploy/README.md with two
cleanup layers: a daily cron job (prunes anything >72h old) and
Docker daemon builder GC (caps buildkit cache at 10 GB).
2026-04-25 19:35:33 +02:00
joakimp b648d83928 Bump opencode to 1.14.25
Publish Docker Image / build-base (push) Successful in 40m15s
Publish Docker Image / build-omos (push) Failing after 1h45m55s
Publish Docker Image / update-description (push) Has been skipped
2026-04-25 17:36:30 +02:00
joakimp f2f8a70dae Add CHANGELOG.md and link it from Docker Hub description
Generated from annotated git tag messages. Covers every release from
v1.4.2 (initial) through v1.14.22b. One-line summaries for simple
bumps, bullet-point detail for feature/fix releases.

DOCKER_HUB.md gains a Changelog link in the Source section so Docker
Hub users can find release history without navigating the git forge.
2026-04-23 21:13:31 +02:00
joakimp c34cf3641b Add devbox-shell bridge line to baked .bash_aliases
Publish Docker Image / build-base (push) Successful in 41m27s
Publish Docker Image / build-omos (push) Successful in 53m45s
Publish Docker Image / update-description (push) Successful in 15s
If the host bind-mounts ~/.config/devbox-shell/ into the container
(the directory-mount pattern that avoids single-file inode breakage),
the container needs a bridge line in .bashrc or .bash_aliases to
source the mounted file. Previously this bridge had to be re-added
manually after every --force-recreate because it lived in the
container's writable layer.

Baking it into the skel .bash_aliases makes it automatic: every
fresh container sources ~/.config/devbox-shell/bash_aliases if it
exists, with zero manual steps. Hosts that don't use the devbox-shell
pattern are unaffected — the [ -r ... ] test silently skips.
2026-04-23 20:39:40 +02:00
joakimp 3a7ec45f4b Add python3-venv to base image (Mason needs ensurepip for venv creation)
python3-pip alone wasn't enough — Debian trixie ships python3 and
python3-pip as separate packages from python3.13-venv. Mason creates
a venv per package then pip-installs into it. Without python3-venv,
'python3 -m venv' fails with 'ensurepip is not available' and every
Mason Python package (ruff, ansible-lint, etc.) errors on every nvim
start.

Adding python3-venv (which pulls in ensurepip + pip-whl + setuptools-whl)
completes the chain: venv creation works, pip is available inside the
venv, Mason installs succeed.
2026-04-23 20:24:07 +02:00
joakimp e1029bbf27 Add python3-pip to base image for Mason LSP installs
Mason (neovim's package manager) creates a Python venv and runs
'pip install' inside it to install Python-based LSP servers like
ruff and ansible-lint. Debian trixie's python3 package ships without
ensurepip, so the venv has no pip and Mason fails with
'spawn: python3 failed with exit code 1'.

Adding python3-pip to the apt install list gives Mason what it needs.
uv is still available as the preferred user-facing Python tool
manager; pip is here specifically for Mason's internal use.
2026-04-23 20:21:40 +02:00
joakimp 8c919074dd Persist neovim plugin/Mason data across container recreations
Mason LSP installs and Lazy plugin cache live at ~/.local/share/nvim,
which was in the container's writable layer. Every --force-recreate
triggered a full re-download of all plugins and LSP servers on next
nvim launch — slow and wasteful.

Add devbox-nvim-data named volume in docker-compose.yml and
docker-compose.shared.yml, add to entrypoint ownership-fix loop,
update persistence tables in README.md and DOCKER_HUB.md.
2026-04-23 19:56:35 +02:00
joakimp bca403c540 Bump opencode to 1.14.22
Publish Docker Image / build-omos (push) Successful in 44m33s
Publish Docker Image / build-base (push) Successful in 46m35s
Publish Docker Image / update-description (push) Successful in 19s
2026-04-23 18:10:08 +02:00
joakimp c182ada0dd Persist zoxide directory history across container recreations
Publish Docker Image / build-base (push) Successful in 40m32s
Publish Docker Image / build-omos (push) Successful in 50m17s
Publish Docker Image / update-description (push) Successful in 13s
Zoxide stores its database at ~/.local/share/zoxide/db.zo. Without a
named volume, the 'z <fragment>' jump targets are lost on every
'docker compose up --force-recreate'.

Add devbox-zoxide named volume in docker-compose.yml and
docker-compose.shared.yml, add ~/.local/share/zoxide to the
entrypoint ownership-fix loop per AGENTS.md convention, and update
the data-persistence tables in README.md and DOCKER_HUB.md.
2026-04-23 09:17:39 +02:00
joakimp b9657415c4 Bump opencode to 1.14.21
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
2026-04-23 09:04:44 +02:00
joakimp b37740bcce Fix incorrect 'Linux unaffected' claim in bind-mount caveat
The previous note scoped the single-file bind-mount staleness bug to
Docker Desktop only. It actually affects ALL platforms including native
Linux: Docker bind-mounts the inode, not the path. Editors that do
atomic save (vim, nvim, VS Code, sed -i) create a new inode via
rename(), leaving the container pinned to the old unlinked one. This
is a kernel limitation (moby/moby#15793, open since 2015, unfixable).

Rewrite both the README.md caveat and the docker-compose.yml inline
note to describe the real mechanism (inode replacement), name the
affected editors, note that append-only writes are safe, and link to
the upstream issue.
2026-04-23 00:27:07 +02:00
joakimp 3982e9f18c Document Docker Desktop single-file bind-mount gotcha
On Docker Desktop (macOS/Windows), single-file bind-mounts can
silently stop propagating host edits — the file gets materialized
onto the VM's ext4 disk and reused forever. This affects anyone who
uncomments the ~/.bash_aliases or ~/.inputrc mount lines.

Add a caveat note in README.md's 'Overriding the defaults / Option A'
section with the verification command and the directory-mount
workaround. Add a matching inline NOTE comment in docker-compose.yml
above the commented mount lines. Linux hosts are unaffected.
2026-04-23 00:25:01 +02:00
joakimp 4d0c270196 Pin project name in default docker-compose.yml
Without an explicit name, Docker Compose derives the project name
from the directory basename. Renaming the directory silently orphans
all named volumes (devbox-data, devbox-state, devbox-shell-history,
etc.) because the new project name no longer matches the old volume
prefixes. Pin to 'opencode-devbox' so volumes survive directory
moves and renames.
2026-04-22 22:41:57 +02:00
joakimp aed5ff106b Add multi-user setup pointer in DOCKER_HUB.md
DOCKER_HUB.md focuses on single-user setup. Rather than duplicating
the multi-user docs, add a short section linking to the source repo's
Multi-user setup section which covers volume isolation, the shared
compose layout, and the SIGNUM / $USER auto-detection.
2026-04-22 21:48:05 +02:00
joakimp 425d53cb57 Update multi-user docs to reflect own-account vs shared-account modes
The shared-machine section in README.md still claimed named volumes
were isolated by directory-name prefixing alone, which was the bug
we just fixed. Rewrite to document both modes (own-account with
automatic $USER fallback, shared-account with explicit SIGNUM) and
explicitly note that the Docker daemon is system-wide — directory
name prefixing is NOT sufficient for volume isolation.
2026-04-22 21:24:59 +02:00
joakimp 60208b2203 Auto-detect username for volume isolation in own-account mode
The previous SIGNUM variable was required (${SIGNUM:?...}), which
broke for users with their own OS accounts who shouldn't need to set
anything manually. Replace with ${SIGNUM:-${USER}} so:

- Own-account mode: leave SIGNUM unset in .env — project name and
  container name default to devbox-$USER automatically. Each OS
  user gets isolated volumes with zero configuration.
- Shared-account mode: set SIGNUM=<id> in .env as before.

Both container_name and the top-level name: field use the same
fallback, so volumes and container names stay consistent.

Updated .env.shared.example to document both modes with the SIGNUM
line commented out by default (own-account is the common case).
2026-04-22 21:21:22 +02:00
joakimp d65f8cc077 Fix volume collision in shared-machine compose: scope project name by SIGNUM
The Docker daemon is system-wide — named volumes are prefixed by the
compose project name, which defaults to the basename of the directory
holding docker-compose.yml. Two users whose compose file lives under
a directory with the same name (e.g. ~/alice/opencode-devbox and
~/bob/opencode-devbox) would silently share volumes, corrupting each
other's opencode data, bash history, and TUI settings.

Add an explicit top-level 'name: devbox-${SIGNUM}' so the project
name (and therefore all volume prefixes) is unique per user. The old
comment claiming directory-name prefixing was sufficient was wrong —
it only works if directory basenames differ, which isn't guaranteed
on multi-user hosts or when users follow the same setup instructions.
2026-04-22 21:17:07 +02:00
joakimp 4560702550 Document the upgrade-ritual for reconciling VM compose files
New releases may add named volumes or bind-mount lines to
docker-compose.yml. The image can't update compose files on the VM —
they're user-owned — so a plain 'docker compose pull && up -d' picks
up the new image but silently misses new mount points.

Example from v1.14.19c → v1.14.20: bash history persistence needs
the devbox-shell-history named volume at /home/developer/.cache/bash.
The v1.14.20 image is configured to write history there either way,
but without the volume mount on the VM, writes land in the container's
writable layer and vanish on every --force-recreate.

Add a 'Upgrading an existing VM to a new release' subsection to
deploy/README.md describing the backup → diff → merge → recreate
ritual, so future upgrades don't quietly drop features the same way.
2026-04-22 10:29:03 +02:00
joakimp c851b4cc8d Clarify tag-letter convention: suffix is build ordinal, 'a' is never used
Publish Docker Image / build-omos (push) Successful in 43m57s
Publish Docker Image / build-base (push) Successful in 45m46s
Publish Docker Image / update-description (push) Successful in 16s
Previous phrasing treated the letter suffix as a plain alphabetical
sequence, which led to confusion about whether the first rebuild
should be 'a' or 'b'. Spell out the intent: the suffix is the build
ordinal, and the letter 'a' is reserved to mean '1st build' — which
always uses the bare tag (no letter). So letters start at 'b' for
the 2nd build, 'c' for the 3rd, and so on.

Examples for opencode version 1.14.20:
  1st build: v1.14.20
  2nd build: v1.14.20b
  3rd build: v1.14.20c
2026-04-21 23:58:12 +02:00
joakimp 9bb93025f0 Fix [devbox] prompt marker disappearing after 'exec bash'
The previous guard used an exported DEVBOX_PS1_SET env var to avoid
double-prefixing on re-source. But env vars survive 'exec bash'
while PS1 does not — a new bash rebuilds PS1 from .bashrc. Result:
the guard saw DEVBOX_PS1_SET=1, skipped the prefix, and the new
shell ran with bare PS1 (no [devbox] marker).

Replace the env-var guard with a substring check on PS1 itself.
If PS1 already contains '[devbox]' we skip, otherwise we prepend.
Correct in all three cases: first shell (PS1 has no marker → add),
exec bash (fresh PS1 has no marker → add), re-source within same
shell (PS1 still has marker → skip, no doubling).
2026-04-21 23:52:03 +02:00
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
20 changed files with 1528 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=
+33
View File
@@ -0,0 +1,33 @@
# ── Shared machine setup ─────────────────────────────────────────────
# SIGNUM isolates your container name and named volumes from other users.
#
# Own-account mode (each user has their own OS login):
# Leave SIGNUM commented out — it defaults to your OS username ($USER).
# SIGNUM=
#
# Shared-account mode (everyone logs in as the same OS user):
# Uncomment and set to your unique identifier.
# 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
+10 -1
View File
@@ -15,7 +15,16 @@ 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.20b`, `v1.14.20c`, … 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 is the build ordinal**, starting at `b` for the second build. The letter `a` is **never used** — think of the suffix as counting rebuilds: `b = 2nd, c = 3rd, d = 4th, …`. For opencode version `1.14.20`: first build `v1.14.20`, second `v1.14.20b`, third `v1.14.20c`, and so on.
- A letter suffix is only used 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
+155
View File
@@ -0,0 +1,155 @@
# Changelog
All notable changes to the opencode-devbox container image.
Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a new opencode release, letter suffix (`b`, `c`, …) for container-level rebuilds on the same version. See [AGENTS.md](AGENTS.md#versioning-scheme) for details.
---
## v1.14.25 — 2026-04-25
Bump opencode to 1.14.25. Also includes container-level changes since v1.14.22b:
- Add `python3-pip` and `python3-venv` to base image (fixes Mason LSP installs).
- Add `devbox-nvim-data` named volume for neovim plugin/Mason persistence.
- Add `devbox-zoxide` named volume for zoxide directory history persistence.
- Bake devbox-shell bridge line into `/etc/skel-devbox/.bash_aliases`.
- Add CHANGELOG.md with full release history.
## v1.14.22b — 2026-04-23
**Fix Mason LSP installs, persist nvim data, devbox-shell bridge.**
- **Fix:** Add `python3-pip` and `python3-venv` to base image. Mason creates a Python venv per LSP package and pip-installs into it; Debian trixie ships python3 without ensurepip, so venv creation failed and every Mason Python package (ruff, ansible-lint) errored on every nvim start.
- **Feature:** Add `devbox-nvim-data` named volume at `~/.local/share/nvim` — Lazy plugin cache and Mason LSP installs now persist across `--force-recreate`.
- **Feature:** Add `devbox-zoxide` named volume at `~/.local/share/zoxide` — zoxide directory history persists across recreates.
- **Feature:** Bake the devbox-shell bridge line into `/etc/skel-devbox/.bash_aliases` — hosts using the `~/.config/devbox-shell/` directory-mount pattern get automatic sourcing without manual setup after recreate.
## v1.14.22 — 2026-04-23
Bump opencode to 1.14.22.
## v1.14.21 — 2026-04-23
**Opencode 1.14.21 + zoxide persistence + multi-user fixes.**
- Bump opencode to 1.14.21.
- Fix single-file bind-mount caveat: document the kernel-level inode issue (affects all platforms, not just Docker Desktop).
- Pin project name in default `docker-compose.yml` — directory renames no longer orphan named volumes.
- Fix volume collision in shared-machine compose: scope project name by `SIGNUM`.
- Auto-detect OS username (`$USER`) for volume isolation in own-account mode.
- Document the upgrade ritual for reconciling VM compose files.
- Add multi-user setup pointer in DOCKER_HUB.md.
## v1.14.20b — 2026-04-21
**Fix `[devbox]` prompt marker lost on `exec bash`.**
- The PS1 prefix guard used an exported env var that survived `exec bash`, but PS1 itself doesn't — so the new shell skipped adding the prefix. Replaced with a substring check on PS1 itself.
- Clarify tag-letter convention in AGENTS.md: suffix is the build ordinal, `a` is never used.
## v1.14.20 — 2026-04-21
**Opencode 1.14.20 + PROMPT_COMMAND/zoxide fix.**
- Bump opencode to 1.14.20.
- Fix `PROMPT_COMMAND` collision with zoxide: `history -a;` followed by zoxide's `;__zoxide_hook` produced `;;` which bash rejected on every prompt. Moved history-flush after zoxide init, using newline separator.
- Includes all v1.14.19c shell-defaults work (baked `.bash_aliases`/`.inputrc` via `/etc/skel-devbox/`, skel-copy on first run, `devbox-shell-history` named volume).
## v1.14.19d — 2026-04-21
*Superseded by v1.14.20 before building. Tagged but never built.*
## v1.14.19c — 2026-04-21
**Bash history persistence, shell defaults, GID auto-detect.**
- **Feature:** Bash history persists across `--force-recreate` via `devbox-shell-history` named volume at `~/.cache/bash`.
- **Feature:** Quality-of-life shell defaults shipped in `/etc/skel-devbox/` and copied to `~/` only if absent: prefix history search on Up/Down, 100k-entry timestamped dedup history, coloured case-insensitive tab completion, eza/bat aliases, zoxide/fzf integrations, `[devbox]` prompt marker.
- **Feature:** Skel-copy pattern — host bind-mounts and in-container customizations are never overwritten on upgrade.
- **Fix:** Entrypoint now detects workspace UID and GID independently. Hosts with UID 1000 but non-1000 GID (e.g. Debian's `useradd` default GID 1001) get correct group remapping.
- **Docs:** SSH banner-timeout troubleshooting (CGNAT), shell defaults section, skel restore/diff commands.
## v1.14.19b — 2026-04-20
**Ownership fixes and config/docs refresh.**
- **Fix:** Root-owned parent dirs left behind by nested named-volume mounts. Entrypoint now chowns `.local`, `.local/share`, `.local/state`, `.config` before leaf mount points.
- **Fix:** `deploy/sync-to-vm.sh` no longer preserves host GIDs (`rsync -a``-rlptDz`).
- Default model IDs refreshed (claude-sonnet-4-6, gpt-5.4, global Bedrock inference profile).
- Documentation gates oh-my-opencode-slim references to the OMOS variant.
## v1.14.19 — 2026-04-20
Bump opencode to 1.14.19.
## v1.14.18 — 2026-04-19
Fix Bun download URL: remove non-existent LATEST file fetch.
## v1.4.17 — 2026-04-19
Bump opencode to v1.4.17, add `file` utility to base image.
## v1.4.12 — 2026-04-18
Bump opencode to v1.4.12.
## v1.4.11 — 2026-04-18
Bump opencode to v1.4.11.
## v1.4.7 — 2026-04-17
Bump opencode to v1.4.7.
## v1.4.6 — 2026-04-15
Bump opencode to v1.4.6.
## v1.4.3k — 2026-04-13
Fix Bedrock config: add `AWS_PROFILE` to generated config, add `.agents/skills` to volume ownership fix.
## v1.4.3j — 2026-04-13
Upgrade base image from Debian bookworm to trixie (current stable). Bookworm EOL June 2026; trixie supported until 2028/LTS 2030.
## v1.4.3i — 2026-04-12
Add rustup for on-demand Rust support, document JS/TS development.
## v1.4.3h — 2026-04-12
Add uv package manager to base image for on-demand Python support.
## v1.4.3g — 2026-04-12
Fix IPv6 connectivity failures: force IPv4 preference in CI builds.
## v1.4.3f — 2026-04-11
Add error handling to Docker Hub description update step.
## v1.4.3e — 2026-04-10
Fix CVEs: install git-lfs from GitHub (Go 1.25), document Go versions for gosu/fzf.
## v1.4.3d — 2026-04-10
Fix CVEs: install gosu 1.19 and fzf 0.71.0 from GitHub releases instead of Debian packages.
## v1.4.3c — 2026-04-10
Fix CVEs: install gosu from GitHub release instead of Debian package (Go 1.19.8 → current).
## v1.4.3b — 2026-04-10
Fix entrypoint crash on read-only SSH mount.
## v1.4.3 — 2026-04-10
Bump opencode to 1.4.3.
## v1.4.2 — 2026-04-10
Initial release. Fix CI: use vars for username, secrets for token.
+50 -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,30 @@ 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/zoxide` | Named volume `devbox-zoxide` | ✅ Yes — Docker volume | Zoxide directory history (`z <fragment>` jump targets) |
| `/home/developer/.local/share/nvim` | Named volume `devbox-nvim-data` | ✅ Yes — Docker volume | Neovim plugins, Mason LSP installs, Lazy plugin cache |
| `/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 +260,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 +408,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 +419,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 +437,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 +467,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 +488,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`)
@@ -515,6 +549,12 @@ ping all agents
All six agents should respond if your provider authentication is working. All six agents should respond if your provider authentication is working.
## Multi-User Setup
This guide covers single-user setup. For running multiple opencode-devbox instances in parallel — whether each user has their own OS account or everyone shares one login — see the [Multi-user setup section](https://gitea.jordbo.se/joakimp/opencode-devbox#multi-user-setup) in the source repository. It covers volume isolation, the `docker-compose.shared.yml` layout, and the `SIGNUM` / `$USER` auto-detection mechanism.
## Source ## Source
Build from source or contribute: [opencode-devbox on Gitea](https://gitea.jordbo.se/joakimp/opencode-devbox) Build from source or contribute: [opencode-devbox on Gitea](https://gitea.jordbo.se/joakimp/opencode-devbox)
See the [Changelog](https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/CHANGELOG.md) for a full release history.
+41 -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.25
LABEL maintainer="joakimp" LABEL maintainer="joakimp"
LABEL description="Portable opencode developer container" LABEL description="Portable opencode developer container"
@@ -32,10 +32,18 @@ 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 \
python3-pip \
python3-venv \
&& 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 +79,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 +99,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 +116,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 +157,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 +168,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 +202,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
+95 -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,45 @@ volumes:
- devbox-vscode:/home/developer/.vscode-server - devbox-vscode:/home/developer/.vscode-server
``` ```
### Multi-user setup
The shared-machine compose file (`docker-compose.shared.yml`) supports two modes:
**Own-account mode** (each user has their own OS login — the common case):
Leave `SIGNUM` unset in `.env`. The project name defaults to `devbox-$USER`, so each OS user automatically gets isolated container names and named volumes with zero configuration.
**Shared-account mode** (everyone logs in as the same OS user, e.g. `garage`):
Each user sets `SIGNUM=<unique-id>` in `.env` to get isolation.
Setup per user:
```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 — set SIGNUM only if you're in shared-account mode
vim .env
# Start
docker compose up -d
docker compose exec -u developer devbox opencode
```
Each user's container, config, and named volumes are fully isolated:
- Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode)
- Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for 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 +444,51 @@ 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
```
> **Single-file bind-mount caveat (all platforms):** Docker bind-mounts the file's **inode**, not its path. When editors like vim, nvim, VS Code, or `sed -i` save a file, they write to a temp file and `rename()` it over the original — creating a new inode. The container stays pinned to the old (now unlinked) inode and never sees the update. This is a kernel limitation ([Docker #15793](https://github.com/moby/moby/issues/15793)), not fixable by Docker. Append-only writes (`echo "alias foo=bar" >> file`) are safe because they modify the same inode. **Workaround:** mount the parent directory instead of the single file (e.g. `~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro`) and source files from there.
**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 +530,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 +546,15 @@ 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/zoxide` | Named volume `devbox-zoxide` | ✅ Yes | Zoxide directory history (`z <fragment>` jump targets) |
| `/home/developer/.local/share/nvim` | Named volume `devbox-nvim-data` | ✅ Yes | Neovim plugins, Mason LSP installs, Lazy plugin cache |
| `/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 ""
+318
View File
@@ -0,0 +1,318 @@
# 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.
### Upgrading an existing VM to a new release
Each tagged release may add new named volumes or bind-mount lines to `docker-compose.yml`. Pulling a new image via `docker compose pull` grabs the new container behaviour, but compose files on the VM are user-owned and never touched by the image — you have to reconcile them yourself when upgrading across versions.
**Symptom of a missed reconcile:** a new feature quietly doesn't work even though the image is correct. Example from v1.14.19c → v1.14.20: bash history persistence requires the `devbox-shell-history` named volume mounted at `/home/developer/.cache/bash`. The v1.14.20 image writes history to that path either way, but without the volume mount on the VM, writes land in the container's writable layer and vanish on every `--force-recreate`.
**Upgrade ritual:**
```bash
# On the VM, before recreating the container:
cd ~/opencode-devbox
cp docker-compose.yml docker-compose.yml.bak-$(date +%Y%m%d-%H%M%S)
# Compare against the repo version to see what's new:
# (from your local checkout)
scp devbox-affection:~/opencode-devbox/docker-compose.yml /tmp/vm-compose.yml
diff -u /tmp/vm-compose.yml ~/src/src_local/opencode-devbox/docker-compose.yml
```
For each new `volumes:` entry or mount line in the repo version that isn't in your VM's file, add it manually — preserving any local customizations you've made (image variant, read/write flags on bind mounts, etc.). Then:
```bash
docker compose config >/dev/null # verify YAML still parses
docker compose up -d --force-recreate
```
If you maintain the VM's compose file with no local changes, `scp` the repo version over wholesale. If you have customizations (the common case), do the diff-and-merge by hand.
### 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`.
### CI runner maintenance: automatic Docker pruning
Gitea Actions runners accumulate Docker build cache, stale buildkit containers, and unused images over time. Without periodic cleanup, the runner's disk fills up and builds stall during the image-push phase (symptom: `#61 exporting to image` / `pushing layers` hangs indefinitely while buildkit repeatedly re-authenticates with Docker Hub).
Set up two layers of automatic cleanup on the runner host:
**1. Daily cron job** — prunes images, containers, and build cache older than 72 hours:
```bash
sudo tee /etc/cron.daily/docker-prune <<'EOF'
#!/bin/sh
docker system prune -af --filter "until=72h" > /var/log/docker-prune.log 2>&1
docker builder prune -af --filter "until=72h" >> /var/log/docker-prune.log 2>&1
EOF
sudo chmod +x /etc/cron.daily/docker-prune
```
**2. Docker daemon builder GC** — caps buildkit cache at 10 GB (Docker 23.0+):
Add to `/etc/docker/daemon.json` (create if absent):
```json
{
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB"
}
}
}
```
Then `sudo systemctl restart docker`.
Both are safe to run on a machine that also hosts long-running containers (like opencode-devbox) — `docker system prune` only removes *unused* images and *stopped* containers, never running ones.
### 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
+72
View File
@@ -0,0 +1,72 @@
# 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
#
# Volume isolation: the top-level 'name:' field derives a unique project
# name per user, which Docker Compose uses as the prefix for all named
# volumes. Without this, two users whose compose file lives in a directory
# with the same basename would share volumes — the Docker daemon is
# system-wide and doesn't scope by OS user.
#
# Two modes:
# Own-account mode (each user has their own OS login):
# Leave SIGNUM unset in .env — it defaults to $USER automatically.
# Shared-account mode (everyone logs in as the same OS user):
# Set SIGNUM=<unique-id> in .env so each person gets isolated volumes.
name: devbox-${SIGNUM:-${USER}}
services:
devbox:
image: joakimp/opencode-devbox:latest
container_name: devbox-${SIGNUM:-${USER}}
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 zoxide directory history ('z <fragment>' to jump)
- devbox-zoxide:/home/developer/.local/share/zoxide
# Persist neovim plugin/Mason data (avoids re-downloading on every recreate)
- devbox-nvim-data:/home/developer/.local/share/nvim
# 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-zoxide:
devbox-nvim-data:
devbox-uv:
+46 -7
View File
@@ -8,15 +8,24 @@
# Or for interactive one-shot: # Or for interactive one-shot:
# docker compose run --rm devbox # docker compose run --rm devbox
# Pin the project name so named volumes survive directory renames.
# Without this, Docker Compose derives the project name from the
# directory basename — renaming the dir orphans all existing volumes.
name: opencode-devbox
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 +54,29 @@ 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
# Persist zoxide directory history ('z <fragment>' to jump).
- devbox-zoxide:/home/developer/.local/share/zoxide
# 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:
#
# NOTE: Single-file bind-mounts break when editors use atomic save
# (vim, VS Code, sed -i write a temp file then rename() over the
# original, creating a new inode the container never sees). This is a
# kernel limitation, not Docker-specific. If host edits stop appearing
# in the container, mount the parent directory instead — see the
# "Shell defaults" section in README.md.
# - ~/.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
@@ -57,11 +89,18 @@ services:
# Optional: persist VS Code server and extensions across container recreations # Optional: persist VS Code server and extensions across container recreations
# - devbox-vscode:/home/developer/.vscode-server # - devbox-vscode:/home/developer/.vscode-server
# Persist neovim plugin/Mason data (avoids re-downloading on every recreate)
- devbox-nvim-data:/home/developer/.local/share/nvim
# Optional: AWS credentials/SSO config (not read-only — SSO writes token cache) # Optional: AWS credentials/SSO config (not read-only — SSO writes token cache)
# - ~/.aws:/home/developer/.aws # - ~/.aws:/home/developer/.aws
volumes: volumes:
devbox-data: devbox-data:
devbox-state:
devbox-shell-history:
devbox-zoxide:
devbox-nvim-data:
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
} }
+35 -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,30 @@ 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"/.local/share/zoxide \
/home/"$USER_NAME"/.local/share/nvim \
/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 \
+94
View File
@@ -0,0 +1,94 @@
# 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.
# ── Host-shared shell customizations (devbox-shell bridge) ───────────
# If the host bind-mounts a directory at ~/.config/devbox-shell/ (the
# recommended pattern for sharing aliases/PATH/utilities between host
# and container), source the bash_aliases file from it. This survives
# --force-recreate because it's baked into the image's skel, not the
# container's writable layer. Hosts that don't use this pattern are
# unaffected — the test silently skips if the file doesn't exist.
[ -r "$HOME/.config/devbox-shell/bash_aliases" ] && . "$HOME/.config/devbox-shell/bash_aliases"
# ── 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.
# We check for the literal '[devbox]' substring in PS1 rather than relying on
# an exported guard variable — otherwise `exec bash` inherits the guard but
# gets a fresh (prefix-less) PS1 from .bashrc, and the prefix would never be
# re-added in the new shell.
if [ -n "${PS1:-}" ] && [[ "$PS1" != *"[devbox]"* ]]; then
PS1='\[\e[38;5;39m\][devbox]\[\e[0m\] '"${PS1}"
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