Blueprint Catalog

Overview

The blueprint catalog (nixda.catalog) is a set of pre-defined service blueprints. A blueprint is a complete, opinionated runtime configuration for a specific service — it encodes the image or NixOS module, default ports, environment variables, and idle-timeout behavior. Blueprints do not contain host-specific data such as IP assignments or zone placement; those are always supplied by the consumer.

For runtime-specific details, see the Native runtime and OCI runtime pages. For zone placement and ID allocation, see Zone Configuration.

Blueprints are exposed as values in config.nixda.catalog.<name>. Each value is a plain attribute set matching the service submodule type defined in nixda.zones.<zone>.services. This structural equivalence means a blueprint can be merged directly into a zone service definition using lib.recursiveUpdate.

What a blueprint provides

  • Runtime — OCI image reference with default environment variables and volumes, or a NixOS deferredModule for native container / system service runtimes.
  • Ports — The TCP ports the service listens on internally.
  • idleTimeout — Default seconds of inactivity before the service is stopped. null disables idle management entirely (always-on).
  • stripPrefix — Whether the reverse proxy strips the service-name path prefix before forwarding. Defaults to true.

What the consumer always provides

  • id — The unique integer (1–254) used to derive the static IP for the service within the zone: 10.100.<zone_id>.<id>.
  • Zone placement — Which nixda.zones.<zone>.services attribute set the service is registered under.

Usage Pattern

The canonical consumer pattern is a lib.recursiveUpdate merge:

nixda.zones.private.services.jdownloader = lib.recursiveUpdate
  config.nixda.catalog.jdownloader
  { id = 20; };

lib.recursiveUpdate performs a deep merge, with the right-hand side winning on conflicts. The blueprint supplies everything the runtime needs; the consumer supplies identity and placement, and optionally overrides any blueprint default.

This pattern keeps zone definitions minimal. A service that needs no customisation beyond id and zone placement requires a single two-line stanza. A service that needs volume mounts or environment overrides adds only those attributes.

The consumer is responsible for any mounts, inputs, or configuration that the blueprint explicitly documents as required. Blueprints cannot enforce these constraints at evaluation time; missing required values produce runtime errors rather than evaluation failures.


Blueprint Reference

The catalog ships the following blueprints. The table below summarises their runtime characteristics. Blueprints marked with a digest-pinned recommended map (ADR-0005, all OCI-runtime entries below) expose the mk constructor surface for explicit version pinning.

BlueprintRuntimePort(s)Idle timeoutAlways-on
terminalSystem7681Yes
uptimeNative (NixOS container)300112 hNo
cockpitSystem9090Yes
copypartyNative (NixOS container)392312 hNo
jdownloaderOCI (Podman)580012 hNo
syncthingNative (NixOS container)8384, 22000, 21027Yes
forgejo-runnerNative (NixOS container)Yes
tubearchivistOCI (Podman)800012 hNo
tubearchivist-esOCI (Podman)9200 (internal)12 hNo
tubearchivist-redisOCI (Podman)6379 (internal)12 hNo
twentyOCI (Podman pod)3000disabledYes

Consumer Override Patterns

Adding volumes to an OCI blueprint

OCI volumes are a list. lib.recursiveUpdate replaces lists rather than concatenating them, so the consumer must supply the full volume list when overriding:

nixda.zones.private.services.jdownloader = lib.recursiveUpdate
  config.nixda.catalog.jdownloader
  {
    id = 20;
    runtime.oci.volumes = [
      "/mnt/storage/downloads:/output"
      "/var/lib/jdownloader/config:/config"
    ];
  };

Overriding environment variables

OCI environment variables are an attribute set. lib.recursiveUpdate merges attribute sets deeply, so individual variables can be overridden without restating the full set:

nixda.zones.private.services.jdownloader = lib.recursiveUpdate
  config.nixda.catalog.jdownloader
  {
    id = 20;
    runtime.oci = {
      environment.TZ = "America/New_York";
      volumes = [
        "/mnt/storage/downloads:/output"
        "/var/lib/jdownloader/config:/config"
      ];
    };
  };

Disabling idle timeout

To keep an idle-timeout-enabled service always-on, override idleTimeout to null:

nixda.zones.private.services.uptime = lib.recursiveUpdate
  config.nixda.catalog.uptime
  {
    id = 10;
    idleTimeout = null;
  };

Overriding idle timeout duration

To use a shorter timeout than the 12-hour default (for example, 2 hours):

nixda.zones.private.services.copyparty = lib.recursiveUpdate
  config.nixda.catalog.copyparty
  {
    id = 30;
    idleTimeout = 7200;
    runtime.native.volumes = [
      "/mnt/storage/media:/media"
    ];
  };

Pod Blueprints

Some blueprints use runtime.oci.pod to group multiple containers into a Podman pod with a shared network namespace. Pod members communicate via localhost and are invisible to other zone services — only the entrypoint gets a zone IP.

Example: Twenty CRM

Migrated blueprints expose the { recommended, mk } constructor surface (see Pinning a Blueprint to an Explicit Version). The consumer calls (blueprint "twenty").mk {} to inherit the recommended digest-pinned versions, then merges host-specific identity and volumes via lib.recursiveUpdate:

crm = lib.recursiveUpdate ((blueprint "twenty").mk {}) {
  id = 1;
  runtime.oci = {
    environment = {
      SERVER_URL = "https://crm.business.lan";
    };
    volumes = [
      "twenty-local-storage:/app/packages/twenty-server/.local-storage"
    ];
    pod."twenty-db".volumes = [
      "twenty-db-data:/var/lib/postgresql/data"
    ];
    pod."twenty-worker".volumes = [
      "twenty-local-storage:/app/packages/twenty-server/.local-storage"
    ];
  };
};

Calling mk {} (no versions argument) inherits the recommended pin and emits one drift warning per evaluation acknowledging the inheritance. To opt into the recommended pin explicitly (suppressing the warning), supply versions = (blueprint "twenty").recommended;:

crm = lib.recursiveUpdate ((blueprint "twenty").mk {
  versions = (blueprint "twenty").recommended;
}) {
  id = 1;
  # ... runtime.oci as above
};

For partial pinning (different main tag, recommended postgres/redis/worker) see the per-blueprint twenty page. The partial-pin rule (ADR-0004) rejects strict-subset keysets — supply all four keys or none.

Pod member volumes and environment can be overridden individually via pod."member-name".volumes and pod."member-name".environment.

Secrets are injected per pod member via environmentFiles in the host’s default.nix — see the consumer host documentation for the wiring pattern.