init: Prove the concept of reproducible builds
This commit is contained in:
commit
b957af3edb
9 changed files with 2075 additions and 0 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Rust
|
||||
target/
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
.direnv/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Build outputs
|
||||
build-output/
|
||||
105
.gitlab-ci.yml
Normal file
105
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
stages:
|
||||
- build
|
||||
- test
|
||||
- package
|
||||
|
||||
variables:
|
||||
# Use Docker-outside-of-Docker (DooD) by mounting the host's Docker socket
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
# Disable TLS as we're using the local socket
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
|
||||
# Build the Rust application using Nix
|
||||
build:
|
||||
stage: build
|
||||
image: nixos/nix:latest
|
||||
before_script:
|
||||
# Enable flakes and nix-command
|
||||
- mkdir -p ~/.config/nix
|
||||
- echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
|
||||
script:
|
||||
# Generate Cargo.lock if it doesn't exist
|
||||
- nix develop --command cargo generate-lockfile || true
|
||||
# Build the Rust application
|
||||
- nix build .#app
|
||||
# Copy the result for artifacts
|
||||
- mkdir -p build-output
|
||||
- cp -rL result/* build-output/ || cp result build-output/hello-world
|
||||
artifacts:
|
||||
paths:
|
||||
- build-output/
|
||||
- Cargo.lock
|
||||
expire_in: 1 hour
|
||||
tags:
|
||||
- test-ci-cd
|
||||
|
||||
# Test the application
|
||||
test:
|
||||
stage: test
|
||||
image: nixos/nix:latest
|
||||
before_script:
|
||||
- mkdir -p ~/.config/nix
|
||||
- echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
|
||||
script:
|
||||
# Run the application in a Nix shell with all dependencies
|
||||
- nix develop --command cargo test
|
||||
# You could also run the binary here if needed
|
||||
# - nix run .#app
|
||||
dependencies:
|
||||
- build
|
||||
tags:
|
||||
- test-ci-cd
|
||||
|
||||
# Build Docker image using Nix and load it into Docker (DooD pattern)
|
||||
build-docker-image:
|
||||
stage: package
|
||||
image: nixos/nix:latest
|
||||
services:
|
||||
# No docker:dind service - we'll use the host's Docker daemon
|
||||
before_script:
|
||||
# Install Docker CLI in the Nix container
|
||||
- nix-env -iA nixpkgs.docker
|
||||
# Enable flakes
|
||||
- mkdir -p ~/.config/nix
|
||||
- echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
|
||||
script:
|
||||
# Build the Docker image using Nix
|
||||
- nix build .#docker
|
||||
# Load the image into Docker daemon (running on host via socket)
|
||||
- docker load < result
|
||||
# Tag the image
|
||||
- docker tag hello-world:latest hello-world:${CI_COMMIT_SHORT_SHA}
|
||||
# Test run the container
|
||||
- docker run --rm hello-world:latest
|
||||
# Optional: Push to registry if configured
|
||||
# - docker tag hello-world:latest ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
|
||||
# - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
|
||||
dependencies:
|
||||
- build
|
||||
tags:
|
||||
- test-ci-cd
|
||||
# This job requires a GitLab runner with Docker socket access
|
||||
# The runner should have /var/run/docker.sock mounted
|
||||
|
||||
# Alternative: Build using Docker directly (DooD)
|
||||
build-docker-traditional:
|
||||
stage: package
|
||||
image: docker:latest
|
||||
services: [] # No dind service
|
||||
variables:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
before_script:
|
||||
# Verify Docker access
|
||||
- docker info
|
||||
script:
|
||||
# Build the Docker image
|
||||
- docker build -t hello-world:traditional-${CI_COMMIT_SHORT_SHA} .
|
||||
# Test run
|
||||
- docker run --rm hello-world:traditional-${CI_COMMIT_SHORT_SHA}
|
||||
dependencies:
|
||||
- build
|
||||
tags:
|
||||
- test-ci-cd
|
||||
only:
|
||||
- branches
|
||||
when: manual
|
||||
1531
Cargo.lock
generated
Normal file
1531
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "hello-world"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
neofetch = "0.2.0"
|
||||
figlet-rs = "0.1.5"
|
||||
tokio = { version = "1.42", features = ["full"] }
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
FROM nixos/nix:latest
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p ~/.config/nix && \
|
||||
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf && \
|
||||
nix build .#app
|
||||
|
||||
# Extract the built binary
|
||||
RUN cp -rL result/* /tmp/app/ || cp result /tmp/app/hello-world
|
||||
|
||||
# Use a minimal runtime image - no system dependencies needed!
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# All functionality is in Rust crates, no need for system binaries
|
||||
COPY --from=0 /tmp/app/ /usr/local/bin/
|
||||
|
||||
CMD ["/usr/local/bin/hello-world"]
|
||||
81
FIGLET_FONTS.md
Normal file
81
FIGLET_FONTS.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# FIGlet Font Examples
|
||||
|
||||
The `figlet-rs` crate supports multiple fonts. Here are some examples of how to use different fonts:
|
||||
|
||||
## Using Different Fonts
|
||||
|
||||
```rust
|
||||
use figlet_rs::FIGfont;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Standard font (built-in)
|
||||
let standard = FIGfont::standard()?;
|
||||
if let Some(fig) = standard.convert("Standard") {
|
||||
println!("{}", fig);
|
||||
}
|
||||
|
||||
// You can also load custom fonts from files
|
||||
// Download fonts from: http://www.figlet.org/fontdb.cgi
|
||||
|
||||
// Example with a custom font file:
|
||||
// let font = FIGfont::from_file("path/to/font.flf")?;
|
||||
// if let Some(fig) = font.convert("Custom") {
|
||||
// println!("{}", fig);
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Popular FIGlet Fonts
|
||||
|
||||
Some popular fonts you might want to try (download from figlet.org):
|
||||
|
||||
- **standard** - The default, classic font (built-in)
|
||||
- **banner** - Large block letters (similar to the Unix `banner` command)
|
||||
- **big** - Large letters
|
||||
- **block** - Blocky style
|
||||
- **bubble** - Rounded bubble letters
|
||||
- **digital** - Digital/LED style
|
||||
- **lean** - Slanted letters
|
||||
- **mini** - Compact font
|
||||
- **script** - Cursive style
|
||||
- **shadow** - Letters with shadows
|
||||
- **slant** - Slanted text
|
||||
- **small** - Small letters
|
||||
|
||||
## Example Output (Standard Font)
|
||||
|
||||
```
|
||||
_ _ _ _
|
||||
| | | | ___| | | ___
|
||||
| |_| |/ _ \ | |/ _ \
|
||||
| _ | __/ | | (_) |
|
||||
|_| |_|\___|_|_|\___/
|
||||
```
|
||||
|
||||
## Adding Custom Fonts to Your Project
|
||||
|
||||
1. Download fonts from http://www.figlet.org/fontdb.cgi
|
||||
2. Place `.flf` files in a `fonts/` directory in your project
|
||||
3. Load them with `FIGfont::from_file("fonts/yourfont.flf")`
|
||||
4. If using Nix, include the font files in your Docker image
|
||||
|
||||
## Alternative: Multiple Text Styles
|
||||
|
||||
You can also create multiple banner styles in your application:
|
||||
|
||||
```rust
|
||||
fn print_banner(text: &str) {
|
||||
let font = FIGfont::standard().unwrap();
|
||||
if let Some(figure) = font.convert(text) {
|
||||
println!("{}", figure);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_banner("HELLO");
|
||||
print_banner("WORLD");
|
||||
}
|
||||
```
|
||||
172
README.md
Normal file
172
README.md
Normal 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
|
||||
81
flake.nix
Normal file
81
flake.nix
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
description = "Hello World Rust application with neofetch and banner";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
};
|
||||
|
||||
# Build the Rust application
|
||||
rustApp = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "hello-world";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
nativeBuildInputs = [ rustToolchain ];
|
||||
buildInputs = [
|
||||
pkgs.xorg.libxcb
|
||||
];
|
||||
};
|
||||
|
||||
# Create a Docker image with the app and dependencies
|
||||
dockerImage = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "hello-world";
|
||||
tag = "latest";
|
||||
|
||||
contents = [
|
||||
rustApp
|
||||
pkgs.bashInteractive
|
||||
pkgs.coreutils
|
||||
];
|
||||
|
||||
config = {
|
||||
Cmd = [ "${rustApp}/bin/hello-world" ];
|
||||
Env = [
|
||||
"PATH=/bin"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
default = rustApp;
|
||||
app = rustApp;
|
||||
docker = dockerImage;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
rustToolchain
|
||||
pkgs.rust-analyzer
|
||||
pkgs.docker
|
||||
pkgs.xorg.libxcb
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Rust development environment loaded!"
|
||||
echo "Rust version: $(rustc --version)"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
58
src/main.rs
Normal file
58
src/main.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use figlet_rs::FIGfont;
|
||||
use neofetch::Neofetch;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create ASCII art banner using figlet-rs
|
||||
println!("\n");
|
||||
let font = FIGfont::standard()?;
|
||||
if let Some(figure) = font.convert("HELLO") {
|
||||
println!("{}", figure);
|
||||
}
|
||||
|
||||
println!("\n=== System Information ===\n");
|
||||
|
||||
// Get system information using neofetch crate
|
||||
let neofetch = Neofetch::new().await;
|
||||
|
||||
// Display system information
|
||||
if let Ok(os) = &neofetch.os {
|
||||
println!("OS: {}", os);
|
||||
}
|
||||
|
||||
if let Ok(host) = &neofetch.host {
|
||||
println!("Host: {}", host);
|
||||
}
|
||||
|
||||
if let Ok(kernel) = &neofetch.kernel {
|
||||
println!("Kernel: {}", kernel);
|
||||
}
|
||||
|
||||
if let Ok(uptime) = &neofetch.uptime {
|
||||
println!("Uptime: {}", uptime);
|
||||
}
|
||||
|
||||
if let Ok(shell) = &neofetch.shell {
|
||||
println!("Shell: {}", shell);
|
||||
}
|
||||
|
||||
if let Ok(terminal) = &neofetch.terminal {
|
||||
println!("Terminal: {}", terminal);
|
||||
}
|
||||
|
||||
if let Ok(cpu) = &neofetch.cpu {
|
||||
println!("CPU: {:?}", cpu);
|
||||
}
|
||||
|
||||
if let Some(gpu) = &neofetch.gpu {
|
||||
println!("GPU: {:?}", gpu);
|
||||
}
|
||||
|
||||
if let Ok(memory) = &neofetch.memory {
|
||||
println!("Memory: {}", memory);
|
||||
}
|
||||
|
||||
println!("\n=== Done! ===\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue