Deploy the control plane
Marshall and Lilly are NixOS modules exported by the flake. Enabling one doesn't enable the other. The host's configuration.nix chooses its roles, and secrets arrive as encrypted documents that are decrypted on the host.
- Modules
nixosModules.default, ormarshall,lilly,proxy,acme,secretsandbackupsindividually- Target
x86_64-linux, NixOS 26.05
1. Build and check
nix build .#marshall .#lilly nix build .#checks.x86_64-linux.marshall-release nix build .#checks.x86_64-linux.lilly-release nix build .#checks.x86_64-linux.module-configuration
Release builds have no network access, and dependency hashes are pinned. No production secrets are needed to build.
2. Enable Marshall
{
imports = [ overalls.nixosModules.default ];
services.marshall = {
enable = true;
publicHost = "control.example.com";
replicaCount = 3;
databaseConnectionBudget = 43;
environmentFile = "/run/kittens-secrets/marshall.env";
instances.node01 = {
port = 4443;
certificateFile = "/run/kittens-secrets/backend.crt";
keyFile = "/run/kittens-secrets/backend.key";
};
};
}
Here overalls is your flake input for the fork. For a single server, set replicaCount = 1 and use local PostgreSQL; that's the supported nonredundant layout (ADR-0048 ↗). With automatic updates enabled, budget at least replicaCount × (poolSize + 3) + 10 database connections.
3. Secrets
services.kittensSecrets decrypts SOPS documents with the host's SSH-derived age key and publishes them under /run/kittens-secrets/NAME. Encrypt each document to the host and to your recovery users:
services.kittensSecrets = {
enable = true;
bundles.marshall = {
encryptedFile = "${./secrets/marshall.enc.json}";
outputs.environment = {
format = "environment";
secrets = [
"DATABASE_URL" "MARSHALL_CLOAK_KEYS" "SECRET_KEY_BASE"
"TOKEN_SIGNING_SECRET" "MARSHALL_SMTP_HOST" "MARSHALL_SMTP_USERNAME"
"MARSHALL_SMTP_PASSWORD" "MARSHALL_SMTP_CA_CERTFILE" "MARSHALL_MAIL_FROM"
];
};
};
};
services.marshall = {
environmentFile = "${config.services.kittensSecrets.bundles.marshall.directory}/environment";
secretUnits = [ config.services.kittensSecrets.bundles.marshall.unit ];
};
nix build .#user-recipients --out-link result-recipient nix run .#secrets -- check
4. Database and email
| Variable | Meaning |
|---|---|
DATABASE_URL | PostgreSQL, reached over verified TLS, shared by every Marshall |
SECRET_KEY_BASE, TOKEN_SIGNING_SECRET | Shared signing secrets |
MARSHALL_CLOAK_KEYS | Encryption keyring. Keep old keys as long as data encrypted with them still exists. |
MARSHALL_SMTP_HOST · _USERNAME · _PASSWORD | Your relay. Plaintext SMTP isn't supported. |
MARSHALL_SMTP_TLS | implicit (the default, port 465) or starttls (port 587) |
MARSHALL_MAIL_FROM | A verified sender, such as noreply@your-domain.example |
PHX_HOST | The public host name used in sign-in links |
Marshall won't start if its SMTP configuration is missing or invalid. Send a test to a mailbox you control before you open the deployment to other people.
5. TLS and certificates
services.marshallProxy runs HAProxy in front of Marshall and verifies each backend's certificate and host name. The ACME integration issues and renews the public certificate:
services.marshallProxy.acme = {
enable = true;
email = "operator@example.com";
};
# Set only after accepting the selected CA's terms.
security.acme.acceptTerms = true;
HTTP-01 needs public DNS pointing at the frontend and port 80 reachable. Use challengeType = "dns-01" with Route 53 when port 80 can't be reachable. If several frontends share one host name, each lists the others as http01.peers.
6. Migrate, then switch
Serving instances never run migrations when they start. Instead, host enrollment and every automatic update run Marshall's migrations from the candidate release, under a PostgreSQL advisory lock, before the running services are stopped. Migrations must work with every release still serving, and a successful schema change is never rolled back automatically. Incompatible changes need a coordinated upgrade.
Full reference: docs/nixos.md
Full reference: docs/production-email.md