Compare commits

...

10 Commits

Author SHA1 Message Date
Ian 1be1b4f606 chore: remove java 17 as default version since is redundant 2026-10-05 23:56:35 +02:00
Ian 1e6787eb30 fix: surface podman errors instead of aborting silently
`set -e` aborted build.sh when `podman image rm`/`prune` failed, and
`2>/dev/null` hid the reason, so --remove exited 125 with no output.
Combining --remove --prune also skipped the prune.

Guard the removal with `podman image exists` for a clear "nothing to
remove", drop `2>/dev/null` so podman's reason is visible, and use
`cmd || echo` to warn without aborting. Exit status stays 0 on failure
by design.
2026-10-05 23:43:18 +02:00
Ian 050e9c0205 fix: exit code 2026-10-05 23:38:19 +02:00
Ian 6502b9a2ab fix: certs readme.md 2026-10-05 23:35:21 +02:00
Ian 688d4ca6d7 chore: coherent exit code on run.sh 2026-10-05 23:26:18 +02:00
Ian f284cb118f fix: img name 2026-10-05 23:25:10 +02:00
Ian 64cc8cee13 chore: add image 2026-10-05 23:24:29 +02:00
Ian b168025ba1 feat: container name can be specified when run 2026-10-05 09:02:39 +02:00
Ian 92e09fe29c chore: update README.md 2026-10-05 08:56:27 +02:00
Ian 754fa84a5e fix: remove unused variables and ignore certs/README.md when build image 2026-10-05 08:50:12 +02:00
7 changed files with 102 additions and 86 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
.git
.gitignore
README.md
*.log
README.md
certs/README.md
+1 -3
View File
@@ -4,7 +4,7 @@ ARG UBUNTU_VERSION=26.04
FROM ubuntu:${UBUNTU_VERSION}
ARG NODE_VERSION=24.21.0
ARG OPENCODE_VERSION=2.0.3
ARG OPENCODE_VERSION=2.0.22
ARG EXTRA_APT_PACKAGES=""
@@ -49,8 +49,6 @@ RUN apt-get update \
wget \
xz-utils \
zip \
&& update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java \
&& update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac \
&& echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \
&& locale-gen \
&& ln -s "$(command -v fdfind)" /usr/local/bin/fd
+72 -57
View File
@@ -1,5 +1,7 @@
# OpenCode Development Container
![container-world](./readme.img.jpg)
This repository provides a Podman-based development container for running an AI coding agent in an isolated and reproducible environment.
The container includes common software development tools, Node.js, OpenCode, Python, Java, Maven, Git, Neovim, and other command-line utilities.
@@ -17,9 +19,9 @@ The host project directory and selected development caches/configuration directo
## Files
* `Dockerfile` — builds the development image.
* `build.sh` — builds or updates the image.
* `dev.sh` — starts the development container.
* `entrypoint.sh` — initializes the runtime environment.
* `build.sh` — builds, updates, removes, or prunes the image.
* `run.sh` — starts the development container.
* `certs/README.md` — rules for custom CA certificates.
## Build the image
@@ -49,12 +51,37 @@ The build uses the current host UID and GID for the `dev` user, so files created
./build.sh --update
```
This is equivalent to `--no-cache --pull=newer`, so the Ubuntu base image is refreshed and every package layer is reinstalled.
### Show the build command without running it
```bash
./build.sh --dry-run
```
### Remove or prune images
```bash
./build.sh --remove # delete the development image
./build.sh --prune # delete dangling images left by previous builds
```
Both options exit immediately without building, and can be combined.
### Install extra system packages
```bash
./build.sh --apt-packages="libpq-dev postgresql-client"
```
The list is forwarded to the image build as `EXTRA_APT_PACKAGES` and installed with `apt-get install --no-install-recommends`.
### Show usage
```bash
./build.sh --help
```
## Build arguments
The following environment variables can be used to customize the image:
@@ -62,21 +89,28 @@ The following environment variables can be used to customize the image:
```bash
UBUNTU_VERSION=26.04
NODE_VERSION=24.21.0
OPENCODE_VERSION=2.0.3
OPENCODE_VERSION=2.0.22
IMAGE_REPO=localhost/opencode-dev
```
For example:
```bash
NODE_VERSION=24.21.0 OPENCODE_VERSION=2.0.3 ./build.sh --no-cache
NODE_VERSION=24.21.0 OPENCODE_VERSION=2.0.22 ./build.sh --no-cache
```
`build.sh` passes the versions above to `podman build` as `--build-arg`. The same values are declared as `ARG` defaults at the top of the `Dockerfile`, so keep both in sync when changing a version.
Two more build arguments are set automatically:
* `DEV_UID` / `DEV_GID` — taken from `id -u` / `id -g` on the host.
* `EXTRA_APT_PACKAGES` — taken from the `--apt-packages` option.
## Custom CA certificates
Custom CA certificates must be placed in the repository `certs/` directory before building the image. See [`certs/README.md`](certs/README.md) for accepted formats and rules.
This directory is kept in git through a placeholder file, so a fresh clone builds without creating it first.
Git cannot track empty directories, so `certs/README.md` itself is the placeholder that keeps `certs/` present in a fresh clone, and the build does not fail on a missing directory. That placeholder is listed in `.dockerignore`, so only real certificate files are copied into the image.
The Dockerfile copies this directory to:
@@ -105,7 +139,7 @@ This allows both system tools and Node.js applications to trust the additional c
Run:
```bash
./dev.sh
./run.sh
```
By default, the host directory is:
@@ -120,21 +154,9 @@ and projects are stored in:
$HOME/opencode-dev/projects
```
The container starts in `/workspace`.
`run.sh` creates every directory it mounts, so it can be run on a clean checkout. If the image does not exist yet, the script exits with an error asking you to build it first.
To start in a specific project:
```bash
./dev.sh my-project
```
or:
```bash
./dev.sh /path/to/opencode-dev/projects/my-project
```
Absolute paths outside the projects directory are not accepted as the initial working directory.
The container starts in `/workspace`, which is the mounted `projects/` directory. Open the project you want to work on inside the shell, or mount additional host directories with `--mount`.
## Mounted directories
@@ -149,10 +171,9 @@ The following host directories are mounted into the container:
| `m2/` | `/home/dev/.m2` | Maven cache |
| `npm-cache/` | `/home/dev/.npm` | npm cache |
| `pip-cache/` | `/home/dev/.cache/pip` | pip cache |
| `bash/` | `/host-bash` | Host-provided Bash configuration |
| `$HOME/.gitconfig` | `/home/dev/.gitconfig` | Git configuration |
All standard development directories are created automatically by `dev.sh`.
All mounted development directories are created automatically by `run.sh`.
## Additional mounts
@@ -161,23 +182,25 @@ Additional host directories can be mounted with `--mount`.
Example:
```bash
./dev.sh my-project --mount "$HOME/other:/extra"
./run.sh --mount "$HOME/other:/extra"
```
Read-only mounts are also supported:
```bash
./dev.sh my-project --mount "$HOME/data:/data:ro"
./run.sh --mount "$HOME/data:/data:ro"
```
Multiple mounts can be specified:
```bash
./dev.sh my-project \
./run.sh \
--mount "$HOME/data:/data:ro" \
--mount "$HOME/tools:/tools:rw"
```
`--mount` is the only option accepted by `run.sh`; any other argument stops the script with an error.
## Environment variables
The main runtime paths can be customized:
@@ -191,14 +214,15 @@ OPENCODE_CACHE
M2_FOLDER
NPM_CACHE
PIP_CACHE
BASH_FOLDER
IMAGE_REPO
```
`IMAGE_REPO` is read by both scripts, so it selects the same image for `build.sh` and `run.sh`.
Example:
```bash
OPENCODE_DEV_HOME="$HOME/my-opencode" ./dev.sh
OPENCODE_DEV_HOME="$HOME/my-opencode" ./run.sh
```
## Git configuration
@@ -211,38 +235,35 @@ $HOME/.gitconfig -> /home/dev/.gitconfig
If the file does not exist, the script prints a warning and Git uses the container defaults.
SSH configuration and SSH agent forwarding are currently disabled in the script. They can be enabled by uncommenting the corresponding sections in `dev.sh`.
SSH configuration and SSH agent forwarding are not mounted. If they are needed, pass them explicitly:
```bash
./run.sh --mount "$HOME/.ssh:/home/dev/.ssh:ro"
```
## Security
The container runs with several restrictions:
* Runs as the `dev` user.
* Uses the host UID/GID.
* Drops all Linux capabilities.
* Runs as the `dev` user with host UID/GID (`--user dev --userns=keep-id`).
* Drops all Linux capabilities (`--cap-drop=ALL`).
* Uses `no-new-privileges`.
* Uses a temporary container (`--rm`).
* Uses the host timezone (`--tz=local`).
* Uses a temporary, named container (`--name opencode-dev --rm`).
* Does not run as root.
* Uses the default Podman network.
The project directory is mounted read/write because the AI agent needs to create and modify source files.
The project directory is mounted read/write because the AI agent needs to create and modify source files. The host Git configuration is mounted read-only.
## Locale
The host locale is passed to the container through:
The image generates the `en_US.UTF-8` locale during the build:
```text
HOST_LANG
HOST_LC_ALL
```dockerfile
echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
```
The entrypoint generates the requested UTF-8 locale when necessary.
For example, the host locale is automatically passed when running:
```bash
./dev.sh
```
The host timezone is applied at runtime through `--tz=local`. The host `LANG`/`LC_ALL` values are not forwarded; export them inside the container if a different locale is needed.
## Typical workflow
@@ -255,22 +276,16 @@ Build the image:
Start the development environment:
```bash
./dev.sh
./run.sh
```
Or start directly in a project:
```bash
./dev.sh my-project
```
Inside the container, the project is available under:
Inside the container, the projects directory is available under:
```bash
/workspace
```
OpenCode can then be started normally:
Move into the project you want to work on, then start OpenCode normally:
```bash
opencode
@@ -280,14 +295,14 @@ The container is removed when the shell exits, while source code, OpenCode data,
## Customization
Most image versions are defined at the top of the `Dockerfile`:
Image versions are defined at the top of the `Dockerfile`:
```dockerfile
ARG UBUNTU_VERSION=26.04
ARG NODE_VERSION=24.21.0
ARG OPENCODE_VERSION=2.0.3
ARG OPENCODE_VERSION=2.0.22
```
Runtime paths and container options are defined in `dev.sh`.
Runtime paths, mounts, and container options are defined in `run.sh`, while `build.sh` controls how the image is built and maintained.
This separation allows the image to contain the stable development environment while projects, configuration, caches, and other working data remain outside the container.
+8 -4
View File
@@ -11,7 +11,7 @@ IMAGE="${IMAGE_REPO:-localhost/opencode-dev}"
usage() {
echo 'Usage: $0 [--no-cache] [--update] [--dry-run] [--remove] [--prune] [--apt-packages="pack1 pack2"]'
exit 2
exit "${1:-0}"
}
BUILD_OPTS=()
@@ -31,7 +31,7 @@ while (($#)); do
read -ra _pkgs <<< "${1#*=}"
EXTRA_APT_PACKAGES="${_pkgs[@]}" ;;
-h|--help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;;
*) echo "Unknown option: $1" >&2; usage 2 ;;
esac
shift
done
@@ -42,10 +42,14 @@ fi
if ((REMOVE || PRUNE)); then
if ((REMOVE)); then
podman image rm "${IMAGE}" 2>/dev/null
if podman image exists "$IMAGE"; then
podman image rm "${IMAGE}" || echo "WARNING: could not remove $IMAGE" >&2
else
echo "$IMAGE does not exist, nothing to remove"
fi
fi
if ((PRUNE)); then
podman image prune -f 2>/dev/null
podman image prune -f || echo "WARNING: prune failed" >&2
fi
exit 0
fi
+16 -10
View File
@@ -1,11 +1,10 @@
# Custom CA certificates
Place any **additional CA certificates** needed to reach internal HTTPS services
in this directory, as PEM files with a `.crt` or `.pem` extension:
in this directory, as PEM files with a `.crt` extension:
```text
certs/
├── .gitignore
├── README.md
├── company-root.crt
└── company-intermediate.crt
@@ -50,9 +49,10 @@ so both system tools and Node.js applications trust the added certificates.
* Only **public** CA certificates belong here. Never commit private material
(`*.key`, `*.p12`, `*.pfx`) — everything matched here is copied into the image
verbatim. The `.gitignore` in this directory blocks those extensions, but it
is a safety net, not a guarantee.
* Files must be PEM encoded. Convert DER with:
verbatim.
* Files must be PEM encoded and use the `.crt` extension. Any other extension
(`.pem`, `.cer`, …) is **silently ignored** by `update-ca-certificates`, so
rename it to `.crt`. Convert DER with:
```bash
openssl x509 -inform der -in cert.der -out cert.crt
@@ -60,13 +60,19 @@ so both system tools and Node.js applications trust the added certificates.
## Troubleshooting
If a certificate seems ignored, check that it parses and that the build picked
it up:
If a certificate seems ignored, the most likely cause is the extension: only
files ending in `.crt` are installed. A `.pem` file produces no error and no
warning, it is simply skipped. Rename it:
```bash
mv company-root.pem company-root.crt
```
Otherwise check that the file parses as a certificate:
```bash
openssl x509 -in company-root.crt -noout -subject -issuer -dates
```
A build that prints warnings from `update-ca-certificates` usually means a file
in this directory is not a valid PEM certificate. The permissive `.crt`/`.pem`
extensions mean a stray text file or archive will be picked up and rejected.
A build that prints warnings from `update-ca-certificates` means a `.crt` file
is present but is not valid PEM certificate data.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

+3 -11
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")")
IMAGE_REPO="${IMAGE_REPO:-localhost/opencode-dev}"
IMAGE="${IMAGE_REPO}"
CONTAINER_NAME="${CONTAINER_NAME:-opencode-dev}"
if ! podman image exists "$IMAGE"; then
echo "Image $IMAGE does not exist. Build it first with ./build.sh" >&2
@@ -18,15 +17,9 @@ OPC_CONFIG="${OPENCODE_CONFIG:-$BASE_DIR/opencode-config}"
OPC_DATA="${OPENCODE_SHARE_DATA:-$BASE_DIR/opencode-data}"
OPC_CACHE="${OPENCODE_CACHE:-$BASE_DIR/opencode-cache}"
M2_FOLDER="${M2_FOLDER:-$BASE_DIR/m2}"
NPM_MODULES="${NPM_MODULES:-$BASE_DIR/npm-modules}"
NPM_CACHE="${NPM_CACHE:-$BASE_DIR/npm-cache}"
PIP_CACHE="${PIP_CACHE:-$BASE_DIR/pip-cache}"
if [[ ! -d "$PROJECTS_DIR" ]]; then
echo "Project directory missing: $PROJECTS_DIR" >&2
exit 1
fi
mkdir -p \
"$BASE_DIR" \
"$PROJECTS_DIR" \
@@ -34,13 +27,12 @@ mkdir -p \
"$OPC_DATA" \
"$OPC_CACHE" \
"$M2_FOLDER" \
"$NPM_MODULES" \
"$NPM_CACHE" \
"$PIP_CACHE"
ARGS=(
run --rm -it
--name opencode-dev
--name "${CONTAINER_NAME}"
--user dev
--userns=keep-id
--network=default
@@ -65,7 +57,7 @@ else
fi
# Additional bind mounts:
# ./dev.sh --mount "$HOME/other:/extra"
# ./run.sh --mount "$HOME/other:/extra"
while (($#)); do
case "$1" in
--mount)