Skip to content

ProjectInitiative/loft

Repository files navigation

Loft

Loft is a lightweight, client-only Nix binary cache uploader designed for S3-compatible storage like Garage or MinIO. The name "Loft" is a nod to its inspiration, Attic, and its primary backend target, Garage. It sits somewhere in between—a cozy loft between the attic and the garage.

While Attic is a fantastic, feature-rich solution, it requires a server-client setup that may be more than what's needed for simpler use cases. Loft fills a specific gap: providing the convenience of a client-side helper with some of Attic’s best features (like cache checking, native Nix bindings, and watching the Nix store) without the overhead of deploying and managing a server, users, and permissions.

It's designed for scenarios where you want more than just a raw S3 bucket but don't need a full-scale cache server. Think of it as the perfect tool for:

  • CI/CD pipelines: Quickly and efficiently pushing build artifacts to a cache.
  • Single-user setups: A simple way to manage your own binary cache.
  • Homelabs: An easy-to-deploy cache for your local network.

If you're looking for a straightforward, no-fuss way to manage a Nix cache on S3, Loft is for you.

Features

  • Direct S3 Upload: Uploads NARs directly to your S3 bucket.
  • Nix Store Watcher: Watches the /nix/store for new paths and automatically uploads them.
  • Multi-threaded Uploads: Uploads multiple NARs in parallel to speed up the process.
  • Closure Deduplication: Before uploading, it checks which paths in a closure already exist in the cache to avoid redundant work.
  • Initial Scan on Startup: Optionally scans existing Nix store paths on startup and uploads them if they are missing from the cache. This scan runs only once.
  • Automatic Retries for Failed Uploads: Automatically retries failed uploads (e.g., due to transient network issues) a few times before giving up.
  • Nix Store Path Signing: Supports signing uploaded Nix store paths with a provided Nix signing key, ensuring authenticity and integrity.

NixOS Integration

Loft provides a NixOS module to simplify configuration and deployment.

1. Add Loft to your Flake

First, add the Loft flake to the inputs of your system's flake.nix.

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    # Add the loft flake
    loft.url = "github:projectinitiative/loft";
  };

  outputs = { self, nixpkgs, loft, ... }: {
    nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        # Simply import the loft module. It handles the rest.
        loft.nixosModules.loft
      ];
    };
  };
}

2. Configure the Loft Service

In your configuration.nix (or a related file), you can now enable and configure the service.

# configuration.nix
{ pkgs, ... }:

{
  services.loft = {
    enable = true;
    package = pkgs.loft; # Or your own overlay package

    # --- S3 Configuration ---
    s3 = {
      bucket = "nix-cache";
      region = "us-east-1";
      endpoint = "http://172.16.1.50:31292"; # Or your S3 endpoint

      # It's highly recommended to use sops-nix or agenix for secrets
      accessKeyFile = "/path/to/your/s3-access-key";
      secretKeyFile = "/path/to/your/s3-secret-key";

      # Optional: Extra HTTP headers for every S3 request (e.g., Cloudflare Access).
      # Three methods, all of which are combined (not overridden):
      #
      # 1. Inline headers (non-secret, baked into the world-readable Nix store):
      extraHeaders = {
        "CF-Access-Client-Id" = "xxx";
        "CF-Access-Client-Secret" = "yyy";
      };

      # 2. File-based headers (secret, read at runtime — works with sops-nix/agenix):
      extraHeadersFile = {
        "CF-Access-Client-Id" = "/run/secrets/cf-access-id";
        "CF-Access-Client-Secret" = "/run/secrets/cf-access-secret";
      };
    };

    # 3. Or set LOFT_EXTRA_HEADER_* env vars directly on the systemd service.
    #    Underscores map to hyphens in header names.
    #    Example: LOFT_EXTRA_HEADER_CF_ACCESS_CLIENT_ID → "CF-Access-Client-Id"

    # --- Loft Service Configuration ---
    debug = false; # Enable debug logging
    localCachePath = "/var/lib/loft/cache.db";
    uploadThreads = 12;
    scanOnStartup = true;
    populateCacheOnStartup = false; # Populate local cache from S3 on startup
    compression = "zstd"; # "zstd" or "xz"

    # --- Path Signing ---
    signingKeyPath = "/path/to/your/nix-private-key";
    signingKeyName = "nix-cache";
    skipSignedByKeys = [
      "cache.nixos.org-1"
      "nix-community.cachix.org-1"
    ];

    # --- Pruning Configuration ---
    pruning = {
      enable = false;
      schedule = "00:00"; # Run pruning daily at midnight
      retentionDays = 30;
      # The following are optional:
      # maxSizeGb = 1000;
      # targetPercentage = 80;
    };

    # Use extraConfig for new or unlisted options to prevent module errors
    extraConfig = {
      loft = {
        # This will be merged into the final toml
        some_new_feature_flag = true;
      };
    };
  };
}

3. Important Notes on Configuration

When using the NixOS module, please be aware of the following:

  • localCachePath Location: Due to the security sandboxing of the systemd service, the localCachePath must be located within the /var/lib/loft directory. The service does not have permission to write to other locations on the filesystem. The default path is /var/lib/loft/cache.db, which is the recommended setting.

  • Extra Headers: The extraHeaders and extraHeadersFile options are combined. You can use both simultaneously — inline headers for non-secrets and file-based headers for secrets. All headers are merged into every S3 request.

Configuration

Loft is configured via a loft.toml file. The NixOS module generates this file for you. For other systems, you may need to create it manually.

Here's an example loft.toml that reflects the available options:

[s3]
bucket = "nix-cache"
region = "us-east-1"
endpoint = "http://172.16.1.50:31292"

# Optional: Extra HTTP headers for every S3 request (e.g., Cloudflare Access)
# Inline (non-secret):
# [s3.extra_headers]
# "CF-Access-Client-Id" = "xxx"
# "CF-Access-Client-Secret" = "yyy"

[loft]
upload_threads = 12
scan_on_startup = true
local_cache_path = ".direnv/cache.db"
compression = "zstd"
signing_key_path = "/run/secrets/nix-signing-key"
signing_key_name = "nix-cache"
skip_signed_by_keys = ["cache.nixos.org-1", "nix-community.cachix.org-1"]

# Optional: Pruning configuration
prune_enabled = false
prune_retention_days = 30
# prune_max_size_gb = 1000
# prune_target_percentage = 80
# prune_schedule = "24h"

Extra HTTP Headers

Loft supports adding custom HTTP headers to every S3 request, which is useful for authentication proxies like Cloudflare Access. There are three ways to provide them, all of which are merged together (not overridden):

Method Scope Use case
[s3.extra_headers] in TOML Config file Non-secret headers baked into config
LOFT_EXTRA_HEADER_* env vars Process environment Secrets (follows AWS_ACCESS_KEY_ID pattern)
extraHeadersFile in NixOS NixOS module Secrets from sops-nix/agenix files

Env var naming convention: LOFT_EXTRA_HEADER_ + header name with hyphens replaced by underscores. For example, LOFT_EXTRA_HEADER_CF_ACCESS_CLIENT_ID sets the CF-Access-Client-Id header.

NixOS wrapper: When using extraHeadersFile, the module's systemd wrapper reads each file at runtime and exports the value as the corresponding LOFT_EXTRA_HEADER_* env var before launching loft.

Implementation detail: Headers are injected via an SDK interceptor at the modify_before_transmit phase — after SigV4 signing. This is intentional: auth proxy headers like CF-Access-* are consumed and stripped by Cloudflare before the request reaches S3, so they must not be part of the AWS signature.

Command-line arguments

You can also use command-line arguments to override settings or perform one-off actions.

Usage: loft [OPTIONS]

Options:
  -c, --config <CONFIG>     Path to the configuration file [default: loft.toml]
      --debug               Enable debug logging
      --clear-cache         Clear the local cache
      --reset-initial-scan  Reset the initial scan complete flag
      --force-scan          Force a full scan, bypassing the local cache
      --populate-cache      Populate the local cache from S3
      --upload-path <PATH>  Manually upload a specific Nix store path
      --prune               Manually trigger pruning of old objects
  -h, --help                Print help
  -V, --version             Print version

About

Loft is a lightweight, client-only Nix binary cache uploader designed for S3-compatible storage like Garage or MinIO.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors