Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

NixOS Module

Plinth ships a NixOS module for declarative deployment with systemd hardening.

Adding the flake

# flake.nix
{
  inputs.plinth.url = "github:caniko/plinth";

  outputs = { self, nixpkgs, plinth, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        plinth.nixosModules.default
        # Make packages available via overlay
        { nixpkgs.overlays = [ plinth.overlays.default ]; }
      ];
    };
  };
}

Minimal deployment

services.plinth.instances.default = {
  host = "127.0.0.1";
  port = 3000;
};

This starts Plinth on port 3000 with all defaults. The module also enables PostgreSQL 16, loads pgvector, creates the plinth database and plinth role, and starts the service after postgresql.service with DATABASE_URL=postgres:///plinth?host=/run/postgresql.

Site personalisation

services.plinth.instances.default = {
  site = {
    name = "Jane's Blog";
    tagline = "Thoughts on systems programming";
    defaultTheme = "dark";

    author = {
      name = "Jane Doe";
      email = "jane@example.com";
    };

    social = {
      github = "https://github.com/janedoe";
      mastodon = "https://fosstodon.org/@janedoe";
    };

    nav = [
      { label = "Blog"; path = "/posts"; }
      { label = "Projects"; path = "/projects"; }
      { label = "About"; path = "/about"; }
    ];
  };

  pages.blog = {
    title = "Blog";
    subtitle = "Notes on software and systems";
  };
};

Production with Caddy

services.plinth.instances.default = {
  host = "127.0.0.1";
  port = 3000;
  stateDir = "/var/lib/plinth";
  apiKeyFile = "/run/secrets/plinth-api-key";

  database = {
    name = "plinth";
    url = "postgres:///plinth?host=/run/postgresql";
  };
};

services.caddy = {
  enable = true;
  virtualHosts."example.com".extraConfig = ''
    reverse_proxy localhost:3000
  '';
};

networking.firewall.allowedTCPPorts = [ 80 443 ];

Observability

services.plinth.instances.default.observability = {
  enable = true;
  otlpEndpoint = "https://openobserve.example.com:5081";
  serviceName = "plinth-prod";
  logLevel = "info,plinth=debug";
};

Secrets with agenix

age.secrets.plinth-api-key = {
  file = ./secrets/plinth-api-key.age;
  owner = "plinth";
  group = "plinth";
};

services.plinth.instances.default = {
  apiKeyFile = config.age.secrets.plinth-api-key.path;
};

Secrets with sops-nix

sops.secrets."plinth/api-key".owner = "plinth";

services.plinth.instances.default = {
  apiKeyFile = config.sops.secrets."plinth/api-key".path;
};

All module options

OptionTypeDefaultDescription
instancesattrset{}Named Plinth instances to run
packagepackagepkgs.plinthPlinth package to use
userstring"plinth" for default, otherwise "plinth-<name>"System user
groupstring"plinth" for default, otherwise "plinth-<name>"System group
hoststring"127.0.0.1"Bind address
portport3000Bind port
stateDirpath/var/lib/plinthStateful data directory
apiKeyFilepath or nullnullPath to API key file (loaded via systemd LoadCredential)
site.*Site identity (see plinth.toml)
pages.*Page-specific config (see plinth.toml)
database.namestring"plinth" for default, otherwise "plinth_<name>"Postgres database name
database.urlstringpostgres:///plinth?host=/run/postgresql for defaultPostgres connection URL
observability.enableboolfalseEnable OTLP export
observability.otlpEndpointstring""OTLP endpoint URL
observability.otlpHeadersstring or nullnullOTLP auth headers
observability.serviceNamestring"plinth"Telemetry service name
observability.logLevelstring"info"Log level (RUST_LOG)
search.defaultLimitint10Search result limit
search.relatedLimitint5Related articles limit
search.minSimilarityfloat0.5Min similarity for opinion tracking
content.wordsPerMinuteint200Reading time WPM
content.vectorTruncationint5000Embedding char limit
immich.apiUrlstring""Immich URL (empty = disabled)
immich.apiKeystring""Immich API key
images.cacheMaxAgeint31536000Image cache max-age (seconds)
extraEnvlines""Additional env vars (KEY=value per line)

Systemd hardening

The module applies security hardening by default:

  • NoNewPrivileges, ProtectSystem=strict, ProtectHome
  • PrivateTmp, PrivateDevices, PrivateMounts
  • RestrictAddressFamilies (AF_UNIX, AF_INET, AF_INET6 only)
  • RestrictNamespaces, LockPersonality, RestrictRealtime
  • ReadWritePaths limited to stateDir