feat: add docker-build-nix action for reproducible OCI images

Add reusable action for building Docker images with Nix flakes:
- Full reproducibility with Nix derivations
- Attic cache integration for build artifacts
- Optimized layering with dockerTools.buildLayeredImage
- Automatic Nix binary cache usage

Use this instead of docker-build when you want:
- Bit-for-bit identical builds
- Better caching via Attic/Nix
- Smaller, optimized images

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Christopher Mühl 2026-03-04 12:11:29 +01:00
parent ac56aac0a7
commit b163ffa64b
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
2 changed files with 231 additions and 0 deletions

143
docker-build-nix/README.md Normal file
View file

@ -0,0 +1,143 @@
# docker-build-nix
Build and push Docker/OCI images generated by Nix flakes, with Attic cache integration.
## Features
- **Reproducible builds**: Uses Nix flakes for bit-for-bit identical images
- **Attic caching**: Pushes build artifacts to your Attic cache for faster subsequent builds
- **Nix store optimization**: Leverages Nix's existing binary caches (nixos.org + your Attic)
- **Smaller images**: Nix `dockerTools.buildLayeredImage` creates optimized layers
## Usage
### Basic example
```yaml
name: Build Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: nix # Requires runner with Nix
steps:
- uses: actions/checkout@v4
- uses: https://git.toph.so/toph/ci-actions/docker-build-nix@main
with:
flake-output: .#dojo-image
image-name: toph/dojo
registry-password: ${{ secrets.GITEA_TOKEN }}
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
```
### Multiple images
```yaml
jobs:
build-web:
runs-on: nix
steps:
- uses: actions/checkout@v4
- uses: https://git.toph.so/toph/ci-actions/docker-build-nix@main
with:
flake-output: .#dojo-image
image-name: toph/dojo
image-tag: main
registry-password: ${{ secrets.GITEA_TOKEN }}
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
build-agent:
runs-on: nix
steps:
- uses: actions/checkout@v4
- uses: https://git.toph.so/toph/ci-actions/docker-build-nix@main
with:
flake-output: .#agent-image
image-name: toph/dojo/agent
image-tag: main
registry-password: ${{ secrets.GITEA_TOKEN }}
env:
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
```
## Inputs
| Input | Required | Default | Description |
|-------|----------|---------|-------------|
| `flake-output` | ✅ | - | Nix flake output (e.g., `.#dojo-image`) |
| `image-name` | ✅ | - | Target image name (e.g., `user/repo`) |
| `registry-password` | ✅ | - | Registry password/token |
| `image-tag` | ❌ | `main` | Image tag |
| `registry` | ❌ | `git.toph.so` | Docker registry |
| `registry-username` | ❌ | `${{ gitea.actor }}` | Registry username |
| `cache-name` | ❌ | `ci` | Attic cache name |
| `attic-endpoint` | ❌ | `https://cache.toph.so` | Attic endpoint |
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `ATTIC_TOKEN` | Optional | Attic token for pushing to cache. If not set, skips cache push. |
## How It Works
1. **Build with Nix**: Runs `nix build <flake-output>` which uses Nix caching
2. **Push to Attic**: Uploads the entire build closure to your Attic cache
3. **Load to Docker**: Loads the OCI tarball into local Docker daemon
4. **Tag & Push**: Tags and pushes to your Docker registry
## Example Flake
Your `flake.nix` should export image outputs:
```nix
{
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux = {
# Your app
default = pkgs.callPackage ./package.nix { };
# OCI image
dojo-image = pkgs.dockerTools.buildLayeredImage {
name = "dojo";
tag = "latest";
contents = [ self.packages.x86_64-linux.default ];
config = {
Cmd = [ "${self.packages.x86_64-linux.default}/bin/dojo" ];
};
};
};
};
}
```
## Prerequisites
- **Nix runner**: Runner with `nix` label using `docker://registry.toph.so/nix-runner:latest`
- **Attic cache**: Optional but recommended for caching (`cache.toph.so`)
- **ATTIC_TOKEN**: Set in Forgejo secrets if you want cache push
- **GITEA_TOKEN**: Auto-available in Forgejo Actions
## Comparison with docker-build
| Feature | docker-build | docker-build-nix |
|---------|-------------|------------------|
| Build method | Docker | Nix |
| Reproducibility | Layer-dependent | Byte-for-byte |
| Caching | S3 layers | Attic derivations |
| Image size | Larger | Optimized |
| Runner | Any | Requires Nix |
## See Also
- [docker-build](../docker-build/) - Traditional Docker builds with S3 cache
- [push-nix-cache](../push-nix-cache/) - Push arbitrary Nix paths to Attic

View file

@ -0,0 +1,88 @@
name: Build and Push Docker Image from Nix
description: Build OCI image with Nix flake, push to registry with Attic caching
inputs:
flake-output:
description: 'Nix flake output for the OCI image (e.g., .#dojo-image)'
required: true
image-name:
description: 'Target image name in registry (e.g., git.toph.so/user/repo)'
required: true
image-tag:
description: 'Image tag'
required: false
default: 'main'
registry:
description: 'Docker registry'
required: false
default: 'git.toph.so'
registry-username:
description: 'Registry username'
required: false
default: ${{ gitea.actor }}
registry-password:
description: 'Registry password/token'
required: true
cache-name:
description: 'Attic cache name to push build artifacts'
required: false
default: 'ci'
attic-endpoint:
description: 'Attic cache endpoint'
required: false
default: 'https://cache.toph.so'
runs:
using: composite
steps:
- name: Build OCI image with Nix
shell: bash
run: |
echo "Building ${{ inputs.flake-output }}..."
nix build "${{ inputs.flake-output }}" --print-build-logs
- name: Push build artifacts to Attic cache
shell: bash
if: env.ATTIC_TOKEN != ''
env:
ATTIC_TOKEN: ${{ env.ATTIC_TOKEN }}
run: |
# Configure attic client
mkdir -p ~/.config/attic
cat > ~/.config/attic/config.toml <<EOF
[servers.cache]
endpoint = "${{ inputs.attic-endpoint }}"
token = "${ATTIC_TOKEN}"
EOF
# Push entire closure to cache
attic push "${{ inputs.cache-name }}" ./result
- name: Load image into Docker
shell: bash
run: |
echo "Loading OCI image into Docker..."
docker load < ./result
- name: Tag and push to registry
shell: bash
run: |
# Extract image name from the loaded output
IMAGE_ID=$(docker images --format "{{.Repository}}:{{.Tag}}" | head -n1)
echo "Loaded image: $IMAGE_ID"
# Tag with target name
TARGET_IMAGE="${{ inputs.registry }}/${{ inputs.image-name }}:${{ inputs.image-tag }}"
echo "Tagging as: $TARGET_IMAGE"
docker tag "$IMAGE_ID" "$TARGET_IMAGE"
# Login and push
echo "${{ inputs.registry-password }}" | docker login ${{ inputs.registry }} -u ${{ inputs.registry-username }} --password-stdin
docker push "$TARGET_IMAGE"