173 lines
5.2 KiB
Markdown
173 lines
5.2 KiB
Markdown
|
|
# 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
|