From e0e6046afe8208a41fd9616cc6deeaa9929d96bd Mon Sep 17 00:00:00 2001 From: wispem-wantex Date: Sat, 10 Jan 2026 13:39:41 -0800 Subject: [PATCH] devcontainer: add doc, user to match host machine, and scripts to build/start --- doc/using-devcontainer.md | 63 +++++++++++++++++++++++++++++++++++++ ops/devcontainer/Dockerfile | 15 ++++++++- ops/devcontainer/build.sh | 9 ++++++ ops/devcontainer/start.sh | 12 +++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 doc/using-devcontainer.md create mode 100755 ops/devcontainer/build.sh create mode 100755 ops/devcontainer/start.sh diff --git a/doc/using-devcontainer.md b/doc/using-devcontainer.md new file mode 100644 index 0000000..dac65cb --- /dev/null +++ b/doc/using-devcontainer.md @@ -0,0 +1,63 @@ +# Dev containers + +A container is a great development environment. However, they tend to underperform because: +1. it's easy to fall into the Docker Compose trap +1. people don't use Alpine + +Using a dev container has multiple benefits: + +- "infrastructure as code", a.k.a. . Your codebase itself (in the `ops/devcontainer` dir) defines explicitly defines all the tools and dependencies that you use, with . +- reproducibility: a few simple commands to create a clean working setup anywhere Docker is supported (i.e., anywhere) +- isolation: you have clean setup and teardown. You can install a bunch of crap to try it out, and you don't have to remember what it was so you can purge it afterward. Just delete the container. + +## Quick start + +Build the container: +```bash +ops/devcontainer/build.sh +``` + +Run the container: +```bash +ops/devcontainer/start.sh +``` + +## Concepts + +A dev container is meant to be short-lived, constantly thrown away and recreated as needed. This explicitly divides the filesystem into "keep" (the source tree and any useful artifacts / caches) and "throw away" (everything else). Frequently regenerating the container ensures that "your environment" never deviates too far from the Infrastructure As Code in your repo; it forces you to add any new tools to the Docker image build process. In service of this, using `docker run --rm [...]` is always recommended. + +Contrary to conventional container ideology, it is not necessary to keep your images tiny and minimize container layers at all costs. For example, conventional container ideology frequently suggests constructs like `RUN cmd1 && cmd2 && cmd3` rather than doing each `cmd` in its own `RUN` layer, in order to reduce the amount of layers generated from 3 to 1. These practices are optimized for massive horizontal deployments, where you have a gazillion containers and images, and resource usage is a big problem. Obviously, the GAS stack is the complete opposite; you want very few containers, ideally just 1 at most. So having *more* layers is actually better, because it speeds up rebuild times by avoiding very heavy, frequently rebuilt layers. It also makes the Dockerfile much easier to read. + +## Methodologies + +There's a few useful techniques and strategies when using dev containers: + +- user management +- volumes for code +- volumes for caching +- openrc services +- `--net host` + +### User management + +To make working in a dev container seamless, create a user on it that matches your host machine user (UID and GID). + +If you don't do this, Git will complain about conflicting ownership, and any tools or tests that create files will create them as "root", which then have to be constantly `chown`'d on the host. + +To make this work, it's necessary to have a `build.sh` script which passes the current user's UID as a build arg to the Docker build step. + +### Volumes + +Anything not in a volume (or built into the image) will be lost on container restart. I like to mount the codebase on `/code`. + +For compiled languages (or anything that needs to "build" the project, e.g., linters), mounting build cache directories can also be useful. + +### OpenRC + +OpenRC is much simpler than systemd. If you want to run background processes, or network services, making the root process OpenRC and writing an openrc service script is the best effort-to-value ratio. ChatGPT can help you write openrc service scripts. + +### `--net host` + +Because this is a dev container, it's meant to make your life easier, not get you tangled up in security best-practices and so forth. One of the biggest annoyances of using containers is having to do port mapping, which leads to an explosion of config. + +Using `docker run --net host [...]` makes the container use the host's networking, instead of creating a virtual network that you have to explicitly map ports back and forth between. diff --git a/ops/devcontainer/Dockerfile b/ops/devcontainer/Dockerfile index c46d7c0..1190867 100644 --- a/ops/devcontainer/Dockerfile +++ b/ops/devcontainer/Dockerfile @@ -2,7 +2,20 @@ FROM alpine:3.22 -RUN apk add build-base git go sqlite shellcheck curl jq bash docker +RUN apk add build-base git go sqlite shellcheck curl jq sudo bash docker + +# Busybox `less` doesn't appear to support colors (makes git diff lose color) +RUN apk add less RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v2.0.2 RUN GOBIN=/usr/local/bin go install git.offline-twitter.com/offline-labs/gocheckout@v0.0.2 + +# Create a user in the container with the same UID as on the host machine, to avoid ownership conflicts. +# The user gets sudo of course. +ARG USERNAME +ARG UID +ARG GID +RUN addgroup -g ${GID} ${USERNAME} +RUN adduser -D -u ${UID} -G ${USERNAME} ${USERNAME} +RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} +USER ${USERNAME} diff --git a/ops/devcontainer/build.sh b/ops/devcontainer/build.sh new file mode 100755 index 0000000..a324616 --- /dev/null +++ b/ops/devcontainer/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) + +sudo docker build \ + --build-arg USERNAME="$(whoami)" \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" \ + -t gas "$SCRIPT_PATH" diff --git a/ops/devcontainer/start.sh b/ops/devcontainer/start.sh new file mode 100755 index 0000000..e7266fc --- /dev/null +++ b/ops/devcontainer/start.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +sudo docker run --rm -it \ + -v "$(pwd)":/code \ + -v "$(go env GOCACHE):/gocache-vol" \ + -e GOCACHE=/gocache-vol \ + -v "$(go env GOMODCACHE):/gocache-vol/mod-cache" \ + -e GOMODCACHE=/gocache-vol/mod-cache \ + -e GOLANGCI_LINT_CACHE=/gocache-vol/lint-cache \ + --workdir /code \ + --net host \ + gas