Custom container images¶
The image gmlx builds for a client holds the client, Node.js, git, ripgrep,
curl and ssh. When a project needs more, such as python3, make or a
database, this page shows how to add it, from one line of config to an image
of your own. Container recipes has complete setups.
Extra packages¶
Most projects need only a few Debian packages. List them in
packages:
The next launch rebuilds the image with them, and later launches reuse it.
Your own Containerfile¶
For anything more, write a Containerfile. gmlx tags each client's image as
gmlx.invalid/launch-<client>:base. The .invalid domain can never exist,
so no registry can serve an image with that name. Start from it to build on
the image gmlx made:
FROM gmlx.invalid/launch-claude-code:base
RUN apt-get update \
&& apt-get install -y --no-install-recommends make python3 \
&& rm -rf /var/lib/apt/lists/*
Put the Containerfile in a folder of its own, and name that folder with
build:
launch rebuilds the image when the Containerfile or another file in the
folder changes, and when a gmlx upgrade changes the base. List large folders
such as node_modules in a .dockerignore to keep them out of the build.
A <Containerfile>.dockerignore beside the Containerfile, such as
Containerfile.dockerignore, replaces .dockerignore. --rebuild builds
again from scratch, which also brings Debian updates.
launch does not watch a .git folder at the top of the build folder for
changes, but the build still receives it. List .git in the ignore file to
keep it out of the image.
Write the FROM line exactly as shown, because launch reads it to find the
base. Any client's :base works, and so does
gmlx.invalid/launch-runtime-python:base, the base for
custom agents with runtime: python.
Keep the build folder outside every folder a session shares read-write. Its
code runs at the next build with internet access, so launch refuses a
build folder that a client could change.
A newer client¶
The image gmlx builds pins each client at one version, and a gmlx release brings newer ones. To run a newer client sooner, install it over the base:
Write an exact version, never latest. A new version number in the file
triggers a rebuild, while latest does not. The install line for each
client is:
| Client | Install line |
|---|---|
claude-code |
npm install -g @anthropic-ai/claude-code@<version> |
opencode |
npm install -g opencode-ai@<version> |
pi |
npm install -g @earendil-works/pi-coding-agent@<version> |
dsh |
npm install -g @deepseek-ai/dsh@<version> |
hermes |
/opt/venv/bin/pip install --no-cache-dir "hermes-agent[mcp]==<version>" |
elia |
/opt/venv/bin/pip install --no-cache-dir elia-chat==<version> |
open-webui |
/opt/venv/bin/pip install --no-cache-dir open-webui==<version> |
omp, goose, aichat |
A release download for Linux on arm64 into /usr/local/bin, as in the Containerfile that gmlx ships. |
For omp, goose and aichat, copy the download and checksum lines from
the shipped Containerfile, gmlx/container/files/Containerfile in the
installed package. Remove your line again once a gmlx upgrade catches up.
A ready-made image¶
To run an image from a registry or your local image store, name it with
image:
launch pulls it once and keeps running that exact image until
--rebuild pulls it again. --image REF runs another image for one launch.
What an image needs¶
An image must be for Linux on arm64 and contain the command that runs.
launch checks this before the first session and stops with a message when
the command is missing.
A command is not in the image
covers the fixes.
The command that runs¶
The container runs the client's command, followed by the arguments after
--. command changes it:
- A list replaces the client's command.
launchstill writes the client's configuration. imageruns the image's own ENTRYPOINT and CMD, and the arguments after--replace CMD.
Some clients get extra arguments from launch, such as goose session. To
keep them in a command list, copy them from the
dry run.
Starting services with the client¶
A start script can start a database or another service before the client:
- In a new folder, write
start.sh:
- Beside it, write a Containerfile that copies the script into the image:
FROM gmlx.invalid/launch-claude-code:base
COPY start.sh /usr/local/bin/start.sh
RUN chmod 755 /usr/local/bin/start.sh
- Name the folder and put the script before the client's command:
launch:
container:
clients:
claude-code:
build: ~/containers/claude-code
command: [/usr/local/bin/start.sh, claude]
The script starts its services and then runs claude. A service that will
not run as root can start with runuser -u <user> --, as the
Postgres recipe shows.