Compare commits

...

17 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
Ian 3bb88ec54e fix: remove unused env variable 2026-10-05 08:40:13 +02:00
Ian feb7d4c9f8 fix: add certs folder 2026-10-04 17:21:17 +02:00
Ian a65f2983f1 chore: add gitignore file 2026-10-04 17:20:39 +02:00
Ian 35dcc67e6a feat: various modifications
- Default ubuntu user is removed
- Can add apt packages during image building with --apt-packages="list
  of pkg"
- Remove and/or prune podman image option
- Fix condition checks
2026-10-04 15:42:03 +02:00
Ian e95d6fa3bd ref: refactored scripts 2026-10-03 22:54:34 +02:00
Ian 5f5db08ad4 chore: new README file 2026-10-01 23:23:01 +02:00
Ian 7b98d6dae6 fix: reduce permissions and fix npm and opencode versions 2026-10-01 23:22:33 +02:00
10 changed files with 498 additions and 322 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
.git .git
.gitignore .gitignore
README.md
*.log *.log
README.md
certs/README.md
+1
View File
@@ -0,0 +1 @@
.idea
+27 -36
View File
@@ -4,14 +4,13 @@ ARG UBUNTU_VERSION=26.04
FROM ubuntu:${UBUNTU_VERSION} FROM ubuntu:${UBUNTU_VERSION}
ARG NODE_VERSION=24.21.0 ARG NODE_VERSION=24.21.0
ARG OPENCODE_VERSION=2.0.22
ARG EXTRA_APT_PACKAGES=""
ARG DEV_UID=1000 ARG DEV_UID=1000
ARG DEV_GID=1000 ARG DEV_GID=1000
# Move the pre-existing ubuntu user/group away from 1000:1000
RUN groupmod --gid 1234 ubuntu \
&& usermod --uid 1234 --gid 1234 ubuntu
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@@ -43,7 +42,6 @@ RUN apt-get update \
python3-pip \ python3-pip \
python3-venv \ python3-venv \
ripgrep \ ripgrep \
sudo \
tar \ tar \
tree \ tree \
unzip \ unzip \
@@ -51,41 +49,41 @@ RUN apt-get update \
wget \ wget \
xz-utils \ xz-utils \
zip \ 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 \
# && rm -rf /var/lib/apt/lists/*
&& echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \ && echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \
&& locale-gen && locale-gen \
&& ln -s "$(command -v fdfind)" /usr/local/bin/fd
RUN if [ -n "${EXTRA_APT_PACKAGES}" ]; then \
echo "Installing extra packages: ${EXTRA_APT_PACKAGES}" \
&& apt-get install -y --no-install-recommends ${EXTRA_APT_PACKAGES}; \
fi
RUN rm -rf /var/lib/apt/lists/*
COPY ./certs /usr/local/share/ca-certificates COPY ./certs /usr/local/share/ca-certificates
RUN update-ca-certificates RUN update-ca-certificates
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Runtime user # Remove ubuntu user and create the dev one with host uid and gid
RUN groupadd --gid "${DEV_GID}" dev \ RUN (userdel -r ubuntu 2>/dev/null || true) \
&& useradd \ && (groupdel ubuntu 2>/dev/null || true) \
--uid "${DEV_UID}" \ && groupadd --gid "${DEV_GID}" dev \
--gid "${DEV_GID}" \ && useradd --uid "${DEV_UID}" --gid "${DEV_GID}" --create-home --shell /bin/bash dev
--create-home \
--shell /bin/bash dev \
&& echo 'dev ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/dev \
&& chmod 0440 /etc/sudoers.d/dev
# Node.js + OpenCode # Node.js + OpenCode
RUN curl -fsSLO \ RUN f="node-v${NODE_VERSION}-linux-x64.tar.xz" \
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \ && curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/${f}" \
&& tar -xJf \ && curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" \
"node-v${NODE_VERSION}-linux-x64.tar.xz" \ && grep " ${f}\$" SHASUMS256.txt | sha256sum -c - \
-C /usr/local \ && tar -xJf "node-v${NODE_VERSION}-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
--strip-components=1 \ && rm -f "${f}" SHASUMS256.txt \
&& rm "node-v${NODE_VERSION}-linux-x64.tar.xz" \
&& echo -n "node version: " && node --version \ && echo -n "node version: " && node --version \
&& echo -n "npm version: " && npm --version \ && echo -n "npm version: " && npm --version \
&& npm install -g @opencode/cli \ && npm install -g "@opencode/cli@${OPENCODE_VERSION}" \
&& echo -n "opencode version: " && opencode --version \ && echo -n "opencode version: " && opencode --version \
&& npm cache clean --force && npm cache clean --force
# User directories # User directories and dot expansion
RUN mkdir -p \ RUN mkdir -p \
/workspace \ /workspace \
/home/dev/.config/opencode \ /home/dev/.config/opencode \
@@ -94,11 +92,8 @@ RUN mkdir -p \
/home/dev/.cache/pip \ /home/dev/.cache/pip \
/home/dev/.npm \ /home/dev/.npm \
/home/dev/.m2 \ /home/dev/.m2 \
&& chown -R dev:dev \ && chown -R dev:dev /workspace /home/dev \
/workspace \ && printf '%s\n' 'shopt -s dotglob' >> /etc/bash.bashrc
/home/dev
RUN printf '%s\n' 'shopt -s dotglob' >> /etc/bash.bashrc
# Runtime # Runtime
USER dev USER dev
@@ -109,10 +104,6 @@ ENV HOME=/home/dev \
PIP_CACHE_DIR=/home/dev/.cache/pip \ PIP_CACHE_DIR=/home/dev/.cache/pip \
NPM_CONFIG_CACHE=/home/dev/.npm NPM_CONFIG_CACHE=/home/dev/.npm
RUN echo 'for f in /host-bash/*; do [ -r "$f" ] && source "$f"; done' >> ~/.bashrc
WORKDIR /workspace WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/opencode-dev-entrypoint"]
CMD ["bash"] CMD ["bash"]
+254 -71
View File
@@ -1,125 +1,308 @@
# OpenCode Podman Dev Environment # OpenCode Development Container
Podman-based Ubuntu 26.04 development container for OpenCode. ![container-world](./readme.img.jpg)
## Decisions This repository provides a Podman-based development container for running an AI coding agent in an isolated and reproducible environment.
- Ubuntu 26.04, amd64/x86_64 The container includes common software development tools, Node.js, OpenCode, Python, Java, Maven, Git, Neovim, and other command-line utilities.
- OpenCode pinned to 1.18.32
- Node.js 24.21.0 LTS, official Linux x64 binary
- OpenJDK 11 + 17, Java 17 default
- Python 3 from Ubuntu + pip + venv
- Maven, no Gradle
- Neovim installed with no custom configuration
- non-root `dev` user
- rootless Podman with `--userns=keep-id`
- no Podman/Docker socket
- no proxy configuration
- SSH only for Git; host `.ssh` is read-only and host SSH agent is forwarded when available
- host timezone and locale
- custom CA certificates are host-mounted read-only and installed at runtime; invalid certs warn and do not stop startup
- OpenCode config/data/cache and development caches persist on the host
- Bash history is ephemeral
## Host data The host project directory and selected development caches/configuration directories are mounted into the container.
The scripts create automatically: ## Requirements
```text * Linux
~/dev/opencode-dev/ * Podman
├── opencode-config/ * Bash
├── opencode-data/ * A working internet connection when building the image
├── opencode-cache/ * CA certificates, if access to internal HTTPS services requires custom CAs
├── m2/
├── npm-cache/
├── pip-cache/
├── certs/
├── bash/
└── projects/
```
OpenCode stores provider credentials in `~/.local/share/opencode/auth.json`, which is therefore persisted by the `opencode-data` mount. ## Files
## Build * `Dockerfile` — builds the development image.
* `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
Run:
```bash ```bash
chmod +x build.sh dev.sh opencode.sh
./build.sh ./build.sh
``` ```
Clean rebuild: The default image is:
```text
localhost/opencode-dev
```
The build uses the current host UID and GID for the `dev` user, so files created in mounted directories normally belong to the host user.
### Build without cache
```bash ```bash
./build.sh --no-cache ./build.sh --no-cache
``` ```
Intentional update rebuild: ### Update the base image and Ubuntu packages
```bash ```bash
./build.sh --update ./build.sh --update
``` ```
## Shell 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 ```bash
./dev.sh ./build.sh --dry-run
``` ```
Start in a project: ### Remove or prune images
```bash ```bash
./dev.sh ~/dev/opencode-dev/projects/my-project ./build.sh --remove # delete the development image
./build.sh --prune # delete dangling images left by previous builds
``` ```
The same `projects` tree is mounted as `/workspace`. Both options exit immediately without building, and can be combined.
Optional additional mount: ### Install extra system packages
```bash ```bash
./dev.sh ~/dev/opencode-dev/projects/my-project \ ./build.sh --apt-packages="libpq-dev postgresql-client"
--mount "$HOME/another-project:/extra"
``` ```
Put custom Bash settings in: 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:
```bash
UBUNTU_VERSION=26.04
NODE_VERSION=24.21.0
OPENCODE_VERSION=2.0.22
IMAGE_REPO=localhost/opencode-dev
```
For example:
```bash
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.
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:
```text ```text
~/dev/opencode-dev/bash/bashrc /usr/local/share/ca-certificates
``` ```
They are loaded after the image's normal Bash environment. History remains ephemeral. and runs:
## OpenCode
```bash ```bash
./opencode.sh update-ca-certificates
``` ```
Or start in a project: The resulting system CA bundle is also exposed through:
```text
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
```
This allows both system tools and Node.js applications to trust the additional certificates.
**Important:** CA certificates need to be present when the image is built. The runtime container does not install certificates from the host. After adding or removing a certificate, rebuild with `./build.sh --no-cache --update`.
## Run the container
Run:
```bash ```bash
./opencode.sh ~/dev/opencode-dev/projects/my-project ./run.sh
``` ```
Arguments are forwarded to OpenCode: By default, the host directory is:
```text
$HOME/opencode-dev
```
and projects are stored in:
```text
$HOME/opencode-dev/projects
```
`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.
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
The following host directories are mounted into the container:
| Host | Container | Purpose |
| ------------------ | --------------------------------- | -------------------------------- |
| `projects/` | `/workspace` | Source code |
| `opencode-config/` | `/home/dev/.config/opencode` | OpenCode configuration |
| `opencode-data/` | `/home/dev/.local/share/opencode` | OpenCode persistent data |
| `opencode-cache/` | `/home/dev/.cache/opencode` | OpenCode cache |
| `m2/` | `/home/dev/.m2` | Maven cache |
| `npm-cache/` | `/home/dev/.npm` | npm cache |
| `pip-cache/` | `/home/dev/.cache/pip` | pip cache |
| `$HOME/.gitconfig` | `/home/dev/.gitconfig` | Git configuration |
All mounted development directories are created automatically by `run.sh`.
## Additional mounts
Additional host directories can be mounted with `--mount`.
Example:
```bash ```bash
./opencode.sh ~/dev/opencode-dev/projects/my-project --help ./run.sh --mount "$HOME/other:/extra"
``` ```
## Persistent mounts Read-only mounts are also supported:
| Host | Container | Mode | ```bash
|---|---|---| ./run.sh --mount "$HOME/data:/data:ro"
| `opencode-config` | `~/.config/opencode` | RW | ```
| `opencode-data` | `~/.local/share/opencode` | RW |
| `opencode-cache` | `~/.cache/opencode` | RW |
| `m2` | `~/.m2` | RW |
| `npm-cache` | `~/.npm` | RW |
| `pip-cache` | `~/.cache/pip` | RW |
| `certs` | `/host-certs` | RO |
| `~/.gitconfig` | `~/.gitconfig` | RO |
| `~/.ssh` | `~/.ssh` | RO |
| `bash` | `/host-bash` | RO |
| `projects` | `/workspace` | RW |
No credentials or personal OpenCode state are baked into the image. Multiple mounts can be specified:
```bash
./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:
```bash
OPENCODE_DEV_HOME
PROJECT_DIR
OPENCODE_CONFIG
OPENCODE_SHARE_DATA
OPENCODE_CACHE
M2_FOLDER
NPM_CACHE
PIP_CACHE
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" ./run.sh
```
## Git configuration
The host Git configuration is mounted read-only:
```text
$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 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 with host UID/GID (`--user dev --userns=keep-id`).
* Drops all Linux capabilities (`--cap-drop=ALL`).
* Uses `no-new-privileges`.
* 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 host Git configuration is mounted read-only.
## Locale
The image generates the `en_US.UTF-8` locale during the build:
```dockerfile
echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
```
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
Build the image:
```bash
./build.sh
```
Start the development environment:
```bash
./run.sh
```
Inside the container, the projects directory is available under:
```bash
/workspace
```
Move into the project you want to work on, then start OpenCode normally:
```bash
opencode
```
The container is removed when the shell exits, while source code, OpenCode data, configuration, and development caches remain on the host.
## Customization
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.22
```
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.
+61 -45
View File
@@ -5,57 +5,73 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
UBUNTU_VERSION="${UBUNTU_VERSION:-26.04}" UBUNTU_VERSION="${UBUNTU_VERSION:-26.04}"
# OPENCODE_VERSION=$(curl -fsSI https://github.com/anomalyco/opencode/releases/latest | sed -n 's/^location:.*\/tag\/v\{0,1\}\(.*\)\r$/\1/ip') NODE_VERSION="${NODE_VERSION:-24.21.0}"
NODE_VERSION=$(curl -fsSL https://nodejs.org/dist/index.json | sed -n 's/.*"version":"v\([^"]*\)".*"lts":".*".*/\1/p' | head -n 1) OPENCODE_VERSION="${OPENCODE_VERSION:-2.0.22}"
IMAGE="${IMAGE_REPO:-localhost/opencode-dev}"
IMAGE_REPO="${IMAGE_REPO:-localhost/opencode-dev}" usage() {
# IMAGE_TAG="${IMAGE_TAG:-${OPENCODE_VERSION}}" echo 'Usage: $0 [--no-cache] [--update] [--dry-run] [--remove] [--prune] [--apt-packages="pack1 pack2"]'
# IMAGE="${IMAGE_REPO}:${IMAGE_TAG}" exit "${1:-0}"
IMAGE="${IMAGE_REPO}" }
NO_CACHE=() BUILD_OPTS=()
if [[ "${1:-}" == "--no-cache" ]]; then DRY_RUN=0
NO_CACHE+=(--no-cache) REMOVE=0
PRUNE=0
EXTRA_APT_PACKAGES=()
while (($#)); do
case "$1" in
--no-cache) BUILD_OPTS+=(--no-cache) ;;
--update) BUILD_OPTS+=(--no-cache --pull=newer) ;;
--dry-run) DRY_RUN=1 ;;
--remove) REMOVE=1 ;;
--prune) PRUNE=1 ;;
--apt-packages=*)
read -ra _pkgs <<< "${1#*=}"
EXTRA_APT_PACKAGES="${_pkgs[@]}" ;;
-h|--help) usage ;;
*) echo "Unknown option: $1" >&2; usage 2 ;;
esac
shift shift
done
if [[ -n "${EXTRA_APT_PACKAGES[@]}" ]]; then
echo "--> Apt extra pkg: ${EXTRA_APT_PACKAGES[*]}"
fi fi
UPDATE=0 if ((REMOVE || PRUNE)); then
if [[ "${1:-}" == "--update" ]]; then if ((REMOVE)); then
UPDATE=1 if podman image exists "$IMAGE"; then
shift 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 || echo "WARNING: prune failed" >&2
fi
exit 0
fi fi
DRY_RUN=false CMD=(
if [[ "${1:-}" == "--dry-run" ]]; then podman build
DRY_RUN=true "${BUILD_OPTS[@]}"
shift --build-arg "UBUNTU_VERSION=${UBUNTU_VERSION}"
--build-arg "NODE_VERSION=${NODE_VERSION}"
--build-arg "OPENCODE_VERSION=${OPENCODE_VERSION}"
--build-arg "EXTRA_APT_PACKAGES=${EXTRA_APT_PACKAGES[*]}"
--build-arg "DEV_UID=$(id -u)"
--build-arg "DEV_GID=$(id -g)"
-t "$IMAGE"
.
)
if ((DRY_RUN)); then
printf 'running:'; printf ' %q' "${CMD[@]}"; echo
exit 0
fi fi
if (($#)); then "${CMD[@]}"
echo "Usage: $0 [--no-cache] [--update] [--dry-run]" >&2 echo
exit 2 echo "Built: $IMAGE"
fi
if ((UPDATE)); then
echo "Update requested: rebuilding ${IMAGE} with current Ubuntu package metadata."
podman rmi $(podman images -q ${IMAGE_REPO})
fi
DEV_UID="$(id -u)"
DEV_GID="$(id -g)"
if [ ${DRY_RUN} == false ]; then
podman build \
"${NO_CACHE[@]}" \
--build-arg "UBUNTU_VERSION=${UBUNTU_VERSION}" \
--build-arg "NODE_VERSION=${NODE_VERSION}" \
--build-arg "DEV_UID=${DEV_UID}" \
--build-arg "DEV_GID=${DEV_GID}" \
-t "$IMAGE" \
.
echo
echo "Built: $IMAGE"
else
echo "running: podman build ${NO_CACHE[@]} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg NODE_VERSION=${NODE_VERSION} -t $IMAGE"
fi
+78
View File
@@ -0,0 +1,78 @@
# Custom CA certificates
Place any **additional CA certificates** needed to reach internal HTTPS services
in this directory, as PEM files with a `.crt` extension:
```text
certs/
├── README.md
├── company-root.crt
└── company-intermediate.crt
```
This file also guarantees the directory exists in a fresh clone. The build
copies `certs/` into the image and runs `update-ca-certificates`, so it fails if
the directory is missing.
## How they are installed
`Dockerfile` copies this directory to:
```text
/usr/local/share/ca-certificates
```
and runs:
```bash
update-ca-certificates
```
The resulting system CA bundle is also exposed to Node.js through:
```text
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
```
so both system tools and Node.js applications trust the added certificates.
## Rules
* Certificates are baked in at **build time**. The runtime container does not
install certificates from the host, so there is no need to mount this
directory into the container.
* Rebuild after any change:
```bash
./build.sh --no-cache --update
```
* Only **public** CA certificates belong here. Never commit private material
(`*.key`, `*.p12`, `*.pfx`) — everything matched here is copied into the image
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
```
## Troubleshooting
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` means a `.crt` file
is present but is not valid PEM certificate data.
-115
View File
@@ -1,115 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")")
BASE_DIR="${OPENCODE_DEV_HOME:-$HOME/opencode-dev}"
IMAGE_REPO="${IMAGE_REPO:-localhost/opencode-dev}"
# OPENCODE_VERSION="${OPENCODE_VERSION:-1.18.32}"
# IMAGE_TAG="${IMAGE_TAG:-${OPENCODE_VERSION}}"
IMAGE="${IMAGE_REPO}"
# Folders
PROJECTS_DIR="${PROJECT_DIR:-$BASE_DIR/projects}"
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}"
CERTIFICATES="${CERTIFICATES:-$BASE_DIR/certs}"
BASH_FOLDER="${BASH_FOLDER:-$BASE_DIR/bash}"
mkdir -p \
"$BASE_DIR" \
"$PROJECTS_DIR" \
"$OPC_CONFIG" \
"$OPC_DATA" \
"$OPC_CACHE" \
"$M2_FOLDER" \
"$NPM_MODULES" \
"$NPM_CACHE" \
"$PIP_CACHE" \
"$CERTIFICATES" \
"$BASH_FOLDER"
if [[ ! -f "$HOME/.gitconfig" ]]; then
echo "WARNING: $HOME/.gitconfig does not exist; Git will use container defaults." >&2
fi
if ! podman image exists "$IMAGE"; then
echo "Image $IMAGE does not exist. Build it first with ./build.sh" >&2
exit 1
fi
START_DIR="${1:-$PROJECTS_DIR}"
shift || true
if [[ "$START_DIR" = /* ]]; then
if [[ "$START_DIR" == "$PROJECTS_DIR"* ]]; then
REL="${START_DIR#"$PROJECTS_DIR"}"
START_DIR="/workspace${REL}"
else
echo "Absolute paths outside $PROJECTS_DIR require --mount and are not accepted as the initial directory." >&2
exit 2
fi
else
START_DIR="/workspace/${START_DIR#./}"
fi
if [[ ! -d "$PROJECTS_DIR" ]]; then
echo "Project directory missing: $PROJECTS_DIR" >&2
exit 1
fi
# --security-opt=no-new-privileges
ARGS=(
run --rm -it
--name opencode-dev
--user dev
--userns=keep-id
--network=default
--tz=local
-e "HOST_LANG=${LANG:-C.UTF-8}"
-e "HOST_LC_ALL=${LC_ALL:-}"
-e "START_DIR=${START_DIR}"
-v "$OPC_CONFIG:/home/dev/.config/opencode:rw"
-v "$OPC_DATA:/home/dev/.local/share/opencode:rw"
-v "$OPC_CACHE:/home/dev/.cache/opencode:rw"
-v "$M2_FOLDER:/home/dev/.m2:rw"
-v "$NPM_CACHE:/home/dev/.npm:rw"
-v "$PIP_CACHE:/home/dev/.cache/pip:rw"
-v "$CERTIFICATES:/host-certs:ro"
-v "$PROJECTS_DIR:/workspace:rw"
-v "$HOME/.gitconfig:/home/dev/.gitconfig:ro"
# -v "$HOME/.ssh:/home/dev/.ssh:ro"
-v "$BASH_FOLDER:/host-bash:ro"
-v "$SCRIPT_DIR/entrypoint.sh:/usr/local/bin/opencode-dev-entrypoint:ro"
)
# if [[ -n "${SSH_AUTH_SOCK:-}" && -S "$SSH_AUTH_SOCK" ]]; then
# ARGS+=(
# -e SSH_AUTH_SOCK=/tmp/ssh-agent.sock
# -v "$SSH_AUTH_SOCK:/tmp/ssh-agent.sock"
# )
# fi
# Additional bind mounts:
# ./dev.sh /project --mount "$HOME/other:/extra"
while (($#)); do
case "$1" in
--mount)
[[ $# -ge 2 ]] || { echo "--mount requires host:container[:ro|rw]" >&2; exit 2; }
ARGS+=(-v "$2")
shift 2
;;
*)
echo "Unknown option: $1" >&2
exit 2
;;
esac
done
exec podman "${ARGS[@]}" "$IMAGE" bash
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
set -euo pipefail
CERT_DIR="${CERT_DIR:-/host-certs}"
if [[ -d "$CERT_DIR" ]]; then
shopt -s nullglob
certs=("$CERT_DIR"/*.crt "$CERT_DIR"/*.pem)
if ((${#certs[@]})); then
mkdir -p /usr/local/share/ca-certificates/opencode-host
for cert in "${certs[@]}"; do
target="/usr/local/share/ca-certificates/opencode-host/$(basename "${cert%.*}").crt"
if openssl x509 -in "$cert" -noout >/dev/null 2>&1; then
cp -f "$cert" "$target"
else
echo "WARNING: ignoring invalid CA certificate: $cert" >&2
fi
done
if update-ca-certificates >/tmp/opencode-ca.log 2>&1; then
:
else
echo "WARNING: update-ca-certificates failed; continuing." >&2
cat /tmp/opencode-ca.log >&2 || true
fi
fi
fi
host_lang="${HOST_LANG:-${LANG:-C.UTF-8}}"
host_lc_all="${HOST_LC_ALL:-${LC_ALL:-}}"
# Generate a host locale when it is a valid UTF-8 locale and not already present.
if [[ "$host_lang" == *.UTF-8 && "$host_lang" != "C.UTF-8" ]]; then
if ! locale -a | grep -Fxqi "${host_lang//UTF-8/utf8}"; then
if [[ "$host_lang" =~ ^([A-Za-z_]+)\.UTF-8$ ]]; then
locale_name="${BASH_REMATCH[1]}.UTF-8"
if grep -qE "^#?[[:space:]]*${locale_name}[[:space:]]+UTF-8" /etc/locale.gen 2>/dev/null; then
sed -i -E "s/^#[[:space:]]*(${locale_name}[[:space:]]+UTF-8)/\1/" /etc/locale.gen
else
printf '%s UTF-8\n' "$locale_name" >> /etc/locale.gen
fi
locale-gen "$locale_name" >/dev/null 2>&1 || \
echo "WARNING: could not generate locale $host_lang; continuing." >&2
fi
fi
fi
export LANG="$host_lang"
if [[ -n "$host_lc_all" ]]; then
export LC_ALL="$host_lc_all"
fi
# exec su -s /bin/bash dev -c 'cd "${START_DIR:-/workspace}" && exec "$@"' -- "$@"
cd "${START_DIR:-/workspace}"
exec "$@"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Executable
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
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
exit 1
fi
# Folders
BASE_DIR="${OPENCODE_DEV_HOME:-$HOME/opencode-dev}"
PROJECTS_DIR="${PROJECT_DIR:-$BASE_DIR/projects}"
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_CACHE="${NPM_CACHE:-$BASE_DIR/npm-cache}"
PIP_CACHE="${PIP_CACHE:-$BASE_DIR/pip-cache}"
mkdir -p \
"$BASE_DIR" \
"$PROJECTS_DIR" \
"$OPC_CONFIG" \
"$OPC_DATA" \
"$OPC_CACHE" \
"$M2_FOLDER" \
"$NPM_CACHE" \
"$PIP_CACHE"
ARGS=(
run --rm -it
--name "${CONTAINER_NAME}"
--user dev
--userns=keep-id
--network=default
--tz=local
--security-opt=no-new-privileges
--cap-drop=ALL
-v "$OPC_CONFIG:/home/dev/.config/opencode:rw"
-v "$OPC_DATA:/home/dev/.local/share/opencode:rw"
-v "$OPC_CACHE:/home/dev/.cache/opencode:rw"
-v "$M2_FOLDER:/home/dev/.m2:rw"
-v "$NPM_CACHE:/home/dev/.npm:rw"
-v "$PIP_CACHE:/home/dev/.cache/pip:rw"
-v "$PROJECTS_DIR:/workspace:rw"
)
if [[ -f "$HOME/.gitconfig" ]]; then
ARGS+=(-v "$HOME/.gitconfig:/home/dev/.gitconfig:ro")
else
echo "WARNING: host .gitconfig not found, using container inner configuration"
fi
# Additional bind mounts:
# ./run.sh --mount "$HOME/other:/extra"
while (($#)); do
case "$1" in
--mount)
[[ $# -ge 2 ]] || { echo "--mount requires host:container[:ro|rw]" >&2; exit 2; }
ARGS+=(-v "$2")
shift 2
;;
*)
echo "Unknown option: $1" >&2
exit 2
;;
esac
done
exec podman "${ARGS[@]}" "$IMAGE" bash