Cooklang Service Module Design¶
Research Date: November 15, 2025 Status: Design Phase AI Assistance: Gemini 2.5 Pro analysis
Executive Summary¶
Cooklang is a markup language and CLI tool for managing cooking recipes. This document outlines the design for a native NixOS service module following established homelab patterns.
Key Decision: Create a native NixOS service module (no native module exists in nixpkgs yet).
About Cooklang¶
Overview¶
- Project: https://github.com/cooklang/cookcli
- Language: Rust
- Binary:
cook - License: MIT
- Package: Available in nixpkgs as
pkgs.cooklang-cli
Features¶
- CLI Commands:
cook server- Web server (port 9080)cook recipe- Parse and display recipescook shopping-list- Generate shopping listscook pantry- Manage ingredient inventorycook doctor- Validate recipescook search- Search recipes-
cook import- Import from websites -
File Formats:
.cook- Recipe files (Cooklang markup)config/aisle.conf- Organize ingredients by store sectionconfig/pantry.conf- Track inventory (TOML format)
Use Cases¶
- Personal Recipe Collection: Store recipes as plain text
- Meal Planning: Generate shopping lists from multiple recipes
- Pantry Management: Track what you have, what's low, what's expiring
- Web Interface: Browse recipes via web browser
Architecture Design¶
1. Native vs Container Decision¶
Decision: Native NixOS service (custom implementation)
Rationale:
- ✅ No native NixOS module exists (opportunity to create one)
- ✅ Simple Rust binary (no complex dependencies)
- ✅ Better systemd integration
- ✅ Easier updates via nix flake update
- ✅ No container overhead
- ❌ Container unnecessary for this workload
Future: Consider upstreaming to nixpkgs after validation.
2. State & Storage Architecture¶
Data Organization¶
/var/lib/cooklang/ # StateDirectory (systemd-managed)
├── data.db # SQLite database (if server uses one)
└── .server/ # Server runtime state
/data/cooklang/ # ZFS dataset (persistent storage)
├── recipes/ # Recipe collection (.cook files)
│ ├── breakfast/
│ │ └── pancakes.cook
│ ├── dinner/
│ └── desserts/
└── config/ # Configuration files
├── aisle.conf # Store section organization
└── pantry.conf # Inventory tracking (TOML)
Configuration Management¶
Declarative Config (Recommended):
services.cooklang = {
settings = {
aisle = ''
[produce]
tomatoes
onions
[dairy]
milk
cheese
'';
pantry = {
# TOML structure
pantry = {
salt = { quantity = "1%kg"; low = "500%g"; };
oil = { quantity = "500%ml"; low = "200%ml"; };
};
};
};
};
Stateful Config (Alternative):
- If users need to modify pantry via CLI (cook pantry)
- Store pantry.conf in ZFS dataset alongside recipes
- Module ensures file exists but doesn't manage content
Recommendation: Start with declarative, add stateful option later if needed.
3. Service Configuration¶
Module Options Structure¶
modules.services.cooklang = {
enable = true;
package = pkgs.cooklang-cli;
# User/Group
user = "cooklang";
group = "cooklang";
# Directories
recipeDir = "/data/cooklang/recipes"; # ZFS dataset mountpoint
dataDir = "/var/lib/cooklang"; # StateDirectory
# Network Configuration
listenAddress = "127.0.0.1"; # Bind to localhost only
port = 9080;
openBrowser = false; # Don't auto-open browser on server
# Declarative Configuration
settings = {
aisle = "..."; # types.lines for aisle.conf
pantry = { ... }; # TOML format for pantry.conf
};
# Standardized Submodules
reverseProxy = {
enable = true;
domain = "recipes.holthome.net";
# Additional Caddy-specific options
};
backup = {
enable = true;
dataset = "tank/services/cooklang";
# Sanoid policy options
};
metrics = null; # No native metrics endpoint
logging = {
enable = true;
journalUnit = "cooklang.service";
labels = {
service = "cooklang";
service_type = "recipe_management";
};
};
preseed = {
enable = true;
sourceType = "syncoid"; # Primary: replicated snapshots
# Restic as fallback
};
};
4. SystemD Service Configuration¶
systemd.services.cooklang = {
description = "Cooklang Recipe Server";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"zfs-mount.service"
];
requires = [ "zfs-mount.service" ];
serviceConfig = {
Type = "simple";
User = "cooklang";
Group = "cooklang";
# Directories
StateDirectory = "cooklang";
StateDirectoryMode = "0750";
WorkingDirectory = cfg.recipeDir; # cook expects to run in recipe dir
# Security
PrivateTmp = true;
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.recipeDir cfg.dataDir ];
# Resources
MemoryMax = "512M";
CPUQuota = "50%";
# Command
ExecStart = "${cfg.package}/bin/cook server --host ${cfg.listenAddress} --port ${toString cfg.port}";
# Restart policy
Restart = "on-failure";
RestartSec = "10s";
};
# Preseed dependency (if enabled)
wants = lib.optional cfg.preseed.enable "preseed-cooklang.service";
after = lib.optional cfg.preseed.enable "preseed-cooklang.service";
};
5. Configuration File Management¶
The module will create config files in the recipe directory:
system.activationScripts.cooklang-config = lib.mkIf cfg.enable ''
# Ensure config directory exists
mkdir -p ${cfg.recipeDir}/config
# Write aisle.conf (declarative)
cat > ${cfg.recipeDir}/config/aisle.conf <<'EOF'
${cfg.settings.aisle}
EOF
# Write pantry.conf (declarative, if managed)
${lib.optionalString (cfg.settings.pantry != null) ''
${pkgs.remarshal}/bin/json2toml < ${pkgs.writeText "pantry.json" (builtins.toJSON cfg.settings.pantry)} \
> ${cfg.recipeDir}/config/pantry.conf
''}
# Set ownership
chown -R ${cfg.user}:${cfg.group} ${cfg.recipeDir}/config
chmod 750 ${cfg.recipeDir}/config
chmod 640 ${cfg.recipeDir}/config/*
'';
Infrastructure Integration¶
1. Reverse Proxy (Caddy)¶
# In cooklang module
config = lib.mkIf (cfg.enable && cfg.reverseProxy.enable) {
modules.caddy.virtualHosts."${cfg.reverseProxy.domain}" = {
enable = true;
extraConfig = ''
reverse_proxy ${cfg.listenAddress}:${toString cfg.port}
'';
};
};
Domain: recipes.holthome.net
Features: - TLS termination via Caddy - Optional authentication (caddy-security/PocketID) - Request logging to Loki
2. Storage (ZFS) & Backup (Sanoid)¶
ZFS Dataset Configuration:
# In forge host config
modules.storage.datasets."cooklang" = {
dataset = "tank/services/cooklang";
mountpoint = "/data/cooklang";
properties = {
recordsize = "128K"; # Good for text files
compression = "zstd"; # Excellent compression for text
atime = "off";
};
};
Backup Policy:
modules.backup.sanoid.datasets."tank/services/cooklang" = {
useTemplate = [ "services" ]; # Standard policy
recursive = true;
# Replication to nas-1
replication = {
enable = true;
targetHost = "nas-1";
targetDataset = "backup/forge/services/cooklang";
};
};
What to Back Up:
- ✅ Recipe files (.cook)
- ✅ Configuration (aisle.conf, pantry.conf)
- ❌ Server state in /var/lib/cooklang (ephemeral, can be recreated)
3. Disaster Recovery (Preseed)¶
Strategy: Multi-tier restore
- Primary: Syncoid replication from nas-1
- Fallback: Restic backup from R2
modules.services.cooklang.preseed = {
enable = true;
sourceType = "syncoid";
# Syncoid (preferred)
syncoid = {
sourceHost = "nas-1";
sourceDataset = "backup/forge/services/cooklang";
targetDataset = "tank/services/cooklang";
};
# Restic (fallback)
restic = {
enable = true;
repository = "r2:forge-backups/cooklang";
paths = [ "/data/cooklang" ];
};
};
Preseed Service:
- Runs before cooklang.service
- Checks if /data/cooklang/recipes is empty
- If empty, restores from Syncoid or Restic
- Creates .preseed-marker on success
4. Monitoring & Alerting¶
Service Health Monitoring¶
No Native Metrics: Cooklang doesn't expose a Prometheus /metrics endpoint.
Monitoring Strategy:
1. SystemD Unit Monitoring (via systemd_exporter):
-
Port Check (via Prometheus
blackbox_exporter): -
HTTP Check via Reverse Proxy (Gatus):
- Monitor
https://recipes.holthome.net - Expected status: 200
- Check interval: 60s
Alert Rules¶
modules.alerting.rules = {
"cooklang-service-down" = {
type = "promql";
alertname = "CooklangServiceDown";
expr = ''
systemd_unit_state{name="cooklang.service",state="active"} == 0
'';
for = "5m";
severity = "high";
labels = {
service = "cooklang";
category = "systemd";
};
annotations = {
title = "Cooklang Service Down";
body = "The Cooklang service on {{ $labels.instance }} has been down for 5 minutes.";
};
};
"cooklang-dataset-unavailable" = {
type = "promql";
alertname = "CooklangDatasetUnavailable";
expr = ''
zfs_dataset_available{dataset="tank/services/cooklang"} == 0
'';
for = "2m";
severity = "critical";
labels = {
service = "cooklang";
category = "storage";
};
};
};
5. Logging (Promtail → Loki)¶
modules.services.cooklang.logging = {
enable = true;
journalUnit = "cooklang.service";
labels = {
service = "cooklang";
service_type = "recipe_management";
environment = "homelab";
};
};
Log Queries in Grafana:
Security Considerations¶
1. Service User & Permissions¶
users.users.cooklang = {
isSystemUser = true;
group = "cooklang";
home = lib.mkForce "/var/empty"; # Prevent home directory interference
};
users.groups.cooklang = {};
File Permissions:
- /var/lib/cooklang: 750 (cooklang:cooklang)
- /data/cooklang: 750 (cooklang:cooklang)
- Config files: 640 (cooklang:cooklang)
- Recipe files: 640 (cooklang:cooklang)
2. Network Exposure¶
Design:
- Service binds to 127.0.0.1:9080 (localhost only)
- NOT directly accessible from network
- ALL traffic goes through Caddy reverse proxy
- Caddy handles TLS termination
- Optional caddy-security/PocketID authentication for SSO
Firewall:
# No firewall rule needed - service is localhost-only
networking.firewall.allowedTCPPorts = []; # Nothing exposed
3. SystemD Sandboxing¶
serviceConfig = {
# Filesystem Protection
ProtectSystem = "strict"; # /usr, /boot, /efi read-only
ProtectHome = true; # /home inaccessible
ReadWritePaths = [ # Only writable paths
cfg.recipeDir
cfg.dataDir
];
# Process Restrictions
NoNewPrivileges = true; # Can't gain privileges
PrivateTmp = true; # Private /tmp
PrivateDevices = true; # Limited device access
ProtectKernelTunables = true; # No sysctl access
ProtectControlGroups = true; # No cgroup manipulation
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
# Resource Limits
MemoryMax = "512M"; # Max 512MB RAM
CPUQuota = "50%"; # Max 50% CPU
TasksMax = "64"; # Max 64 processes/threads
};
4. Authentication Options¶
Option A: Public Access - No authentication required - Suitable for internal network only - Trust network-level security
Option B: caddy-security/PocketID SSO
modules.services.cooklang.reverseProxy = {
enable = true;
domain = "recipes.holthome.net";
caddySecurity = {
enable = true;
portal = "pocketid";
policy = "default";
};
};
Option C: Basic Auth
Recommendation: Start with Option A (internal only), add caddy-security/PocketID later if exposing externally.
Implementation Plan¶
Phase 1: Core Module (Week 1)¶
Tasks:
1. ✅ Research completed (this document)
2. ⬜ Create module structure:
- modules/nixos/services/cooklang/default.nix
3. ⬜ Implement core options (enable, package, user, directories)
4. ⬜ Implement systemd service
5. ⬜ Test basic functionality (service starts, web UI accessible)
Deliverables:
- Working cook server via systemd
- Basic configuration options
- Service accessible on localhost:9080
Phase 2: Storage Integration (Week 1)¶
Tasks: 1. ⬜ Add ZFS dataset configuration 2. ⬜ Implement declarative config management (aisle.conf, pantry.conf) 3. ⬜ Add backup integration (Sanoid) 4. ⬜ Test with real recipe data
Deliverables: - Recipes stored on ZFS dataset - Automated snapshots configured - Configuration managed declaratively
Phase 3: Infrastructure Integration (Week 2)¶
Tasks: 1. ⬜ Add reverse proxy integration (Caddy) 2. ⬜ Implement logging (Promtail) 3. ⬜ Add monitoring alerts (Prometheus) 4. ⬜ Configure Gatus endpoint contribution 5. ⬜ Test end-to-end access via reverse proxy
Deliverables:
- HTTPS access via recipes.holthome.net
- Logs shipped to Loki
- Alerts configured
- Health monitoring active
Phase 4: Disaster Recovery (Week 2)¶
Tasks: 1. ⬜ Implement preseed service 2. ⬜ Add Syncoid replication support 3. ⬜ Add Restic fallback support 4. ⬜ Test restore scenarios: - Fresh deployment (empty dataset) - Restore from Syncoid - Restore from Restic
Deliverables: - Automated restore on fresh deployment - Multi-tier restore strategy validated - Documentation for manual restore
Phase 5: Polish & Documentation (Week 3)¶
Tasks:
1. ⬜ Add NixOS option documentation
2. ⬜ Create user guide in /docs/services/cooklang.md
3. ⬜ Add example configuration
4. ⬜ Security hardening review
5. ⬜ Performance tuning (if needed)
6. ⬜ Consider upstreaming to nixpkgs
Deliverables: - Complete documentation - Example configurations - Security review complete - Ready for upstream consideration
Usage Examples¶
Basic Configuration¶
# hosts/forge/services/cooklang.nix
{ ... }:
{
modules.services.cooklang = {
enable = true;
# Use default package from nixpkgs
package = pkgs.cooklang-cli;
# Point to ZFS dataset
recipeDir = "/data/cooklang/recipes";
# Bind to localhost only
listenAddress = "127.0.0.1";
port = 9080;
};
# Storage configuration
modules.storage.datasets."cooklang" = {
dataset = "tank/services/cooklang";
mountpoint = "/data/cooklang";
};
# Backup configuration
modules.backup.sanoid.datasets."tank/services/cooklang" = {
useTemplate = [ "services" ];
recursive = true;
};
}
Full Featured Configuration¶
{ ... }:
{
modules.services.cooklang = {
enable = true;
# Declarative configuration
settings = {
aisle = ''
[produce]
tomatoes
onions
garlic
[dairy]
milk
cheese
butter
[meat]
chicken
beef
[pantry]
flour
sugar
salt
pasta
rice
'';
pantry = {
pantry = {
flour = { quantity = "2%kg"; low = "500%g"; };
sugar = { quantity = "1%kg"; low = "250%g"; };
salt = { quantity = "500%g"; low = "100%g"; };
};
dairy = {
milk = {
quantity = "1%L";
expire = "2025-11-20";
low = "500%ml";
};
};
};
};
# Reverse proxy
reverseProxy = {
enable = true;
domain = "recipes.holthome.net";
};
# Backup
backup = {
enable = true;
dataset = "tank/services/cooklang";
};
# Logging
logging = {
enable = true;
journalUnit = "cooklang.service";
};
# Disaster recovery
preseed = {
enable = true;
sourceType = "syncoid";
};
};
# Storage
modules.storage.datasets."cooklang" = {
dataset = "tank/services/cooklang";
mountpoint = "/data/cooklang";
properties = {
recordsize = "128K";
compression = "zstd";
};
};
# Backup policy
modules.backup.sanoid.datasets."tank/services/cooklang" = {
useTemplate = [ "services" ];
recursive = true;
replication = {
enable = true;
targetHost = "nas-1";
targetDataset = "backup/forge/services/cooklang";
};
};
}
File Locations¶
Module Files¶
modules/nixos/services/cooklang/default.nix- Main modulelib/types.nix- Shared type definitions (already exists)
Host Configuration¶
hosts/forge/services/cooklang.nix- Host-specific confighosts/forge/infrastructure/storage.nix- ZFS dataset (existing)hosts/forge/infrastructure/backup.nix- Sanoid policy (existing)
Documentation¶
docs/services/cooklang.md- User guidedocs/services/cooklang-module-design.md- This design document
Testing Strategy¶
Unit Tests¶
- Module loads without errors
- Options are correctly typed
- Service file is generated correctly
Integration Tests¶
- Service starts and binds to correct port
- Web UI is accessible
- Recipe parsing works
- Shopping list generation works
- Pantry management works
End-to-End Tests¶
- Fresh deployment
- Access via reverse proxy (HTTPS)
- Logs appear in Loki
- Alerts fire when service is down
- Backup snapshots are created
- Restore from backup works
Future Enhancements¶
Short Term¶
- Support for multiple recipe collections (multi-tenant)
- Integration with recipe import from popular websites
- Automated shopping list export to mobile apps
- Recipe sharing via public URLs
Long Term¶
- Upstream to nixpkgs as
services.cooklang - Native Prometheus metrics exporter
- Integration with smart home systems (recipe → shopping list → automation)
- AI-powered recipe recommendations based on pantry inventory
References¶
Documentation¶
Related Patterns¶
- Modular Design Patterns
- Disaster Recovery Preseed Pattern
- Monitoring Strategy - Black-box vs white-box monitoring
AI Research¶
- Model Used: Gemini 2.5 Pro
- Continuation ID:
17de3b63-2eae-4011-a831-5cc0bd7f33c0 - Research Date: November 15, 2025
Approval & Next Steps¶
This design document should be reviewed before implementation begins. Once approved, proceed with Phase 1.
Questions for Review: 1. ✅ Native service vs container approach approved? 2. ✅ Storage architecture (ZFS + declarative config) approved? 3. ✅ Security model (localhost + reverse proxy) approved? 4. ⬜ Any additional features needed for initial release?
Status: ✅ IMPLEMENTED (November 15, 2025) Implementation Time: ~2 hours Priority: Medium Dependencies: None (all infrastructure patterns exist)
Implementation Summary¶
The Cooklang module has been successfully implemented with the following components:
- Module File:
modules/nixos/services/cooklang/default.nix(465 lines) - Native systemd service configuration
- Declarative aisle.conf and pantry.conf management
-
Full integration with storage, backup, monitoring, and preseed patterns
-
Example Configuration:
hosts/forge/services/cooklang.nix(141 lines) - Complete working example for the forge host
- ZFS storage configuration
- Sanoid backup with replication
- Caddy reverse proxy
-
Loki logging integration
-
Service Registration: Added to
modules/nixos/services/default.nix
What Works¶
✅ Native systemd service (no containers) ✅ Declarative configuration management ✅ ZFS storage integration ✅ Sanoid backup with replication ✅ Disaster recovery via preseed ✅ Reverse proxy via Caddy ✅ Logging to Loki via Promtail ✅ Monitoring alerts (service down, dataset unavailable) ✅ Security hardening (systemd sandboxing) ✅ Resource limits (512MB RAM, 50% CPU)
Next Steps¶
To enable Cooklang on forge:
-
Enable the service by importing the config:
-
Deploy:
-
Access: https://recipes.holthome.net
-
Add recipes: Place
.cookfiles in/data/cooklang/recipes/
Testing Checklist¶
⬜ Service starts and binds to port 9080 ⬜ Web UI accessible at https://recipes.holthome.net ⬜ Recipe parsing and display works ⬜ Shopping list generation works ⬜ Logs appear in Loki ⬜ Alerts fire when service is down ⬜ Backup snapshots are created ⬜ Restore from backup works