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

7.9 KB · 171 lines · Docker Raw History
  1# webdev
  2#
  3# Debian 13. Go, Python (uv), Bun, Claude Code, Docker CLI, gh, typst, Playwright.
  4# No nodejs, no npm, no Rust. Work lives in the four bythewood-* volumes, so the
  5# container itself is disposable.
  6#
  7# Driven by the Makefile at the root of this repo, which is also the build
  8# context, and where every command lives. From a clone:
  9#
 10#     make up          create whatever is missing and start it, safe to re-run
 11#     make update      rebuild this image and replace the running container
 12#     make doctor      what exists, what is running, and what to type next
 13#     make shell       get in, via tmux
 14#
 15# A machine that has never run this needs Docker and a git key at
 16# ~/.ssh/home_key that is on GitHub, then one command:
 17#
 18#     make install     the above, plus the key, the B2 credentials and a restore
 19#
 20# `make help` lists the rest and `make doctor` reports on all of it.
 21#
 22# Gotchas: --init or tmux leaves zombies. docker.sock is root:root 660, so
 23# docker needs sudo in here. --publish is what makes port 8000 reachable from
 24# the host browser. Bind mounts under /home/dev silently mount empty, because
 25# the daemon is Docker Desktop and resolves paths on the Windows side, so use
 26# named volumes.
 27
 28FROM debian:trixie-slim
 29
 30# BUN_INSTALL is persistent: it also decides where `bun install -g` puts globals.
 31ENV DEBIAN_FRONTEND=noninteractive \
 32    TZ=UTC \
 33    BUN_INSTALL=/usr/local \
 34    LANG=C.UTF-8 \
 35    LC_ALL=C.UTF-8 \
 36    TERM=xterm-256color \
 37    PATH="/home/dev/.local/bin:/home/dev/scripts:/usr/local/go/bin:/home/dev/go/bin:$PATH"
 38
 39# Docker's repo, not Debian's, since trixie has no docker-compose-v2 package.
 40# Client only, never the daemon. jq is required by code-sync, and there is no
 41# pip or python3-venv because uv covers both.
 42RUN apt-get update && \
 43    apt-get install -y --no-install-recommends ca-certificates curl && \
 44    install -m 0755 -d /etc/apt/keyrings && \
 45    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
 46    chmod a+r /etc/apt/keyrings/docker.asc && \
 47    echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" \
 48        > /etc/apt/sources.list.d/docker.list && \
 49    apt-get update && \
 50    apt-get install -y --no-install-recommends \
 51        git rsync neovim openssh-client tmux \
 52        unzip xz-utils htop tree sudo jq restic sqlite3 poppler-utils \
 53        build-essential tzdata python3 \
 54        docker-ce-cli docker-compose-plugin docker-buildx-plugin
 55
 56RUN curl -fsSL https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh && \
 57    curl -fsSL https://bun.sh/install | bash
 58
 59# npm-published CLIs have a `#!/usr/bin/env node` shebang and die without this
 60# shim. `node --version` does not work under it, hence the real script.
 61RUN ln -sf /usr/local/bin/bun /usr/local/bin/node && \
 62    echo 'console.log("node shim ok")' > /tmp/shim.js && \
 63    node /tmp/shim.js && rm /tmp/shim.js
 64
 65# Upstream tarball, not apt, which lags releases.
 66ARG GO_VERSION=1.27.0
 67ARG GO_SHA256=675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685
 68RUN curl -fsSLO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" && \
 69    echo "${GO_SHA256}  go${GO_VERSION}.linux-amd64.tar.gz" | sha256sum -c - && \
 70    tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" && \
 71    rm "go${GO_VERSION}.linux-amd64.tar.gz" && \
 72    go version
 73
 74# Pinned to the version orchard's sites use. musl is the only linux x86_64 build
 75# typst ships, and it is static so it runs fine on glibc.
 76ARG TYPST_VERSION=0.14.0
 77ARG TYPST_SHA256=99816d2982de08d2b091bac56b59b2faa523a10e1378ad3cdd68e35b8eb74b3d
 78RUN curl -fsSL -o /tmp/typst.tar.xz \
 79      "https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-x86_64-unknown-linux-musl.tar.xz" && \
 80    echo "${TYPST_SHA256}  /tmp/typst.tar.xz" | sha256sum -c - && \
 81    tar -xJf /tmp/typst.tar.xz -C /tmp && \
 82    mv /tmp/typst-x86_64-unknown-linux-musl/typst /usr/local/bin/typst && \
 83    rm -rf /tmp/typst.tar.xz /tmp/typst-x86_64-unknown-linux-musl && \
 84    typst --version
 85
 86# Debian does not package gh, and GitHub's apt repo would be a second keyring
 87# for one binary.
 88ARG GH_VERSION=2.98.0
 89ARG GH_SHA256=3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de
 90RUN curl -fsSL -o /tmp/gh.tar.gz \
 91      "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" && \
 92    echo "${GH_SHA256}  /tmp/gh.tar.gz" | sha256sum -c - && \
 93    tar -xzf /tmp/gh.tar.gz -C /tmp && \
 94    mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
 95    rm -rf /tmp/gh.tar.gz "/tmp/gh_${GH_VERSION}_linux_amd64" && \
 96    gh --version
 97
 98# The pin matters. 0.1.14 drives a page, while 0.1.18 and unpinned both die with
 99# "No usable sandbox" under Docker Desktop on WSL2, so drive a real page after
100# any bump. Some tools want Chrome at the /opt path, hence the symlink.
101ARG PLAYWRIGHT_CLI_VERSION=0.1.14
102ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
103RUN bun install -g @playwright/cli@${PLAYWRIGHT_CLI_VERSION} && \
104    playwright-cli --help >/dev/null && \
105    CORE="$(find /usr/local/install/global -path '*/playwright-core/cli.js' | head -1)" && \
106    test -n "$CORE" && \
107    node "$CORE" install --with-deps chromium && \
108    chmod -R a+rx /opt/playwright-browsers && \
109    mkdir -p /opt/google/chrome && \
110    ln -snf /opt/playwright-browsers/chromium-*/chrome-linux64/chrome /opt/google/chrome/chrome && \
111    test -x /opt/google/chrome/chrome && \
112    rm -rf /var/lib/apt/lists/*
113
114RUN ln -snf /usr/share/zoneinfo/UTC /etc/localtime && echo "UTC" > /etc/timezone
115
116# Debian's /etc/profile assigns PATH outright, so a login shell (what tmux panes
117# get) drops everything the ENV added. The ENV still covers plain `docker exec`.
118RUN printf '%s\n' \
119    'export PATH="/home/dev/.local/bin:/home/dev/scripts:/usr/local/go/bin:/home/dev/go/bin:$PATH"' \
120    > /etc/profile.d/10-dev-path.sh && chmod 644 /etc/profile.d/10-dev-path.sh
121
122# Everything in the bythewood-* volumes is owned by 1001 and Debian has no
123# default 1000 user, so an unpinned useradd takes 1000 and every volume mounts
124# owned by a stranger. Do not remove the -u.
125RUN useradd -m -u 1001 -G sudo -s /bin/bash dev && \
126    echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
127
128COPY dotfiles/bash_aliases /home/dev/.bash_aliases
129COPY dotfiles/gitconfig /home/dev/.gitconfig
130COPY dotfiles/neovim/init.lua /home/dev/.config/nvim/init.lua
131COPY dotfiles/tmux.conf /home/dev/.tmux.conf
132COPY containers/webdev/scripts/ /home/dev/scripts/
133
134# The file tree sidebar, as a native package so there is no plugin manager and
135# nothing to fetch on first run. Pinned because nvim here is 0.10, which is the
136# oldest release nvim-tree still supports.
137RUN git clone --quiet --depth 1 --branch v1.18.0 \
138        https://github.com/nvim-tree/nvim-tree.lua.git \
139        /home/dev/.config/nvim/pack/dev/start/nvim-tree.lua && \
140    rm -rf /home/dev/.config/nvim/pack/dev/start/nvim-tree.lua/.git
141
142# .claude.json lives in the volume so auth survives a rebuild.
143RUN mkdir -p /home/dev/code /home/dev/.claude /home/dev/.ssh /home/dev/.restic && \
144    for f in /home/dev/scripts/*.sh; do mv "$f" "${f%.sh}"; done && \
145    chmod +x /home/dev/scripts/* && \
146    echo "source ~/.bash_aliases" >> /home/dev/.bashrc && \
147    ln -s /home/dev/.claude/.claude.json /home/dev/.claude.json && \
148    ln -s code/CLAUDE.md /home/dev/CLAUDE.md && \
149    printf '%s\n' \
150    'Host *' \
151    '  IdentityFile ~/.ssh/home_key' \
152    '  IdentitiesOnly yes' \
153    '  StrictHostKeyChecking accept-new' \
154    '  UpdateHostKeys yes' \
155    '  HashKnownHosts yes' \
156    '  PasswordAuthentication no' \
157    '  ServerAliveInterval 60' \
158    '  ServerAliveCountMax 3' \
159    '  VisualHostKey yes' \
160    > /home/dev/.ssh/config && \
161    chmod 700 /home/dev/.ssh /home/dev/.restic && \
162    chmod 600 /home/dev/.ssh/config && \
163    chown -R dev:dev /home/dev
164
165WORKDIR /home/dev
166USER dev
167
168RUN curl -fsSL https://claude.ai/install.sh | bash
169
170CMD ["sleep", "infinity"]