Nix container build for nettest #214

Merged
eblume merged 9 commits from feature/nettest-nix-container into main 2026-02-19 08:42:59 -08:00
10 changed files with 244 additions and 130 deletions

View file

@ -1,17 +1,17 @@
# Nix container build workflow
# Triggers on tags matching: <container>-nix-v<version>
# Builds from containers/<container>/default.nix using nix build
# Pushes to Zot registry via skopeo
# Triggers on tags matching: <container>-v<version>
# Builds from containers/<container>/default.nix if it exists, skips otherwise
# Pushes to Zot registry via skopeo with -nix image tag suffix
#
# Examples:
# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix
# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix
# nettest-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix
# devpi-v2.1.0 -> skips (no default.nix)
name: Build Container (Nix)
on:
push:
tags:
- '*-nix-v[0-9]*'
- '*-v[0-9]*'
jobs:
build:
@ -23,10 +23,10 @@ jobs:
TAG="${GITHUB_REF_NAME}"
echo "Tag: $TAG"
# Extract container name (everything before -nix-v)
# e.g., "nettest-nix-v1.0.0" -> "nettest"
CONTAINER="${TAG%-nix-v[0-9]*}"
VERSION="${TAG#"${CONTAINER}"-nix-}"
# Extract container name (everything before -v)
# e.g., "nettest-v1.0.0" -> "nettest", "my-app-v2.0.0" -> "my-app"
CONTAINER="${TAG%-v[0-9]*}"
VERSION="${TAG#"${CONTAINER}"-}"
echo "container=$CONTAINER" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@ -46,33 +46,29 @@ jobs:
echo "Found $CONTEXT/default.nix"
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "No default.nix found at $CONTEXT/default.nix"
echo "No default.nix found at $CONTEXT/default.nix — skipping"
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip if container not found
if: steps.check.outputs.exists != 'true'
- name: Resolve nixpkgs
if: steps.check.outputs.exists == 'true'
id: nixpkgs
run: |
echo "========================================"
echo "Nix container not found: ${{ steps.parse.outputs.container }}"
echo "========================================"
echo ""
echo "Tag '${{ github.ref_name }}' does not match any nix container in containers/"
echo ""
echo "Available nix containers:"
for nix in containers/*/default.nix; do
[ -f "$nix" ] && echo " - $(basename "$(dirname "$nix")")"
done
echo ""
echo "Skipping build."
# Resolve nixpkgs from the flake registry for <nixpkgs> lookup
NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path')
echo "Resolved nixpkgs: $NIXPKGS_PATH"
echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT"
- name: Build with nix
if: steps.check.outputs.exists == 'true'
id: build
env:
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
run: |
CONTAINER="${{ steps.parse.outputs.container }}"
echo "Building containers/$CONTAINER/default.nix"
nix build -f "containers/$CONTAINER/default.nix" -o result
echo "NIX_PATH=$NIX_PATH"
nix-build "containers/$CONTAINER/default.nix" -o result
echo "Build complete: $(readlink result)"
- name: Push to registry
@ -80,7 +76,7 @@ jobs:
run: |
CONTAINER="${{ steps.parse.outputs.container }}"
VERSION="${{ steps.parse.outputs.version }}"
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION"
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION-nix"
echo "Pushing to $IMAGE"
skopeo copy \

View file

@ -17,7 +17,6 @@ on:
jobs:
build:
if: "!contains(github.ref_name, '-nix-v')"
runs-on: k8s
steps:
- name: Parse tag

View file

@ -0,0 +1,39 @@
# Nix-built nettest container
# Equivalent to the Dockerfile: curl, jq, bind (nslookup), ca-certs, bash
# Built with dockerTools.buildLayeredImage for efficient layer caching
{ pkgs ? import <nixpkgs> { } }:
let
testScript = ./test-connectivity.sh;
tools = pkgs.buildEnv {
name = "nettest-tools";
paths = [
pkgs.curl
pkgs.jq
pkgs.dnsutils # provides nslookup, dig
pkgs.cacert
pkgs.coreutils
pkgs.hostname
pkgs.bashInteractive
];
};
in
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/nettest";
tag = "latest";
contents = [ tools ];
extraCommands = ''
cp ${testScript} test-connectivity.sh
chmod +x test-connectivity.sh
'';
config = {
Entrypoint = [ "/bin/bash" "/test-connectivity.sh" ];
Env = [
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
];
};
}

View file

@ -0,0 +1 @@
Added Nix container build for nettest, validating the full nix-container-builder pipeline on ringtail. One git tag now triggers both Dockerfile and Nix workflows — each skips if its build file is absent. Rewrote container-tag-and-release as a typer CLI with --dry-run support. Added container policy.json and registries.conf to ringtail for skopeo.

View file

@ -1,6 +1,6 @@
---
title: Build Container Image
modified: 2026-02-15
modified: 2026-02-19
last-reviewed: 2026-02-15
tags:
- how-to
@ -14,30 +14,35 @@ How to create a custom container image in BlumeOps, build it locally, and releas
## Prerequisites
- [Dagger CLI](https://docs.dagger.io/install) installed locally
- A Dockerfile for the service you want to build
- [Dagger CLI](https://docs.dagger.io/install) installed locally (for Dockerfile builds)
- A `Dockerfile` and/or `default.nix` for the service
## 1. Create the container directory
Add a `Dockerfile` (and any supporting files) under `containers/<name>/`:
Add build files under `containers/<name>/`:
```
containers/<name>/
├── Dockerfile
├── Dockerfile (built by Dagger on the k8s runner)
├── default.nix (built by nix-build on the ringtail runner)
└── (optional scripts, configs)
```
The directory name becomes the image name: `registry.ops.eblu.me/blumeops/<name>`.
A container can have one or both build files. The directory name becomes the image name: `registry.ops.eblu.me/blumeops/<name>`.
## 2. Build locally
Test your image with Dagger:
**Dockerfile** — test with Dagger:
```bash
dagger call build --src=. --container-name=<name>
```
This builds `containers/<name>/Dockerfile` using the Dagger `docker_build()` function. Fix any build errors before proceeding.
**Nix** — test with nix-build (requires nix, e.g. on [[ringtail]]):
```bash
nix-build containers/<name>/default.nix -o result
```
## 3. Release
@ -47,7 +52,14 @@ Once the image builds cleanly, create a tagged release:
mise run container-tag-and-release <name> v1.0.0
```
This creates a git tag `<name>-v1.0.0` and pushes it. The `build-container` Forgejo workflow triggers on the tag, builds the image via Dagger, and publishes it to the registry as `registry.ops.eblu.me/blumeops/<name>:v1.0.0`.
Use `--dry-run` to preview without creating tags.
This creates a single git tag `<name>-v1.0.0` and pushes it. Both Forgejo workflows trigger on the tag — each checks for its build file and skips if not present:
| Build file | Workflow | Runner | Registry tag |
|------------|----------|--------|--------------|
| `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:v1.0.0` |
| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:v1.0.0-nix` |
Check available images and tags with:
@ -76,6 +88,7 @@ Existing containers demonstrate several build approaches:
| Multi-stage with Node + Go | [[#navidrome]] | Separate UI and backend build stages |
| Multi-stage Elixir | [[#teslamate]] | Elixir release with Node assets |
| Runtime tarball download | [[#kiwix-serve]] | Download pre-built binary with arch detection |
| Nix `dockerTools` | [[#nettest-nix]] | `buildLayeredImage` with nixpkgs tools |
### transmission
@ -97,6 +110,10 @@ Existing containers demonstrate several build approaches:
`containers/kiwix-serve/Dockerfile` — Downloads a pre-built binary from upstream, with architecture detection for cross-platform support.
### nettest (nix)
`containers/nettest/default.nix` — Uses `dockerTools.buildLayeredImage` with `buildEnv` to merge nixpkgs tools (curl, jq, dnsutils, bash). Runs alongside the existing Dockerfile; the nix variant is tagged `:version-nix` in the registry.
## Related
- [[deploy-k8s-service]] — Deploying the service that uses the image

View file

@ -1,6 +1,6 @@
---
title: Ringtail
modified: 2026-02-18
modified: 2026-02-19
tags:
- infrastructure
- host
@ -81,7 +81,7 @@ argocd cluster add default --name k3s-ringtail
### Forgejo Actions Runner
A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix build` and pushes them to Zot via `skopeo`.
A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix-build` and pushes them to Zot via `skopeo`.
| Property | Value |
|----------|-------|
@ -89,6 +89,9 @@ A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd servi
| **Execution** | Host (no containers) |
| **Token** | `/etc/forgejo-runner/token.env` (provisioned by Ansible) |
| **Service unit** | `gitea-runner-nix_container_builder.service` |
| **Host packages** | bash, coreutils, curl, gawk, git, gnused, jq, nodejs, wget, nix, skopeo |
The runner resolves `<nixpkgs>` from the flake registry at build time. Container trust policy (`/etc/containers/policy.json`) and registry search order (`/etc/containers/registries.conf`) are configured minimally in `configuration.nix` for skopeo — no full `virtualisation.containers` module needed.
## Maintenance Notes

View file

@ -1,6 +1,6 @@
---
title: Forgejo
modified: 2026-02-08
modified: 2026-02-19
tags:
- service
- git
@ -31,15 +31,20 @@ Git forge and CI/CD platform. **Primary source of truth for blumeops** (mirrored
## CI/CD (Forgejo Actions)
**Runner:** Kubernetes pod with Docker-in-Docker sidecar
- Namespace: `forgejo-runner`
- Labels: `k8s`
- ArgoCD app: `forgejo-runner`
**Runners:**
| Runner | Host | Labels | Purpose |
|--------|------|--------|---------|
| k8s DinD pod | [[indri]] (minikube) | `k8s` | Dockerfile builds via Dagger |
| ringtail-nix-builder | [[ringtail]] (native) | `nix-container-builder` | Nix builds via `nix-build` + `skopeo` |
**Workflows:** `.forgejo/workflows/`
- `build-container.yaml` - Container image builds on tag
- `build-container.yaml` - Dockerfile builds on tag (runs on `k8s`)
- `build-container-nix.yaml` - Nix builds on tag (runs on `nix-container-builder`)
- `build-blumeops.yaml` - Documentation builds and releases
Both container workflows trigger on the same tag pattern (`*-v[0-9]*`). Each checks for its build file (`Dockerfile` or `default.nix`) and skips if not present. See [[build-container-image]].
## Secrets (Forgejo Config)
Server configuration secrets managed via 1Password → Ansible:

View file

@ -14,20 +14,26 @@ echo ""
for dir in "$CONTAINER_DIR"/*/; do
[[ -d "$dir" ]] || continue
# Determine build type
if [[ -f "$dir/default.nix" ]]; then
build_type="nix"
elif [[ -f "$dir/Dockerfile" ]]; then
build_type="dockerfile"
else
continue
fi
# Determine available build types
has_dockerfile=false
has_nix=false
[[ -f "$dir/Dockerfile" ]] && has_dockerfile=true
[[ -f "$dir/default.nix" ]] && has_nix=true
# Skip directories with no build files
$has_dockerfile || $has_nix || continue
# Build type label
types=()
$has_dockerfile && types+=("dockerfile")
$has_nix && types+=("nix")
label=$(IFS=+; echo "${types[*]}")
# Extract container name from directory
container=$(basename "$dir")
image="blumeops/$container"
echo "[$build_type] $container"
echo "[$label] $container"
echo " Image: $REGISTRY/$image"
echo " Path: $dir"
@ -49,5 +55,7 @@ echo "---"
echo "To release a new version:"
echo " mise run container-tag-and-release <container> <version>"
echo ""
echo "One tag triggers all applicable workflows (dockerfile and/or nix)."
echo ""
echo "Example:"
echo " mise run container-tag-and-release nettest v1.0.0"

View file

@ -1,77 +1,114 @@
#!/usr/bin/env bash
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["typer>=0.15.0"]
# ///
#MISE description="Release a container image by creating a git tag"
#USAGE arg "<container>" help="Container name (directory under containers/)"
#USAGE arg "<version>" help="Version in vX.Y.Z format"
#USAGE flag "--dry-run" help="Show what would be done without creating tags"
"""Release a container image by creating a git tag that triggers CI builds.
set -euo pipefail
One tag triggers all applicable workflows:
- Dockerfile present -> Build Container workflow -> :v<version>
- default.nix present -> Build Container (Nix) workflow -> :v<version>-nix
"""
CONTAINER="${1:-}"
VERSION="${2:-}"
import re
import subprocess
import sys
from pathlib import Path
if [[ -z "$CONTAINER" || -z "$VERSION" ]]; then
echo "Usage: mise run container-tag-and-release <container> <version>"
echo ""
echo "Run 'mise run container-list' to see available containers and recent tags."
exit 1
fi
import typer
# Validate version format
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format vX.Y.Z (e.g. v1.0.0)"
exit 1
fi
REGISTRY = "registry.ops.eblu.me"
FORGE_ACTIONS = "https://forge.ops.eblu.me/eblume/blumeops/actions"
# Determine build type: Nix or Dockerfile
CONTAINER_DIR="containers/${CONTAINER}"
if [[ -f "$CONTAINER_DIR/default.nix" ]]; then
BUILD_TYPE="nix"
TAG="${CONTAINER}-nix-${VERSION}"
elif [[ -f "$CONTAINER_DIR/Dockerfile" ]]; then
BUILD_TYPE="dockerfile"
TAG="${CONTAINER}-${VERSION}"
else
echo "Error: No Dockerfile or default.nix found in '$CONTAINER_DIR'"
echo ""
echo "Available containers:"
for dir in containers/*/; do
[[ -d "$dir" ]] || continue
name=$(basename "$dir")
if [[ -f "$dir/default.nix" ]]; then
echo " - $name (nix)"
elif [[ -f "$dir/Dockerfile" ]]; then
echo " - $name (dockerfile)"
fi
done
exit 1
fi
app = typer.Typer(add_completion=False)
echo "Creating release tag: $TAG"
echo "Build type: $BUILD_TYPE"
echo ""
# Check if tag already exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Error: Tag '$TAG' already exists"
echo "Existing tags for $CONTAINER:"
git tag -l "${CONTAINER}-*v*" | sort -V | tail -5
exit 1
fi
def git(*args: str) -> str:
result = subprocess.run(
["git", *args], capture_output=True, text=True, check=True
)
return result.stdout.strip()
# Image name follows convention: blumeops/<container>
IMAGE="blumeops/${CONTAINER}"
echo "Container: $CONTAINER"
echo "Directory: $CONTAINER_DIR"
echo "Image: registry.ops.eblu.me/$IMAGE:$VERSION"
echo ""
def git_tag_exists(tag: str) -> bool:
result = subprocess.run(
["git", "rev-parse", tag], capture_output=True, text=True
)
return result.returncode == 0
# Create and push tag
git tag "$TAG"
git push origin "$TAG"
echo ""
echo "Tag '$TAG' created and pushed"
echo ""
echo "The workflow will now build and push:"
echo " registry.ops.eblu.me/$IMAGE:$VERSION"
echo ""
echo "Monitor the build at:"
echo " https://forge.ops.eblu.me/eblume/blumeops/actions"
def list_containers() -> None:
typer.echo("Available containers:")
for d in sorted(Path("containers").iterdir()):
if not d.is_dir():
continue
types = []
if (d / "Dockerfile").exists():
types.append("dockerfile")
if (d / "default.nix").exists():
types.append("nix")
if types:
typer.echo(f" - {d.name} ({', '.join(types)})")
@app.command()
def main(
container: str = typer.Argument(help="Container name (directory under containers/)"),
version: str = typer.Argument(help="Version in vX.Y.Z format"),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without creating tags"),
) -> None:
"""Release a container image by creating a git tag that triggers CI builds."""
if not re.match(r"^v\d+\.\d+\.\d+$", version):
typer.echo("Error: Version must be in format vX.Y.Z (e.g. v1.0.0)")
raise typer.Exit(1)
container_dir = Path("containers") / container
has_dockerfile = (container_dir / "Dockerfile").exists()
has_nix = (container_dir / "default.nix").exists()
if not has_dockerfile and not has_nix:
typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'")
typer.echo()
list_containers()
raise typer.Exit(1)
image = f"blumeops/{container}"
tag = f"{container}-{version}"
# Show what workflows will trigger
builds = []
if has_dockerfile:
builds.append(f" dockerfile -> {REGISTRY}/{image}:{version}")
if has_nix:
builds.append(f" nix -> {REGISTRY}/{image}:{version}-nix")
if dry_run:
typer.echo("[dry-run mode]")
typer.echo(f"Container: {container}")
typer.echo(f"Tag: {tag}")
typer.echo(f"Builds:")
for b in builds:
typer.echo(b)
typer.echo()
if git_tag_exists(tag):
typer.echo(f"Error: Tag '{tag}' already exists")
raise typer.Exit(1)
if dry_run:
typer.echo(f"[dry-run] Would create and push tag: {tag}")
else:
git("tag", tag)
git("push", "origin", tag)
typer.echo(f"Tag '{tag}' created and pushed")
typer.echo()
typer.echo(f"Monitor builds at: {FORGE_ACTIONS}")
if __name__ == "__main__":
app()

View file

@ -446,6 +446,15 @@ in
"d /mnt/storage2 0755 eblume users -"
];
# Container config for skopeo (used by the forgejo runner to push images)
# and for unqualified image pulls via Zot pull-through cache
environment.etc."containers/policy.json".text = builtins.toJSON {
default = [{ type = "insecureAcceptAnything"; }];
};
environment.etc."containers/registries.conf".text = ''
unqualified-search-registries = ["registry.ops.eblu.me", "docker.io", "ghcr.io", "quay.io"]
'';
# Forgejo Actions runner (nix container builder)
services.gitea-actions-runner = {
package = pkgs.forgejo-runner;
@ -456,7 +465,7 @@ in
tokenFile = "/etc/forgejo-runner/token.env";
labels = [ "nix-container-builder:host" ];
hostPackages = with pkgs; [
bash coreutils curl gawk gitMinimal gnused nodejs wget
bash coreutils curl gawk gitMinimal gnused jq nodejs wget
nix skopeo
];
settings = {