docker-outside-of-docker and nix with Forgejo.
Find a file
Matthew Binning 5378d3cdfa
fixup
2026-01-21 15:17:37 -08:00
.devcontainer ci: Experiment with JSON with comments 2025-12-15 09:32:41 -08:00
src init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
.gitignore init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
.gitlab-ci.yml fixup 2026-01-21 15:17:37 -08:00
Cargo.lock init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
Cargo.toml init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
Dockerfile Feature/ci-encdec 2026-01-16 16:22:12 -08:00
FIGLET_FONTS.md init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
flake.nix init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00
nix.dock Feature/ci-encdec 2026-01-16 16:22:12 -08:00
README.md init: Prove the concept of reproducible builds 2025-12-12 14:51:53 -08:00

Hello World - Docker-outside-of-Docker with Rust, Nix, and GitLab CI

This project demonstrates the Docker-outside-of-Docker (DooD) pattern using:

  • Rust for the application
  • Nix for reproducible dependency management
  • GitLab CI for automated builds

What is Docker-outside-of-Docker (DooD)?

Docker-outside-of-Docker is a pattern where CI/CD pipelines build Docker images by mounting the host's Docker socket (/var/run/docker.sock) into the CI container, rather than running Docker-in-Docker (DinD). This approach:

  • Avoids nested virtualization overhead
  • Shares the Docker layer cache with the host
  • Simplifies networking
  • ⚠️ Requires proper security considerations (the container has full Docker daemon access)

Project Structure

.
├── src/
│   └── main.rs          # Rust application that calls neofetch and banner
├── Cargo.toml           # Rust package manifest
├── flake.nix            # Nix flake for pinned dependencies
├── .gitlab-ci.yml       # GitLab CI pipeline
└── README.md            # This file

The Application

The Rust application (src/main.rs) is a simple hello-world program that:

  1. Uses the figlet-rs crate to display ASCII art text (banner-style)
  2. Uses the neofetch crate to gather and display system information

All functionality is implemented using native Rust crates, requiring no system binaries.

Nix Flake

The flake.nix file:

  • Pins exact versions of all dependencies (Rust toolchain, crates dependencies via Cargo.lock)
  • Defines how to build the Rust application
  • Creates a minimal Docker image with only the application binary and essential runtime dependencies
  • Provides a development shell for local development

Since we're using Rust crates (neofetch and figlet-rs) instead of system binaries, the Docker image is much smaller and more portable.

Building locally with Nix

# Enter the development environment
nix develop

# Build the application
nix build .#app

# Run the application
nix run .#app

# Build the Docker image
nix build .#docker

# Load the Docker image
docker load < result
docker run hello-world:latest

GitLab CI Pipeline

The .gitlab-ci.yml implements DooD with three stages:

1. Build Stage

  • Uses a Nix container to build the Rust application
  • Generates Cargo.lock if needed
  • Produces build artifacts

2. Test Stage

  • Runs tests in a Nix environment
  • Verifies the application works

3. Package Stage (Two Approaches)

  • Builds the Docker image using Nix's dockerTools.buildLayeredImage
  • Loads the image into the host's Docker daemon via DooD
  • Tests the container
  • Ready for pushing to a registry

Approach B: build-docker-traditional (Alternative)

  • Uses a traditional Dockerfile
  • Builds in two stages: Nix build + runtime image
  • Manual trigger only

GitLab Runner Configuration

For DooD to work, your GitLab Runner must be configured with Docker socket access:

Using Docker Executor

In /etc/gitlab-runner/config.toml:

[[runners]]
  name = "docker-runner"
  executor = "docker"
  [runners.docker]
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
    privileged = false  # DooD doesn't require privileged mode

Security Considerations

⚠️ Important: Mounting the Docker socket gives the container full control over the Docker daemon. Only use this on trusted runners. Consider:

  1. Using dedicated runners for DooD jobs
  2. Implementing runner token rotation
  3. Restricting which projects can use DooD runners
  4. Using rootless Docker on the host
  5. Implementing image scanning and security policies

Advantages of This Setup

  1. Reproducibility: Nix ensures exact dependency versions
  2. Efficiency: DooD avoids DinD overhead and shares layer cache
  3. Minimal Images: Rust crates eliminate system binary dependencies; Nix's buildLayeredImage creates optimized layers
  4. Type Safety: Rust provides compile-time guarantees
  5. Declarative: Everything is defined in code
  6. Portability: Pure Rust implementation works across platforms without requiring system tools

Environment Variables

The GitLab CI uses these key variables for DooD:

DOCKER_HOST: unix:///var/run/docker.sock  # Use host's Docker daemon
DOCKER_TLS_CERTDIR: ""                    # Disable TLS for local socket

Troubleshooting

"Cannot connect to Docker daemon"

  • Ensure the runner has /var/run/docker.sock mounted
  • Check socket permissions on the host
  • Verify the Docker daemon is running on the host

"Permission denied" on Docker socket

  • The container user needs access to the socket
  • Add the container user to the docker group on the host, or
  • Change socket permissions (less secure)

Nix build fails

  • Ensure experimental features are enabled in nix.conf
  • Check that Cargo.lock is generated before building
  • Verify network access for downloading dependencies

Alternative: Docker-in-Docker

If you prefer Docker-in-Docker instead of DooD, modify the CI config:

services:
  - docker:dind

variables:
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"

Note: DinD requires privileged: true in the runner config and has performance implications.

License

MIT