repos
/ taproot main

taproot

mirror

The 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

11.5 KB · 255 lines · Docker Raw History
  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"]