taproot
mirrorThe dotfiles and containers I use to set up a machine for development, one container to write code in and another that runs a local coding model on the desktop's GPU.
alpine-linuxcaddydevelopment-environmentdockerdotfileshomelabinfrastructureneovimserver-configurationtmux
1# aiagent
2#
3# Ornith 1.5 9B at 128k context on a llama.cpp CUDA server, plus the pi coding
4# agent pointed at it. Sized for an 8GB Ampere card. No telemetry, no update
5# checks, no published port. It has no toolchain of its own and borrows what it
6# needs to compile from webdev over the mounted docker socket, see
7# scripts/webdev-exec.sh.
8#
9# Everything is a make target in the Makefile at the root of this repo, which is
10# also the build context, so run these from a clone. C=aiagent picks this
11# container, since the targets default to webdev:
12#
13# make build C=aiagent build or rebuild, about 15 minutes the first time
14# make models fetch the weights, the only step that reaches HF
15# make up C=aiagent create it and start it, safe to re-run
16# make update C=aiagent rebuild and replace a running one
17# make shell C=aiagent get in, then type: pi
18# make stop C=aiagent stop it, which frees the VRAM
19#
20# Serve a different model without rebuilding, as a one-off against the same
21# weights volume:
22#
23# make serve MODEL=<hf-repo>:<quant>
24#
25# Gotchas: --gpus all or it loads nothing. --init or tmux leaves zombies. The
26# server binds loopback and publishes nothing, so only pi in here reaches it and
27# it needs no API key. Anything that reaches the docker socket is root on the
28# host.
29#
30# Weights plus a 128k q4_0 KV cache measure 7931MiB of 8192 with a desktop on
31# the same card, so a second CUDA process will not fit and 261MiB is all the
32# room anything else has. Two ways to buy it back if that bites: 96k of context
33# saves about 300MiB, and q8_0 K and V at 64k measures 7704MiB and is the
34# trade worth making if output ever comes back as gibberish, since this model
35# runs 4 KV heads against 16 attention heads and that is the shape that
36# tolerates a 4 bit cache worst.
37
38
39FROM debian:trixie-slim AS build
40
41# CUDA 13.2 produces gibberish from low-bit quants.
42ARG CUDA=13-3
43
44RUN apt-get update && \
45 apt-get install -y --no-install-recommends \
46 ca-certificates curl git cmake ninja-build build-essential \
47 libcurl4-openssl-dev && \
48 curl -fsSLo /tmp/keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64/cuda-keyring_1.1-1_all.deb && \
49 dpkg -i /tmp/keyring.deb && \
50 apt-get update && \
51 apt-get install -y --no-install-recommends \
52 cuda-nvcc-${CUDA} cuda-cudart-dev-${CUDA} libcublas-dev-${CUDA} && \
53 rm -rf /var/lib/apt/lists/* /tmp/keyring.deb
54
55ENV PATH=/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
56
57# sm_86 alone keeps the build short; change it for another card.
58# FA_ALL_QUANTS is what makes flash attention work with a quantized KV cache.
59# Static libs because upstream ships shared objects without a SONAME, which
60# ldconfig then refuses to cache.
61RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp /tmp/src && \
62 cmake -S /tmp/src -B /tmp/build -G Ninja \
63 -DCMAKE_BUILD_TYPE=Release \
64 -DBUILD_SHARED_LIBS=OFF \
65 -DGGML_CUDA=ON \
66 -DCMAKE_CUDA_ARCHITECTURES=86 \
67 -DGGML_CUDA_FA_ALL_QUANTS=ON \
68 -DGGML_NATIVE=OFF \
69 -DLLAMA_CURL=ON \
70 -DLLAMA_BUILD_TESTS=OFF \
71 -DLLAMA_BUILD_EXAMPLES=OFF && \
72 cmake --build /tmp/build --target llama-server llama-bench -j"$(nproc)" && \
73 strip /tmp/build/bin/llama-server /tmp/build/bin/llama-bench
74
75
76FROM debian:trixie-slim
77
78ARG CUDA=13-3
79
80ENV DEBIAN_FRONTEND=noninteractive \
81 TZ=UTC \
82 LANG=C.UTF-8 \
83 LC_ALL=C.UTF-8 \
84 TERM=xterm-256color \
85 HOME=/home/ai \
86 LLAMA_CACHE=/models \
87 PATH=/home/ai/scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
88
89# Node, not bun, because pi's extension installer shells out to `npm root -g`.
90# The Docker CLI is the client only and reaches webdev, where the build tools
91# are. From Docker's own repo, not Debian's.
92RUN apt-get update && \
93 apt-get install -y --no-install-recommends \
94 ca-certificates curl git openssh-client jq tmux less nano procps sudo \
95 ripgrep fd-find iptables libgomp1 libcurl4 && \
96 curl -fsSLo /tmp/keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64/cuda-keyring_1.1-1_all.deb && \
97 dpkg -i /tmp/keyring.deb && \
98 install -m 0755 -d /etc/apt/keyrings && \
99 curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
100 chmod a+r /etc/apt/keyrings/docker.asc && \
101 echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" \
102 > /etc/apt/sources.list.d/docker.list && \
103 apt-get update && \
104 apt-get install -y --no-install-recommends \
105 cuda-cudart-${CUDA} libcublas-${CUDA} docker-ce-cli && \
106 curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
107 apt-get install -y --no-install-recommends nodejs && \
108 npm install -g @earendil-works/pi-coding-agent && \
109 npm cache clean --force && \
110 rm -rf /var/lib/apt/lists/* /tmp/keyring.deb
111
112COPY --from=build /tmp/build/bin/llama-server /tmp/build/bin/llama-bench /usr/local/bin/
113
114# Debian's /etc/profile assigns PATH outright, so a login shell (every tmux
115# pane) drops anything ENV added. Both this and the ENV above are needed.
116RUN printf 'export PATH=/home/ai/scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n' \
117 > /etc/profile.d/10-path.sh
118
119# UID 1001 matches webdev's dev user so both share the bythewood-code volume,
120# and the sudo rule is there so the wrappers can reach the root:root 660 socket.
121#
122# pi fetches its own rg and fd into .pi/agent/bin on first run, which offline
123# mode blocks, so link the packaged ones where it looks for them.
124RUN useradd -m -u 1001 -s /bin/bash ai && \
125 echo "ai ALL=(root) NOPASSWD: /usr/bin/docker" > /etc/sudoers.d/ai && \
126 chmod 440 /etc/sudoers.d/ai && \
127 mkdir -p /home/ai/code /home/ai/.pi/agent/bin /models && \
128 ln -s /usr/bin/rg /home/ai/.pi/agent/bin/rg && \
129 ln -s /usr/bin/fdfind /home/ai/.pi/agent/bin/fd && \
130 chown -R ai:ai /home/ai /models
131
132COPY dotfiles/bash_aliases /home/ai/.bash_aliases
133COPY dotfiles/tmux.conf /home/ai/.tmux.conf
134COPY containers/aiagent/scripts/ /home/ai/scripts/
135# pi walks up from its cwd to find this.
136COPY --chown=ai:ai <<'AGENTS' /home/ai/AGENTS.md
137# AGENTS.md
138
139## Tools
140
141You have `read`, `bash`, `edit`, `write`, and web tools: `web_search`,
142`fetch_content` and `get_search_content`. Use them. Do not answer from memory
143when a fact can be looked up.
144
145`go`, `gofmt` and `bun` are on your PATH. They run in another container against
146these same files and there is nothing for you to set up, so use them as you
147would anywhere. If that container is not running they print what to do and exit
148non-zero, so pass the message on and do not claim the code builds.
149
150Never run a command that does not exit on its own. No dev servers, no watch
151modes, no `bun run dev`, no `make run`. They hang and the work stops there.
152
153Only `~/code` is shared. Build from inside it.
154
155## Not every task is a coding task
156
157Work out what you were actually asked for before you start, since plenty of it
158is a question, a lookup or a piece of research with no code in it at all. If you
159changed no files then there is nothing to build, so answer with what you found
160and name the sources you read. Everything below applies once you have written or
161edited code.
162
163## Before adding any dependency
164
165Look up the current stable version before writing it into a manifest. Training
166data is out of date and will suggest an old major version.
167
1681. `web_search` for the package's latest stable release.
1692. Write the version you found, not one you remember.
1703. Say in your summary which version you found.
171
172## Go
173
174- Final images are minimal and have no C compiler. Any SQLite driver must be
175 pure Go and must build with `CGO_ENABLED=0`. `modernc.org/sqlite` works;
176 `mattn/go-sqlite3` needs cgo and fails at runtime.
177- `embed` patterns cannot reach outside the package directory and do not
178 support `**`.
179
180## Frontend
181
182Bun and Vite, no npm and no nodejs. Build the frontend before claiming it
183works: `bun install && bun run build`. Vite emits a manifest so the server can
184resolve content hashed filenames.
185
186## Definition of done
187
188If you changed code, `go build ./...` and `go vet ./...` both have to exit zero
189and the frontend has to build. Run them and do not report success without
190running them, since a program that compiles but serves a blank page is not done.
191
192If a tool would not run, say so plainly and say what is unverified.
193AGENTS
194# extended-keys lets pi see modified Enter, appended here rather than added to
195# the shared dotfile so webdev's tmux is untouched.
196#
197# One script under three names, dispatching on its own argv[0], so the agent
198# runs a plain `go build ./...` and never has to assemble a docker exec.
199RUN chown ai:ai /home/ai/.bash_aliases /home/ai/.tmux.conf && \
200 for f in /home/ai/scripts/*.sh; do mv "$f" "${f%.sh}"; done && \
201 chmod +x /home/ai/scripts/* && \
202 for t in go gofmt bun; do ln -s webdev-exec /home/ai/scripts/$t; done && \
203 chown -R ai:ai /home/ai/scripts && \
204 echo "source ~/.bash_aliases" >> /home/ai/.bashrc && \
205 printf 'set -g extended-keys on\nset -g extended-keys-format csi-u\n' >> /home/ai/.tmux.conf
206
207WORKDIR /home/ai/code
208USER ai
209
210# web-search.json pins duckduckgo because the default chain ends at Exa's
211# public endpoint when no API keys are set, and workflow "none" because the
212# default blocks on a browser curator that never opens in a container.
213RUN pi install npm:pi-web-access && \
214 printf '%s' '{"providers":{"local":{"baseUrl":"http://127.0.0.1:8000/v1","api":"openai-completions","apiKey":"none","compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false},"models":[{"id":"local","name":"local","contextWindow":131072,"maxTokens":8192,"input":["text"],"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0}}]}}}' \
215 > /home/ai/.pi/agent/models.json && \
216 printf '%s' '{"packages":["npm:pi-web-access"],"enableInstallTelemetry":false,"enableAnalytics":false}' \
217 > /home/ai/.pi/agent/settings.json && \
218 printf '%s' '{"searchProvider":"duckduckgo","workflow":"none","autoOpenBrowser":false}' \
219 > /home/ai/.pi/web-search.json
220
221# Set after the install above so it cannot block the build.
222ENV PI_OFFLINE=1 \
223 PI_SKIP_VERSION_CHECK=1 \
224 PI_TELEMETRY=0 \
225 DO_NOT_TRACK=1 \
226 LLAMA_ARG_OFFLINE=1 \
227 NPM_CONFIG_UPDATE_NOTIFIER=false \
228 NPM_CONFIG_FUND=false \
229 NPM_CONFIG_AUDIT=false
230
231# --no-mmproj refuse the vision projector, which costs VRAM and makes the
232# server disable prompt caching
233# --parallel 1 hybrid attention corrupts context checkpoints under
234# multi-slot load (ggml-org/llama.cpp#20222), and one slot
235# keeps the prompt cache an agent loop depends on
236# --flash-attn required for a quantized KV cache; K and V must match type
237# The entrypoint decides whether to be a model server at all. With
238# llm.bythewood.me reachable it points pi there and loads nothing, because one
239# card cannot hold the estate's model and a second copy of it. Without a gateway
240# it starts the server below, which is what this container always was.
241ENTRYPOINT ["/home/ai/scripts/aiagent-entrypoint"]
242CMD ["-hf", "ornith-ai/Ornith-1.5-9B-GGUF:Q4_K_M", \
243 "--alias", "local", \
244 "--host", "127.0.0.1", "--port", "8000", \
245 "--ctx-size", "131072", \
246 "--n-gpu-layers", "999", \
247 "--flash-attn", "on", \
248 "--cache-type-k", "q4_0", "--cache-type-v", "q4_0", \
249 "--parallel", "1", \
250 "--batch-size", "2048", "--ubatch-size", "256", \
251 "--no-mmproj", \
252 "--jinja", \
253 "--reasoning", "off", \
254 "--temp", "0.7", "--top-p", "0.8", "--top-k", "20"]