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:
parent
ac56aac0a7
commit
b163ffa64b
2 changed files with 231 additions and 0 deletions
143
docker-build-nix/README.md
Normal file
143
docker-build-nix/README.md
Normal 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
|
||||||
88
docker-build-nix/action.yaml
Normal file
88
docker-build-nix/action.yaml
Normal 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"
|
||||||
Loading…
Add table
Reference in a new issue