Nix Integration Reference¶
This document is the technical reference for deploying applications on Hop3 using Nix. For a tutorial-style introduction, see the Nix deployment guide.
Overview¶
Hop3 supports Nix-based deployments as an alternative to native buildpacks and Docker. Two modes are supported:
- Generated mode (ADR 008). Hop3 generates a
hop3.nixfile at build time from a[nix]section inhop3.toml, using one of the built-in templates. This is the preferred mode for most apps. - Hand-crafted mode. The user provides a
hop3.nixfile directly. Used when the templates don't fit, or when extracted viahop3 nix eject.
The two modes are mutually exclusive. If both a hop3.nix file
and a [nix].template section in hop3.toml are present, NixBuilder
raises Abort rather than silently picking one. The error message
points the user to either delete hop3.nix or remove the [nix]
section. To deliberately convert a template to a hand-crafted file,
use hop3 nix eject <app>.
Architecture¶
The NixBuilder is a Level 1 Builder in Hop3's two-level build architecture:
- Level 1 (Builders): Orchestrate how to build (LocalBuilder, DockerBuilder, NixBuilder)
- Level 2 (LanguageToolchains): Execute what to build (Python, Node, Ruby, etc.)
NixBuilder does not delegate to LanguageToolchains. All build
logic is encapsulated in the hop3.nix expression — either
hand-crafted or generated.
hop3.toml configuration¶
To use the NixBuilder, set the builder in hop3.toml:
For template-generated mode, also add a [nix] section. See the
hop3.toml reference for
the full schema.
Build process¶
When Hop3 deploys a Nix app:
- NixBuilder accepts the build if either condition holds:
- A
hop3.nixfile exists in the source directory, or - The
[nix]section inhop3.tomldeclares atemplate - Verifies
nix-buildis available - Resolves the Nix file:
- Hand-crafted mode: uses the existing
hop3.nix - Generated mode: generates a
hop3.nixfrom the[nix]section using the appropriate template - Runs:
nix-build hop3.nix -A package --no-out-link - Reads
$out/hop3/runtime.jsonfrom the built store path - Produces a
BuildArtifactwith: kind="nix"(orkind="static"for static-only apps)locationpointing to the Nix store pathruntimecontaining workers, env vars, and PATH fromruntime.json- Hands off to the deployer (uWSGI for
kind="nix", StaticDeployer forkind="static")
hop3.nix file format¶
The hop3.nix file is a standard Nix expression that evaluates to an
attribute set with:
| Attribute | Required | Description |
|---|---|---|
package |
Yes | A Nix derivation that builds the application |
env |
No | Static environment variables (attribute set) |
runtime.json contract¶
The built package must generate $out/hop3/runtime.json
containing runtime configuration:
{
"workers": {
"web": "/nix/store/.../bin/myapp --bind $BIND_ADDRESS:$PORT"
},
"env": {
"VAR_NAME": "value"
},
"path": [
"/nix/store/.../bin"
]
}
| Field | Required | Description |
|---|---|---|
workers |
Yes | Map of worker name to command |
env |
No | Environment variables to set at runtime |
path |
No | Paths to prepend to PATH |
Worker types¶
| Worker name | Behavior |
|---|---|
web |
Spawned as a uWSGI daemon. Must listen on $BIND_ADDRESS:$PORT. |
static |
Value is a directory path. Hop3 serves it via nginx (StaticDeployer). |
| Other names | Spawned as generic uWSGI daemons. |
If workers contains only the key "static", Hop3 produces a
BuildArtifact with kind="static" and the StaticDeployer takes
over. Otherwise the artifact has kind="nix" and uWSGI manages all
workers.
Variable substitution¶
Worker commands support shell variable expansion at runtime. The following variables are available:
$PORT— Port assigned by Hop3$BIND_ADDRESS— Bind address (default127.0.0.1)- All environment variables from
[env], addons, andruntime.json
Note: Nix ${...} interpolations in hop3.nix are evaluated by Nix
at build time and produce store paths. Shell $VAR expansions are
evaluated at runtime by the wrapper. To produce a literal ${VAR} in
the generated wrapper script, escape it as ''${VAR} inside Nix ''
strings.
Templates (generated mode)¶
Eight built-in templates cover common deployment patterns. Templates
are selected by setting template = "<name>" in the [nix] section
of hop3.toml.
| Template | Use case | Reproducibility tier |
|---|---|---|
nixpkgs-wrapper |
Apps already in nixpkgs | 1 (best) |
python-venv |
Python apps installed via pip into a virtualenv | 2 |
php-app |
PHP apps with Composer + extensions | 2 |
java-war |
Java WAR files served with a JDK | 1 (JDK from nixpkgs) |
ruby-bundler |
Ruby apps using bundlerEnv from gemset.nix |
2 |
prebuilt-binary |
Single binary from upstream releases | 3 (compromise) |
prebuilt-archive |
Multi-file archive from upstream releases | 3 (compromise) |
node-prebuilt |
Node.js apps shipped as a pre-built tarball | 3 (compromise) |
For the full field reference per template, see the
hop3.toml [nix] section.
Reproducibility tiers¶
Not all Nix builds are equally reproducible:
| Tier | Method | Reproducible | Auditable | Multi-arch |
|---|---|---|---|---|
| 1 | nixpkgs package (pkgs.foo) |
Yes | Yes | Yes |
| 2 | Source build with __noChroot (pip, composer) |
Mostly (depends on upstream registries) | Yes | Yes |
| 3 | Pre-built binary (fetchurl) |
Hash-pinned but not rebuildable from source | No | x86_64-linux only |
The goal is Tier 1 wherever possible. Tier 3 templates exist as a pragmatic shortcut for apps not yet in nixpkgs.
The nix eject command¶
Materializes the auto-generated hop3.nix from the template into a
real file in the app's source directory. After ejection:
- The committed
hop3.nixis used directly by NixBuilder - The
[nix]section inhop3.tomlis ignored - You can edit the file freely
The ejected file includes a header noting which template it came from and the date of ejection.
Use nix eject when:
- You need to add custom build logic the templates don't support
- You want to pin the generated Nix expression for reproducibility
- You want to commit the exact build recipe to version control
Nix installation¶
Nix is installed automatically by the Hop3 server installer when you
pass --with nix. It supports:
- Multi-user (daemon): Used when systemd is available. Provides better isolation.
- Single-user: Fallback for containers and non-systemd environments.
To manually install Nix on a Hop3 server:
Local development¶
Validate a Nix build¶
cd apps/real-apps-nix-gen/miniflux
# For template mode, generate first:
uv run python -c "
from hop3.plugins.build.nix.gen import generate
from hop3.plugins.build.nix.gen.toml_adapter import app_spec_from_config
import tomllib
from pathlib import Path
config = tomllib.loads(Path('hop3.toml').read_text())
spec = app_spec_from_config(config['nix'], config.get('metadata', {}), 'miniflux')
print(generate(spec))
" > /tmp/hop3.nix
nix-build /tmp/hop3.nix --no-out-link
# For hand-crafted mode, just build directly:
cd apps/real-apps-nix/landing
nix-build hop3.nix --no-out-link
Inspect runtime config¶
Validate all Nix apps¶
hop3-test system --docker --clean --with nix apps/real-apps-nix
hop3-test system --docker --clean --with nix apps/real-apps-nix-gen
Limitations¶
- Nix must be installed on the server (
nix-buildmust be in PATH). The installer handles this when--with nixis passed. - First builds can be slow as the Nix store is populated. Subsequent builds and re-deploys are fast (Nix caches everything).
- No flake support yet (standard
import <nixpkgs>only). - The
prebuilt-*templates are x86_64-linux only and not reproducible from source. Usenixpkgs-wrapperwhen possible. - Some apps in nixpkgs (e.g., Wiki.js) ship as a raw source tree
without a
bin/<name>wrapper, so thenixpkgs-wrappertemplate doesn't fit them directly. Hand-crafted mode is the workaround.
Related¶
- ADR 006: Nix Integration — Phase 1 architecture decision
- ADR 008: Template-Based Nix Generation — Phase 3 template system
- Nix Deployment Guide — Tutorial-style introduction
- hop3.toml
[nix]section — Field-by-field reference nix ejectcommand