Skip to content

The Construct Image

projectjackin/construct:trixie is the shared base Docker image for every agent — the foundation layer that every role extends. It provides system tools, shell environment, and container infrastructure, kept in one image so every agent inherits the same baseline.

Every agent Dockerfile starts from the construct:

FROM projectjackin/construct:0.3-trixie

The construct is built on Debian Trixie and includes:

PackagePurpose
bashDefault shell for scripts
zsh + Oh My ZshDefault interactive shell for the agent user. Oh My Zsh runs with ZSH_THEME="" so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 auto-title hooks are active without overriding the Starship prompt.
fishOpt-in alternative shell. Pre-configured ~/.config/fish/config.fish initialises Starship and emits the same OSC 0/2 + OSC 7 pane title escapes as zsh, so pane border titles render identically. Enter with fish from a default zsh pane, or set as the login shell with sudo chsh -s /usr/bin/fish agent.
git + Git LFSSource control with large file support
curlHTTP client
jq + yqJSON and YAML processors
openssh-clientSSH for git operations
sudoPrivilege escalation (passwordless for agent user)
treeDirectory visualization
PackagePurpose
ripgrep (rg)Fast regex search
fd-find (fd)Fast file finder
fzfFuzzy finder for interactive selection
PackagePurpose
misePolyglot language version manager
Docker CLI + ComposeContainer operations via DinD
GitHub CLI (gh)Repository, PR, and issue operations
Starship promptInformative terminal prompt

The construct creates an agent user with:

  • Home directory at /home/agent
  • Zsh as the default shell, with fish available as an opt-in alternative (fish from zsh, or sudo chsh -s /usr/bin/fish agent to change the login shell)
  • Passwordless sudo access
  • Oh My Zsh sourced with ZSH_THEME="" so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 title hooks are active without competing with Starship for the prompt
  • Starship prompt configured for both zsh and fish so the prompt surface stays the same when switching shells
  • Pane border title (user@host:cwd) emitted via OSC 0/2 on every prompt — the jackin-capsule multiplexer reads it and renders it as the pane title (the same mechanism zellij uses)
  • mise shims in $PATH for both shells

The construct source starts at docker/construct/Dockerfile in the jackin’ repository. The declarative build definition lives in docker-bake.hcl at the repo root, and the supported command wrapper is Justfile at the repo root.

docker/construct/versions.env remains the source of truth for the pinned tirith, shellfirm, and MISE_VERSION build args used by docker/construct/Dockerfile. mise is installed from the official rolling apt repository during the Docker build; pinning the apt package version gives Buildx a stable cache key, and Renovate bumps invalidate the mise install layer when upstream publishes a new package.

Use just for day-to-day construct work. Docker Bake is the underlying build engine, but contributors should treat just as the supported command surface.

Before opening a pull request for construct changes, validate the image locally:

Terminal window
just construct-init-buildx
just construct-build-local

By default, local validation loads the image into your Docker daemon as jackin-local/construct:trixie, so it does not silently replace the canonical projectjackin/construct:trixie base image that normal jackin workflows consume.

To test jackin load against your local construct build, set JACKIN_CONSTRUCT_IMAGE after building:

Terminal window
just construct-init-buildx
just construct-build-local
export JACKIN_CONSTRUCT_IMAGE="jackin-local/construct:trixie"

With JACKIN_CONSTRUCT_IMAGE set, jackin’ validates role Dockerfiles against the canonical image name as usual (role repos reference a versioned construct tag such as projectjackin/construct:0.3-trixie), but substitutes your local image at derived-build time so docker build uses jackin-local/construct:trixie as the actual base.

Use just construct-inspect to print the fully resolved Bake config without building the image.

If builder state gets stale or confusing, run just construct-doctor-buildx to inspect it or just construct-reset-buildx to recreate it. Set BUILDX_BUILDER before running just if you want to isolate this workflow under a different local builder name.

To force a specific target architecture during local debugging:

Terminal window
just construct-build-platform amd64
just construct-build-platform arm64

These commands work when your buildx builder supports the target platform natively or through emulation.

If you need to rehearse the release path against a temporary registry, point REGISTRY_IMAGE at your own namespace instead of the canonical projectjackin/construct repository:

Terminal window
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER just construct-push-platform amd64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER just construct-push-platform arm64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER just construct-publish-manifest

GitHub Actions reuses the same just commands on native ubuntu-24.04 and ubuntu-24.04-arm runners. Construct CI triggers when changes touch docker/construct/**, docker-bake.hcl, Justfile, or .github/workflows/construct.yml.

Pull requests, plus manual workflow runs against non-main branches, build both architectures natively without pushing images. Those validation jobs use the GitHub Actions cache backend; the cache key includes the pinned MISE_VERSION, so a Renovate mise bump invalidates the apt install layer while unrelated earlier layers can still be reused. They still do not receive Docker Hub credentials.

Pushes to main, plus manual workflow runs against main, publish each platform by digest first, then assemble the final multi-platform manifest from those digests. The registry cache uses the same pinned MISE_VERSION keying behavior as PR builds. That keeps the public tag surface limited to the canonical release tags instead of leaving permanent public -amd64 and -arm64 tags behind.

Published images also carry explicit BuildKit provenance and SBOM attestations.

The published tags are:

  • projectjackin/construct:trixie — the stable tag
  • projectjackin/construct:trixie-<sha> — commit-specific tag

When you load an agent, jackin’ doesn’t use the construct directly. It generates a derived Dockerfile that adds:

  1. User remapping — adjusts the agent user’s UID/GID to match your host user
  2. Agent installation — installs every agent declared in the role manifest
  3. Plugin installation — any Claude plugins declared in the role manifest
  4. Runtime entrypoint + setup bridge — the script delegates deterministic setup to jackin-capsule runtime-setup, then keeps the shell-native work: sourcing role hooks and exec-ing the selected agent. The Rust setup path handles one-time container git/GitHub initialization, the shared git trailer hook, durable agent-home seeding, auth handoff refreshes, and Claude MCP registration. It wires up GitHub if gh is already authenticated; it does not perform gh auth login, so an unauthenticated gh is left alone with a notice.

Role repos add their tools on top of the construct. The construct provides the foundation — agents provide the specialization:

┌─────────────────────────────────┐
│ Derived Layer (jackin-managed) │ Agent CLIs, entrypoint, user mapping
├─────────────────────────────────┤
│ Agent Layer (your Dockerfile) │ Rust, Node, Python, custom tools
├─────────────────────────────────┤
│ Construct (shared base) │ Debian, git, Docker CLI, mise, zsh
└─────────────────────────────────┘

This layered approach means:

  • Agent authors focus on their tools, not infrastructure
  • The construct can be updated independently (security patches, new tools)
  • Docker layer caching makes builds fast