Interactive harness container for daily workflows
4.4K
Minimal Ubuntu 26.04 LTS coding-agent image for daily workflows, published as lohas1107/harness with /workspace as the working directory, bash as the default command, and an opt-in tooling installer.
The image ships a small core (see What's included below). Node.js, Python, Rust, JVM tools, native build tools, coding-agent CLIs, database clients, Docker tooling, and Bitbucket repository cloning are installed on demand through the built-in harness CLI. Every interactive shell prints a one-line reminder that it exists.
Docker is not part of the core image. Choose one Docker mode when you need it:
dood) installs the Docker CLI and connects to an external daemon.dind) installs Docker Engine and runs a nested daemon managed by harness.The two modes are mutually exclusive. harness never removes an existing Docker installation automatically.
Warning
When using DooD, mounting `/var/run/docker.sock` gives the container control over the external Docker daemon. Use this image with the socket mounted only for trusted code and trusted agents.
harness CLIharness # interactive menu (needs gum >=2.0.0 and a TTY)
harness list # every item and whether it is installed
harness install node uv # non-interactive
harness install java gradle # OpenJDK 17 and the latest Gradle distribution
harness install dood # Docker outside of Docker
harness install dind # Docker inside of Docker
harness docker doctor # diagnose the selected Docker mode
Everything installs the latest version available at the time you run it.
In the interactive menu, use x or Tab to toggle items, the arrow keys to
navigate, and Enter to submit each category. Gum v2 displays the operation
help/footer below each list. After the final category, harness lists all
selected items; press Enter to submit the installation or Ctrl+C to cancel.
The interactive categories appear in this order:
The Docker page uses the same controls as the other categories: x or Tab
toggles an option and Enter advances to the next category. Leave both options
unselected to skip Docker; at most one mode may be selected. If one mode is
installed, the other remains visible but is marked blocked.
The JVM page presents Java JDK and Gradle as two independent options. Java is
considered installed when javac is available. Gradle may be selected by
itself only when that JDK is present and the active java -version is Java 17
or newer; if Java is missing, select both items and harness installs Java
first. The Java installer uses Ubuntu's openjdk-17-jdk package and does not
write a global JAVA_HOME or automatically upgrade, remove, or switch an
existing JDK. An existing JDK below 17 therefore blocks Gradle until you
change the environment yourself.
Gradle is downloaded as the official binary distribution from
services.gradle.org and checked against its published SHA-256 checksum. The
installer resolves the latest release at install time and does not upgrade an
existing gradle command. For an existing Gradle project, prefer its
./gradlew Wrapper; install the global Gradle command mainly for creating new
projects or updating a Wrapper. See Installing Gradle
and Wrapper Basics.
Installs land in the container's writable layer: they survive docker exec and docker start/docker restart on the same container, but not docker run --rm, docker compose run --rm, or docker compose down && up. If you need the tools to persist, bake your own image (next section).
harness install is fully non-interactive and fails the build if any item fails, so it works in a RUN step:
FROM lohas1107/harness:latest
RUN harness install node python3 uv && rm -rf /var/lib/apt/lists/*
For Docker tooling, select exactly one mode:
FROM lohas1107/harness:latest
RUN harness install dood
or:
FROM lohas1107/harness:latest
RUN harness install dind
This is the same pattern as node:slim / python:slim: the published lohas1107/harness stays minimal, and anyone who needs more tooling builds their own derived image and owns its scan results.
docker pull lohas1107/harness:latest
The default image does not mount a Docker socket and does not start a Docker daemon:
docker run --rm -it \
-v "$PWD:/workspace" \
lohas1107/harness:latest
The image starts in /workspace and drops you into bash as root. Nothing is installed or cloned automatically — run harness to install the tooling you need.
harness install codex runs the official standalone installer as the current user. Unless you set an override, it uses:
| Variable | Default | Purpose |
|---|---|---|
| CODEX_HOME | $HOME/.codex | Codex packages, login data, sessions, and other state |
| CODEX_INSTALL_DIR | $HOME/.local/bin | The codex launcher and its companion launcher |
The installer is per-user by default: it does not require root, use sudo, or fall back to /opt or /usr/local. harness list checks the resolved launcher path directly, so it recognizes an installed launcher even before the current shell has the install directory on PATH. The official installer still prints its Current terminal and Future terminals instructions; export the path in the current shell when needed, or start a new shell.
$HOME/.codex contains login/authentication data, session history, package files, and other Codex state. Treat it as sensitive and persist it when the container is recreated. For a non-root container, use a home directory owned by the runtime UID/GID:
mkdir -p .codex-home
chown "$(id -u):$(id -g)" .codex-home
docker run --rm -it \
--user "$(id -u):$(id -g)" \
-e HOME=/home/codex \
-v "$PWD:/workspace" \
-v "$PWD/.codex-home:/home/codex" \
lohas1107/harness:latest \
bash -lc 'harness install codex && export PATH="$HOME/.local/bin:$PATH" && codex update && exec bash'
When baking an image, run the install as the final runtime user so the launcher and state belong to that user:
FROM lohas1107/harness:latest
RUN useradd --create-home --shell /bin/bash coder
USER coder
ENV HOME=/home/coder
RUN harness install codex
If the runtime user changes after the install, install Codex again as the new user. Setting CODEX_HOME or CODEX_INSTALL_DIR selects a custom layout; the caller is responsible for making both locations writable and using the same layout and runtime user for later commands. Existing global, npm, bun, or pnpm-managed Codex installations are not removed or migrated; conflict handling remains with the official installer.
DooD installs only the Docker client-side packages:
harness install dood
harness docker doctor
Run the container with the external Docker socket mounted:
docker run --rm -it \
-v "$PWD:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host=host.docker.internal:host-gateway \
lohas1107/harness:latest \
bash -lc 'harness install dood && harness docker doctor && exec bash'
Mounting /var/run/docker.sock gives code inside the container control over the external Docker daemon, which is effectively host-level control. Use DooD only with trusted code and trusted agents. Docker also supports remote endpoints through DOCKER_HOST; harness docker doctor checks the configured endpoint.
To build with the external daemon:
docker run --rm -it \
-v "$PWD:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host=host.docker.internal:host-gateway \
lohas1107/harness:latest \
bash -lc 'harness install dood && docker buildx build --load -t local/test-image .'
DinD installs the complete Docker Engine package set and provides explicit lifecycle commands:
harness install dind
harness dind start
harness dind status
harness docker doctor
DinD must run in a privileged container and must not have the host Docker socket mounted. Use a separate volume for nested Docker data:
docker run --rm -it --privileged \
-v "$PWD:/workspace" \
-v harness-dind:/var/lib/harness/dind \
lohas1107/harness:latest \
bash -lc 'harness install dind && harness dind start && harness docker doctor && exec bash'
The default DinD paths are:
| Resource | Path |
|---|---|
| Docker socket | /var/run/docker.sock |
| Engine data root | /var/lib/harness/dind |
| PID file | /run/harness/dind.pid |
| Log file | /var/log/harness/dind.log |
harness dind stop stops the daemon without deleting its data. The harness-dind volume preserves nested images, containers, volumes, and build cache when the container is recreated. Docker's official DinD documentation also warns that --privileged is required and must be used carefully because it grants broad host capabilities. Docker Official Image
The lifecycle commands launch dockerd directly under harness; they do not
depend on systemd running as the container's PID 1.
The modes are deliberately not installed together. They share the Docker CLI,
Buildx, and Compose packages, while DinD additionally owns the engine,
containerd, and the default Docker socket. Installing both makes package state
and daemon ownership ambiguous: commands can reach the wrong endpoint, the two
daemons can contend for /var/run/docker.sock, and engine packages can clash
with distro docker.io, containerd, or runc. DinD also needs privileged
kernel capabilities and its own storage. harness therefore rejects the
second mode and never purges packages automatically; switch modes only after
manually cleaning up the existing installation.
For a long-running DinD container:
docker volume create harness-dind
docker run --name harness-dind -dit --privileged \
-v "$PWD:/workspace" \
-v harness-dind:/var/lib/harness/dind \
lohas1107/harness:latest \
bash -lc 'harness install dind && harness dind start && exec sleep infinity'
docker exec -it harness-dind harness dind status
docker exec -it harness-dind bash
docker exec harness-dind harness dind stop
docker rm -f harness-dind
If /var/run/docker.sock already exists when DinD is installed or started, harness refuses to continue. This prevents a host socket mount or another daemon from being mistaken for the nested daemon.
If you want Copilot authenticated inside the container, install it first and provide a supported token environment variable yourself:
docker run --rm -it \
-v "$PWD:/workspace" \
-e COPILOT_GITHUB_TOKEN="$COPILOT_GITHUB_TOKEN" \
lohas1107/harness:latest \
bash -lc 'harness install copilot && exec bash'
If you want to clone Bitbucket Cloud repositories, provide BITBUCKET_API_TOKEN together with a multiline BITBUCKET_REPOS list, then run the bitbucket-clone item. Each line accepts workspace/repo or workspace/repo@branch:
docker run --rm -it \
-v "$PWD:/workspace" \
-e BITBUCKET_API_TOKEN="$BITBUCKET_API_TOKEN" \
-e BITBUCKET_REPOS=$'bigobject/repo-a\nbigobject/repo-b@main\n# comments are ignored' \
lohas1107/harness:latest \
bash -lc 'harness install bitbucket-clone && exec bash'
harness install bitbucket-clone (or the Actions page in the interactive menu):
/workspace/<repo-name>--depth 1@branch is providedIf you prefer docker compose, the docker run examples above can be expressed like this:
services:
harness:
image: lohas1107/harness:latest
working_dir: /workspace
volumes:
- ./:/workspace
- /var/run/docker.sock:/var/run/docker.sock
environment:
COPILOT_GITHUB_TOKEN: ${COPILOT_GITHUB_TOKEN:-}
# BITBUCKET_API_TOKEN: ${BITBUCKET_API_TOKEN:-}
# BITBUCKET_REPOS: |
# bigobject/repo-a
# bigobject/repo-b@main
# COPILOT_HOME: /root/.copilot
extra_hosts:
- host.docker.internal:host-gateway
command: bash -lc 'harness install dood && exec bash'
tty: true
stdin_open: true
The service above uses DooD. For DinD, replace the socket mount and command with a privileged service and an independent data volume:
services:
harness:
image: lohas1107/harness:latest
working_dir: /workspace
privileged: true
volumes:
- ./:/workspace
- harness-dind:/var/lib/harness/dind
command: bash -lc 'harness install dind && harness dind start && exec sleep infinity'
tty: true
stdin_open: true
volumes:
harness-dind:
Do not configure both modes in the same service.
Start an interactive shell:
docker compose run --rm harness
--rm means anything installed via harness during that run disappears when it exits. Run a custom command:
docker compose run --rm harness harness list
If you want to persist Codex and Copilot config, mount both directories:
services:
harness:
image: lohas1107/harness:latest
working_dir: /workspace
volumes:
- ./:/workspace
- /var/run/docker.sock:/var/run/docker.sock
- ${HOME}/.codex:/root/.codex
- ${HOME}/.copilot:/root/.copilot
environment:
COPILOT_GITHUB_TOKEN: ${COPILOT_GITHUB_TOKEN:-}
# BITBUCKET_API_TOKEN: ${BITBUCKET_API_TOKEN:-}
# BITBUCKET_REPOS: |
# bigobject/repo-a
# bigobject/repo-b@main
extra_hosts:
- host.docker.internal:host-gateway
command: bash -lc 'harness install dood && exec bash'
tty: true
stdin_open: true
If your goal is to keep a container running for later docker compose exec, set a long-running command first:
services:
harness:
image: lohas1107/harness:latest
working_dir: /workspace
command: bash -lc 'harness install dood && exec sleep infinity'
volumes:
- ./:/workspace
- /var/run/docker.sock:/var/run/docker.sock
environment:
COPILOT_GITHUB_TOKEN: ${COPILOT_GITHUB_TOKEN:-}
extra_hosts:
- host.docker.internal:host-gateway
tty: true
stdin_open: true
Then start and enter it:
docker compose up -d harness
docker compose exec harness bash
Anything you harness install inside this container survives later docker compose exec sessions, because it's the same container — it disappears only if you docker compose down and up again.
docker execdocker run --name harness -dit \
-v "$PWD:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host=host.docker.internal:host-gateway \
lohas1107/harness:latest \
bash -lc 'harness install dood && exec sleep infinity'
docker exec -it harness bash
Nothing is installed when the container starts. Anything you harness install lands in this container's writable layer, so later docker exec sessions see it — but a docker rm (or a --rm container) loses it. See Bake your own image if you want the tooling to persist across containers.
Commands passed to docker run are preserved and executed directly; nothing is installed implicitly.
docker run --rm -it \
-v "$PWD:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host=host.docker.internal:host-gateway \
lohas1107/harness:latest \
bash -lc 'harness install dood && docker compose version'
Build an image with the host daemon:
docker run --rm -it \
-v "$PWD:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host=host.docker.internal:host-gateway \
lohas1107/harness:latest \
bash -lc 'harness install dood && docker buildx build --load -t local/test-image .'
The image does not force persistence of Copilot settings or login state. Install Copilot first (harness install copilot), and if you want to keep ~/.copilot across short-lived containers, mount it yourself:
docker run --rm -it \
-v "$PWD:/workspace" \
-v "$HOME/.copilot:/root/.copilot" \
-e COPILOT_GITHUB_TOKEN="$COPILOT_GITHUB_TOKEN" \
lohas1107/harness:latest \
bash -lc 'harness install copilot && exec bash'
You can also set COPILOT_HOME to use a different config directory.
The root Compose example above keeps Codex state at /root/.codex. For a non-root service, provide explicit UID/GID values and bind-mount a writable home directory. The mount contains both the default Codex state directory and the default launcher directory:
mkdir -p .codex-home
chown "$(id -u):$(id -g)" .codex-home
CODEX_UID="$(id -u)" CODEX_GID="$(id -g)" docker compose run --rm harness
services:
harness:
image: lohas1107/harness:latest
user: "$CODEX_UID:$CODEX_GID"
working_dir: /workspace
environment:
HOME: /home/codex
volumes:
- ./:/workspace
- ./.codex-home:/home/codex
command: bash -lc 'harness install codex && export PATH="$HOME/.local/bin:$PATH" && exec bash'
tty: true
stdin_open: true
The host home mount must remain writable by the selected UID/GID. If the service later runs as a different user, use a separate writable home or reinstall Codex for that user.
| Category | Tools |
|---|---|
| Base environment | Ubuntu 26.04 LTS, bash, ca-certificates, /workspace, default root user |
| Source control and API | git, gh (from GitHub's official apt repository), curl, jq |
| Search | ripgrep |
| Installer | the harness CLI and its gum menu |
harness install <item>)| Item | Category | What you get |
|---|---|---|
make | Native build tools | make |
gcc | Native build tools | gcc, g++ |
cmake | Native build tools | cmake |
dood | Docker | Docker CLI, Buildx, Compose for an external daemon |
dind | Docker | Docker Engine, containerd, Docker CLI, Buildx, Compose |
claude | Coding agents | Claude Code |
codex | Coding agents | latest stable Codex CLI |
copilot | Coding agents | latest GitHub Copilot CLI |
antigravity | Coding agents | Antigravity CLI (agy) |
python3-runtime | Python 3 | python3 |
python3-pip | Python 3 | python3-pip |
python3-venv | Python 3 | python3-venv |
python-is-python3 | Python 3 | python command alias |
pipx | Python 3 | pipx (pulls in python3) |
uv | Python 3 | latest stable uv |
rust | Rust | rustc, cargo (pulls in gcc) |
node | Node.js | current Node.js LTS + npm, from the official nodejs.org tarball |
java | JVM | OpenJDK 17 JDK (java, javac) |
gradle | JVM | latest official Gradle binary distribution with SHA-256 verification |
postgresql-client | Database clients | PostgreSQL client |
bitbucket-clone | Actions | clones the repos listed in BITBUCKET_REPOS into /workspace |
harness install python3 remains a compatibility alias for the complete Python 3 bundle: python3-runtime, python3-pip, python3-venv, and python-is-python3. harness install agy is a compatibility alias for harness install antigravity.
Ubuntu 26.04 brings a large version jump for several opt-in toolchains versus the previous 24.04-based image (e.g. gcc 13→15, Python 3.12→3.14, CMake 3.28→4.2, Rust 1.75→1.93, PostgreSQL 16→18). This can affect your own projects' build configs — for example, CMake 4.x drops compatibility with cmake_minimum_required(VERSION <3.5).
Content type
Image
Digest
sha256:89501ff15…
Size
86.4 MB
Last updated
4 days ago
docker pull lohas1107/harness