init: Prove the concept of reproducible builds

This commit is contained in:
Matthew Binning 2025-12-12 14:34:46 -08:00
commit b957af3edb
No known key found for this signature in database
9 changed files with 2075 additions and 0 deletions

172
README.md Normal file
View file

@ -0,0 +1,172 @@
# 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
```bash
# 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)
#### Approach A: `build-docker-image` (Recommended)
- 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`:
```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:
```yaml
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:
```yaml
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