Skip to main content

Building a Custom Image

In this tutorial, we will build a custom container image with project-specific dependencies pre-installed. By the end, you will understand how to create images that give your agents immediate access to all the tools they need.

This tutorial takes approximately 30-40 minutes to complete.

Prerequisites

Before starting, ensure you have:

  • Completed the Getting Started tutorial
  • Docker or Podman installed on your machine
  • Basic familiarity with Dockerfile syntax
  • A project with specific runtime requirements (Python version, Node.js packages, system tools, etc.)

Why Build a Custom Image

The default Headjack image includes version managers (pyenv, nodenv, goenv) but no pre-installed language versions. When an agent needs Python 3.11, it must install it first, which takes time.

A custom image lets you:

  • Pre-install specific language versions your project requires
  • Include system packages (database clients, build tools)
  • Add global tooling (linters, formatters, CLI tools)
  • Reduce agent startup time significantly

Step 1: Identify Your Dependencies

First, catalog what your project needs. For this tutorial, we will build an image for a Python web application that requires:

  • Python 3.11
  • Poetry for dependency management
  • PostgreSQL client for database access
  • Node.js 20 for frontend build tools

Review your project's requirements and make a similar list.

Step 2: Create the Dockerfile

Create a new file called Dockerfile.headjack in your project root:

cd ~/projects/my-app
touch Dockerfile.headjack

Open the file and add the base image:

FROM ghcr.io/gilmanlab/headjack:base

The Headjack base image provides:

  • Ubuntu base with common development tools
  • A developer user (non-root)
  • pyenv, nodenv, and goenv pre-configured
  • Claude Code, Gemini CLI, and Codex CLI installed

Step 3: Install System Packages

System packages require root access. Add them first:

FROM ghcr.io/gilmanlab/headjack:base

# Switch to root for system package installation
USER root

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

# Switch back to developer user
USER developer
WORKDIR /home/developer

The --no-install-recommends flag keeps the image smaller. Always clean up apt lists to reduce layer size.

Step 4: Install Python

Now add Python using pyenv. This runs as the developer user:

FROM ghcr.io/gilmanlab/headjack:base

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

USER developer
WORKDIR /home/developer

# Install Python 3.11
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pyenv install 3.11.7 && \
pyenv global 3.11.7

The eval command initializes pyenv in the shell. This is necessary because the Dockerfile runs each RUN command in a fresh shell.

Step 5: Install Python Tools

Add Poetry and any other Python tooling:

FROM ghcr.io/gilmanlab/headjack:base

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

USER developer
WORKDIR /home/developer

# Install Python 3.11
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pyenv install 3.11.7 && \
pyenv global 3.11.7

# Install Poetry
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pip install --upgrade pip && \
pip install poetry

Step 6: Install Node.js

Add Node.js for frontend tooling:

FROM ghcr.io/gilmanlab/headjack:base

USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

USER developer
WORKDIR /home/developer

# Install Python 3.11
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pyenv install 3.11.7 && \
pyenv global 3.11.7

# Install Poetry
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pip install --upgrade pip && \
pip install poetry

# Install Node.js 20
RUN eval "$(~/.nodenv/bin/nodenv init -)" && \
nodenv install 20.10.0 && \
nodenv global 20.10.0

# Install global Node.js tools
RUN npm install -g pnpm

Step 7: Build the Image

Build the image with Docker:

docker build -t my-app-headjack:latest -f Dockerfile.headjack .

Or with Podman:

podman build -t my-app-headjack:latest -f Dockerfile.headjack .
note

Build with the same container runtime that Headjack uses. Check your configuration with hjk config and look for runtime.name. If Headjack uses Podman, build with Podman. Images built with Docker are not automatically available to Podman (and vice versa) unless pushed to a registry.

The build takes several minutes as it compiles Python and Node.js. You will see output for each step:

[+] Building 245.3s (12/12) FINISHED
=> [1/6] FROM ghcr.io/gilmanlab/headjack:base
=> [2/6] RUN apt-get update && apt-get install -y ...
=> [3/6] RUN eval "$(~/.pyenv/bin/pyenv init -)" && pyenv install 3.11.7 ...
=> [4/6] RUN eval "$(~/.pyenv/bin/pyenv init -)" && pip install poetry
=> [5/6] RUN eval "$(~/.nodenv/bin/nodenv init -)" && nodenv install 20.10.0 ...
=> [6/6] RUN npm install -g pnpm

Step 8: Test the Image

Verify the image works by running a container:

docker run --rm -it my-app-headjack:latest bash

Inside the container, verify your tools:

python --version
# Python 3.11.7

poetry --version
# Poetry (version 1.x.x)

node --version
# v20.10.0

psql --version
# psql (PostgreSQL) 15.x

Type exit to leave the container.

Step 9: Use the Image with Headjack

Now use your custom image with Headjack. Specify it with the --base flag:

hjk run feat/new-feature --base my-app-headjack:latest --agent claude "Add user authentication using PostgreSQL sessions"

The agent starts immediately with all dependencies available. No waiting for Python or Node.js installation.

Step 10: Set as Default

To avoid specifying --base every time, set your image as the default:

hjk config default.base_image my-app-headjack:latest

Or use an environment variable:

export HEADJACK_BASE_IMAGE=my-app-headjack:latest

Add this to your shell profile (.bashrc, .zshrc) to make it permanent.

Now all hjk run commands use your custom image automatically:

hjk run feat/new-feature --agent claude "Add user authentication"

Step 11: Share with Your Team

For team projects, publish the image to a registry.

Push to Docker Hub:

docker tag my-app-headjack:latest yourusername/my-app-headjack:latest
docker push yourusername/my-app-headjack:latest

Push to GitHub Container Registry:

docker tag my-app-headjack:latest ghcr.io/your-org/my-app-headjack:latest
docker push ghcr.io/your-org/my-app-headjack:latest

Team members can then use the shared image:

hjk config default.base_image ghcr.io/your-org/my-app-headjack:latest

Step 12: Build for Multiple Architectures

If your team uses both Intel and Apple Silicon Macs, build a multi-architecture image:

docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/your-org/my-app-headjack:latest \
--push \
-f Dockerfile.headjack .

This creates an image that works on both architectures. Docker and Podman automatically pull the correct variant.

Complete Dockerfile

Here is the complete Dockerfile from this tutorial:

FROM ghcr.io/gilmanlab/headjack:base

# Install system packages as root
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

# Switch to developer user for language installation
USER developer
WORKDIR /home/developer

# Install Python 3.11
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pyenv install 3.11.7 && \
pyenv global 3.11.7

# Install Poetry
RUN eval "$(~/.pyenv/bin/pyenv init -)" && \
pip install --upgrade pip && \
pip install poetry

# Install Node.js 20
RUN eval "$(~/.nodenv/bin/nodenv init -)" && \
nodenv install 20.10.0 && \
nodenv global 20.10.0

# Install global Node.js tools
RUN npm install -g pnpm

What We Learned

In this tutorial, we:

  • Identified project dependencies for the custom image
  • Created a Dockerfile extending the Headjack base image
  • Installed system packages, Python, and Node.js
  • Built and tested the image locally
  • Configured Headjack to use the custom image by default
  • Learned how to share images with a team

Custom images dramatically improve the agent experience. Instead of waiting for dependency installation, agents can start working immediately. For projects with complex dependency chains, this saves significant time per session.

Next Steps

Now that you can build custom images, explore these resources:

How-To Guides

Reference

Concepts