Compare commits

...

2 Commits

Author SHA1 Message Date
e0e6046afe devcontainer: add doc, user to match host machine, and scripts to build/start
Some checks failed
CI / build-docker (push) Failing after 0s
CI / build-docker-bootstrap (push) Failing after 1m40s
CI / release-test (push) Has been skipped
2026-01-10 13:39:41 -08:00
381d648be0 doc: remove some completed TODO items 2025-12-12 23:19:44 -08:00
5 changed files with 98 additions and 9 deletions

View File

@ -5,14 +5,6 @@ TODO: auto-timestamps
- SaveXyz shouldn't set created_at in the do-update branch
- GetXyzByID should include `ErrItemIsDeleted` if item is soft-deleted
TODO: primary-key
- SaveXyz shouldn't set primary key if it's rowid
TODO: foreign-key
- Generated types should match foreign keys to the type of the column they point to
TODO: modified-timestamps
- set updated_at and created_at in SaveXYZ
- soft delete option
TODO: the `db_meta` table doesn't pass sqlite_lint

63
doc/using-devcontainer.md Normal file
View File

@ -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.

View File

@ -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}

9
ops/devcontainer/build.sh Executable file
View File

@ -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"

12
ops/devcontainer/start.sh Executable file
View File

@ -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