π ncps: Nix Cache Proxy Server
A high-performance proxy server that accelerates Nix dependency retrieval across your local network by caching and serving packages locally.

π Table of Contents
π― Overview
ncps acts as a local binary cache for Nix, fetching store paths from upstream caches (like cache.nixos.org) and storing them locally. This reduces download times and bandwidth usage, especially beneficial when multiple machines share the same dependencies.
π Problem & Solution
The Problem
When multiple machines running NixOS or Nix pull packages, they often download the same dependencies from remote caches, leading to:
- β Redundant downloads - Each machine downloads identical files
- β High bandwidth usage - Significant network traffic for large projects
- β Slower build times - Network latency impacts development velocity
The Solution
ncps solves these issues by acting as a centralized cache on your local network, dramatically reducing redundant downloads and improving build performance.
β¨ Key Features
| Feature |
Description |
| π Easy Setup |
Simple configuration and deployment |
| π Multi-Upstream |
Support for multiple upstream caches with failover |
| πΎ Smart Caching |
LRU cache management with configurable size limits |
| π Secure Signing |
Signs cached paths with private keys for integrity |
| π Monitoring |
OpenTelemetry support for centralized logging |
| ποΈ Compression |
Harmonia's transparent zstd compression support |
| πΎ Embedded Storage |
Built-in SQLite database for easy deployment |
βοΈ How It Works
sequenceDiagram
participant Client as Nix Client
participant NCPS as ncps Server
participant Cache as Local Cache
participant Upstream as Upstream Cache
Client->>NCPS: Request store path
NCPS->>Cache: Check local cache
alt Path exists locally
Cache-->>NCPS: Return cached path
NCPS-->>Client: Serve cached path
else Path not cached
NCPS->>Upstream: Fetch from upstream
Upstream-->>NCPS: Return store path
NCPS->>Cache: Cache and sign path
NCPS-->>Client: Serve downloaded path
end
- Request - Nix client requests a store path from ncps
- Cache Check - ncps checks if the path exists in local cache
- Upstream Fetch - If not cached, fetches from configured upstream caches
- Cache & Sign - Stores and signs the path with ncps private key
- Serve - Delivers the path to the requesting client
π Quick Start
Get ncps running quickly with Docker:
# Pull the images
docker pull alpine
docker pull kalbasit/ncps
# Create the storage volume
docker volume create ncps-storage
docker run --rm -v ncps-storage:/storage alpine /bin/sh -c \
"mkdir -m 0755 -p /storage/var && mkdir -m 0700 -p /storage/var/ncps && mkdir -m 0700 -p /storage/var/ncps/db"
# Initialize database
docker run --rm -v ncps-storage:/storage kalbasit/ncps /bin/dbmate --url=sqlite:/storage/var/ncps/db/db.sqlite migrate up
# Start the server
docker run -d --name ncps -p 8501:8501 -v ncps-storage:/storage kalbasit/ncps \
/bin/ncps serve \
--cache-hostname=your-ncps-hostname \
--cache-data-path=/storage \
--cache-database-url=sqlite:/storage/var/ncps/db/db.sqlite \
--upstream-cache=https://cache.nixos.org \
--upstream-public-key=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
Your cache will be available at http://localhost:8501 and the public key at http://localhost:8501/pubkey.
π¦ Installation
π³ Docker
Docker Setup
Step 1: Pull the image
docker pull kalbasit/ncps
Step 2: Initialize storage and database
docker volume create ncps-storage
docker run --rm -v ncps-storage:/storage alpine /bin/sh -c \
"mkdir -m 0755 -p /storage/var && mkdir -m 0700 -p /storage/var/ncps && mkdir -m 0700 -p /storage/var/ncps/db"
docker run --rm -v ncps-storage:/storage kalbasit/ncps /bin/dbmate --url=sqlite:/storage/var/ncps/db/db.sqlite migrate up
Step 3: Start the server
docker run -d \
--name ncps \
-p 8501:8501 \
-v ncps-storage:/storage \
kalbasit/ncps \
/bin/ncps serve \
--cache-hostname=your-ncps-hostname \
--cache-data-path=/storage \
--cache-database-url=sqlite:/storage/var/ncps/db/db.sqlite \
--upstream-cache=https://cache.nixos.org \
--upstream-cache=https://nix-community.cachix.org \
--upstream-public-key=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= \
--upstream-public-key=nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
π³ Docker Compose
Create a docker-compose.yml file:
services:
create-directories:
image: alpine:latest
volumes:
- ncps-storage:/storage
command: >
/bin/sh -c "
mkdir -m 0755 -p /storage/var &&
mkdir -m 0700 -p /storage/var/ncps &&
mkdir -m 0700 -p /storage/var/ncps/db
"
restart: "no"
migrate-database:
image: kalbasit/ncps:latest
depends_on:
create-directories:
condition: service_completed_successfully
volumes:
- ncps-storage:/storage
command: >
/bin/dbmate --url=sqlite:/storage/var/ncps/db/db.sqlite migrate up
restart: "no"
ncps:
image: kalbasit/ncps:latest
depends_on:
migrate-database:
condition: service_completed_successfully
ports:
- "8501:8501"
volumes:
- ncps-storage:/storage
command: >
/bin/ncps serve
--cache-hostname=your-ncps-hostname
--cache-data-path=/storage
--cache-database-url=sqlite:/storage/var/ncps/db/db.sqlite
--upstream-cache=https://cache.nixos.org
--upstream-cache=https://nix-community.cachix.org
--upstream-public-key=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
--upstream-public-key=nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
restart: unless-stopped
volumes:
ncps-storage:
Then run:
docker compose up -d
βΈοΈ Kubernetes
PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ncps
labels:
app: ncps
tier: proxy
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ncps
labels:
app: ncps
tier: proxy
spec:
replicas: 1
selector:
matchLabels:
app: ncps
tier: proxy
template:
metadata:
labels:
app: ncps
tier: proxy
spec:
initContainers:
- image: alpine:latest
name: create-directories
args:
- /bin/sh
- -c
- "mkdir -m 0755 -p /storage/var && mkdir -m 0700 -p /storage/var/ncps && mkdir -m 0700 -p /storage/var/ncps/db"
volumeMounts:
- name: ncps-persistent-storage
mountPath: /storage
- image: kalbasit/ncps:latest # NOTE: It's recommended to use a tag here!
name: migrate-database
args:
- /bin/dbmate
- --url=sqlite:/storage/var/ncps/db/db.sqlite
- migrate
- up
volumeMounts:
- name: ncps-persistent-storage
mountPath: /storage
containers:
- image: kalbasit/ncps:latest # NOTE: It's recommended to use a tag here!
name: ncps
args:
- /bin/ncps
- serve
- --cache-hostname=ncps.yournetwork.local # TODO: Replace with your own hostname
- --cache-data-path=/storage
- --cache-temp-path=/nar-temp-dir
- --cache-database-url=sqlite:/storage/var/ncps/db/db.sqlite
- --upstream-cache=https://cache.nixos.org
- --upstream-cache=https://nix-community.cachix.org
- --upstream-public-key=cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
- --upstream-public-key=nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
ports:
- containerPort: 8501
name: http-web
volumeMounts:
- name: ncps-persistent-storage
mountPath: /storage
- name: nar-temp-dir
mountPath: /nar-temp-dir
volumes:
- name: ncps-persistent-storage
persistentVolumeClaim:
claimName: ncps
- name: nar-temp-dir
emptyDir:
sizeLimit: 5Gi
Service
apiVersion: v1
kind: Service
metadata:
name: ncps
labels:
app: ncps
tier: proxy
spec:
type: ClusterIP
ports:
- name: http-web
port: 8501
selector:
app: ncps
tier: proxy
βοΈ Configuration
Global Options
| Option |
Description |
Environment Variable |
Default |
--otel-enabled |
Enable OpenTelemetry logs, metrics, and tracing |
OTEL_ENABLED |
false |
--prometheus-enabled |
Enable Prometheus metrics endpoint at /metrics |
PROMETHEUS_ENABLED |
false |
--log-level |
Set log level: debug, info, warn, error |
LOG_LEVEL |
info |
--otel-grpc-url |
OpenTelemetry gRPC URL (omit for stdout) |
OTEL_GRPC_URL |
- |
Server Configuration
π§ Essential Options
| Option |
Description |
Environment Variable |
Required |
--cache-hostname |
Cache hostname for key generation |
CACHE_HOSTNAME |
β
|
--cache-data-path |
Local storage directory |
CACHE_DATA_PATH |
β
|
--upstream-cache |
Upstream cache URL (repeatable) |
UPSTREAM_CACHES |
β
|
--upstream-public-key |
Upstream public key (repeatable) |
UPSTREAM_PUBLIC_KEYS |
β
|
| Option |
Description |
Environment Variable |
Default |
--cache-database-url |
Database URL (SQLite only) |
CACHE_DATABASE_URL |
embedded SQLite |
--cache-max-size |
Max cache size (5K, 10G, etc.) |
CACHE_MAX_SIZE |
unlimited |
--cache-lru-schedule |
Cleanup cron schedule |
CACHE_LRU_SCHEDULE |
- |
--cache-temp-path |
Temporary download directory |
CACHE_TEMP_PATH |
system temp |
π Security & Signing
| Option |
Description |
Environment Variable |
Default |
--cache-sign-narinfo |
Sign narInfo files |
CACHE_SIGN_NARINFO |
true |
--cache-secret-key-path |
Path to signing key |
CACHE_SECRET_KEY_PATH |
auto-generated |
--cache-allow-put-verb |
Allow PUT uploads |
CACHE_ALLOW_PUT_VERB |
false |
--cache-allow-delete-verb |
Allow DELETE operations |
CACHE_ALLOW_DELETE_VERB |
false |
π Network
| Option |
Description |
Environment Variable |
Default |
--server-addr |
Listen address and port |
SERVER_ADDR |
:8501 |
π§ Client Setup
Get Your Public Key
First, retrieve the public key from your running ncps instance:
curl http://your-ncps-hostname:8501/pubkey
NixOS Configuration
Add ncps to your configuration.nix:
nix.settings = {
substituters = [
"http://your-ncps-hostname:8501" # Use https:// if behind reverse proxy
"https://cache.nixos.org"
# ... other substituters
];
trusted-public-keys = [
"your-ncps-hostname=<paste-public-key-here>"
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
# ... other keys
];
};
Non-NixOS Configuration
Edit your nix.conf file (typically /etc/nix/nix.conf or ~/.config/nix/nix.conf):
substituters = http://your-ncps-hostname:8501 https://cache.nixos.org
trusted-public-keys = your-ncps-hostname=<paste-public-key-here> cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
π§ Troubleshooting
π³ Docker Issues
"no such table: nars" Error
Cause: Database not properly initialized
Solutions:
-
β
Run migration first:
docker run --rm -v ncps-storage:/storage kalbasit/ncps /bin/sh -c \
"mkdir -m 0755 -p /storage/var && mkdir -m 0700 -p /storage/var/ncps && mkdir -m 0700 -p /storage/var/ncps/db && /bin/dbmate --url=sqlite:/storage/var/ncps/db/db.sqlite migrate up"
-
β
Check database path consistency between migration and application
-
β
Verify directory permissions (0700 for database directory)
"unable to open database file" Error
Cause: Permissions or volume mounting issues
Solutions:
- β
Ensure storage volume is mounted to
/storage
- β
Check directory permissions
- β
For bind mounts, ensure host directory is writable
Cause: Missing required parameters
Required options:
- β
--cache-hostname
- β
--cache-data-path
- β
--cache-database-url
- β
At least one
--upstream-cache and --upstream-public-key
π General Issues
Cache Not Working
-
Check public key setup:
curl http://your-ncps-hostname:8501/pubkey
-
Verify Nix configuration:
nix show-config | grep substituters
nix show-config | grep trusted-public-keys
-
Test cache connectivity:
curl http://your-ncps-hostname:8501/nix-cache-info
- β
Check available disk space
- β
Monitor cache hit rates in logs
- β
Consider adjusting
--cache-max-size
- β
Review LRU cleanup schedule
π€ Contributing
Contributions are welcome! Here's how to get started:
Development Setup
-
Clone the repository:
git clone https://github.com/kalbasit/ncps.git
cd ncps
-
Start development server:
./dev-scripts/run.sh # Auto-restarts on changes
-
Submit your changes:
- π Open issues for bugs
- β¨ Submit pull requests for features
- π Improve documentation
Getting Help
π License
This project is licensed under the MIT License - see the LICENSE file for details.