- Shell 100%
One-way IMAP pull from mailbox.org to the local Dovecot server using doveadm sync with the imapc backend. Incremental and idempotent — safe to re-run for catching up on new mail before final cutover. Usage: utils/import-mailbox-org.sh <mailbox> <password> Creates a temporary Dovecot config with imapc settings, runs the sync, prints a folder summary, and cleans up the temp config on exit. |
||
|---|---|---|
| .githooks | ||
| config/fail2ban/filter.d | ||
| sample.conf.d | ||
| scripts | ||
| utils | ||
| .editorconfig | ||
| .gitignore | ||
| .shellcheckrc | ||
| AGENTS.md | ||
| LICENSE | ||
| local-bootstrap.sh | ||
| README.md | ||
| sat | ||
| sat.conf.sample | ||
| vps-bootstrap.sh | ||
Software Atrium
Self-hosted business software installer for bare Debian Trixie (or compatible) servers.
Command: sat
Installs and configures production-ready business software — git hosting, ERP, email, and more — directly on the host OS using systemd, nginx, and native packages. No Docker.
Prerequisites
- Debian Trixie (13) or compatible
- Root or sudo access
- A configured
/etc/sat/sat.conffile (seesat.conf.sample)
Quick Start
Fresh VPS
# Download and run the bootstrap script (installs git + vim, clones this repo,
# creates /etc/sat/sat.conf + /etc/sat/sat.d/*.conf from samples)
curl -fsSL https://forgejo.openeyedev.eu/OpenEyeDev/software-atrium/raw/branch/main/vps-bootstrap.sh | bash
# Fill in your domain and email in the main config
vim /etc/sat/sat.conf
# Edit the component config(s) you want to install
vim /etc/sat/sat.d/forgejo.conf
# Install a component
sat install forgejo
Developer machine
# Clone the repo, then run the local bootstrap script:
git clone https://forgejo.openeyedev.eu/OpenEyeDev/software-atrium.git
cd software-atrium
bash local-bootstrap.sh
# Creates ~/bin, adds it to PATH, symlinks sat + bash completion.
# Verify:
sat help
Existing machine
git clone https://forgejo.openeyedev.eu/OpenEyeDev/software-atrium.git /opt/software-atrium
ln -sf /opt/software-atrium/sat /usr/local/bin/sat
mkdir -p /etc/sat/sat.d && chmod 700 /etc/sat
cp /opt/software-atrium/sat.conf.sample /etc/sat/sat.conf && chmod 600 /etc/sat/sat.conf
cp /opt/software-atrium/sample.conf.d/*.conf /etc/sat/sat.d/ && chmod 600 /etc/sat/sat.d/*.conf
vim /etc/sat/sat.conf # set SAT_DEFAULT_DOMAIN and LETSENCRYPT_EMAIL
vim /etc/sat/sat.d/forgejo.conf
sat install forgejo
Config structure
sat.conf defines global variables (like SAT_DEFAULT_DOMAIN) and includes all *.conf files from /etc/sat/sat.d/. Each component has its own file (e.g. forgejo.conf, nextcloud.conf, exim4.conf). See sample.conf.d/ in the repo for the full set.
SAT_DEFAULT_DOMAIN is available to all component configs via ${SAT_DEFAULT_DOMAIN} — set it once in sat.conf and use it in component files to avoid repeating your domain everywhere.
Components
| Component | Description |
|---|---|
forgejo |
Self-hosted git server (nginx + TLS via Let's Encrypt) |
nextcloud |
Self-hosted file-sharing platform (nginx + PHP-FPM + PostgreSQL + Redis) |
odoo-base |
Odoo OS dependencies + wkhtmltopdf + shared SSH key — run once per server |
odoo <inst> <ver> [cc] |
Odoo instance; version e.g. 17.0; optional ISO 3166-1 country code |
odoo-dev-base |
Dev laptop setup: build deps + PostgreSQL role — run once with sudo |
odoo-dev <inst> <ver> [cc] |
Dev Odoo instance in ~/odoo-dev/; no sudo; start.sh to run |
update-odoo <inst> |
Update an instance: gitaggregate → refresh addons/ → upgrade base → restart |
link-addons <inst> |
Refresh addons/ symlinks only (no gitaggregate, no restart) |
exim4 |
Internet MTA: virtual mailboxes, domain aliases, per-domain DKIM, SPF |
fail2ban |
Brute-force protection for SSH, SMTP (exim4), IMAP (dovecot), and nginx |
health |
Install daily health check timer (06:00); sat health runs it immediately |
Forgejo
Install
# Set required variables in /etc/sat/sat.d/forgejo.conf:
# FORGEJO_DOMAIN=forgejo.${SAT_DEFAULT_DOMAIN}
# FORGEJO_ADMIN_EMAIL=admin@${SAT_DEFAULT_DOMAIN}
#
# Optional:
# FORGEJO_ADMIN_USER=admin (default: admin)
# FORGEJO_ADMIN_PASSWORD=secret (auto-generated and printed once if not set)
# FORGEJO_APP_NAME=My Git Server
sudo sat install forgejo
The install script creates the git OS user, sets up PostgreSQL with peer authentication, downloads the latest Forgejo binary, writes /etc/forgejo/app.ini, obtains a TLS certificate via Let's Encrypt, and creates the initial admin account. Public registration is disabled by default — all accounts must be created by an admin.
User management
sudo sat forgejo list # list all users
sudo sat forgejo info <username> # show one user
sudo sat forgejo add <username> <email> # create user (password auto-generated)
sudo sat forgejo add <username> <email> --admin # create admin user
sudo sat forgejo add <username> <email> --fullname "Full Name"
sudo sat forgejo passwd <username> # reset password (auto-generated)
sudo sat forgejo delete <username> # delete user (keeps repositories)
sudo sat forgejo delete --purge <username> # delete user and all their repositories
sudo sat forgejo token <username> [<token-name>] # generate an API access token
sudo sat forgejo reset-mfa <username> # remove two-factor authentication
Passwords are always auto-generated and printed once — the user should change them on first login. To promote or demote admin status, use the web admin panel at /-/admin/users/<id>/edit.
Nextcloud
Install
# Set required variables in /etc/sat/sat.d/nextcloud.conf:
# NEXTCLOUD_DOMAIN=nextcloud.${SAT_DEFAULT_DOMAIN}
# NEXTCLOUD_ADMIN_EMAIL=admin@${SAT_DEFAULT_DOMAIN}
#
# Optional:
# NEXTCLOUD_ADMIN_USER=admin (default: admin)
# NEXTCLOUD_ADMIN_PASSWORD=secret (auto-generated and printed once if not set)
# NEXTCLOUD_DATA_DIR=/var/lib/nextcloud/data
# NEXTCLOUD_VERSION=34.0.1 (default: latest stable)
# NEXTCLOUD_PHONE_REGION=NL (ISO 3166-1 alpha-2)
sudo sat install nextcloud
The install script downloads the latest stable Nextcloud, installs PHP-FPM and Redis, sets up PostgreSQL with peer authentication (OS www-data mapped to DB nextcloud via pg_ident), runs the unattended occ maintenance:install, configures Redis as the distributed/local/locking memcache, obtains a TLS certificate via Let's Encrypt, and installs a cron job for background tasks. After install, the admin account is ready to use at https://<NEXTCLOUD_DOMAIN>.
If sat install exim4 is also set up on this server, Nextcloud's email notifications work out of the box via sendmail mode (Settings → Basic settings → Email server → sendmail).
Odoo
Install
# 1. Install OS dependencies (once per server) — also generates the shared SSH key
sudo sat install odoo-base
# 2. Add instance settings to /etc/sat/sat.d/odoo.conf
# ODOO_MYCOMPANY_DOMAIN=odoo.${SAT_DEFAULT_DOMAIN}
# 3. Install an instance
sudo sat install odoo mycompany 17.0
# 4. With Dutch localisation
sudo sat install odoo mycompany 17.0 nl
# 5. Multiple domains (comma-separated)
# ODOO_MYCOMPANY_DOMAIN=www.${SAT_DEFAULT_DOMAIN},odoo.${SAT_DEFAULT_DOMAIN}
sudo sat install odoo mycompany 17.0
Update
# Pull latest code and OCA modules, refresh addons/, upgrade base module, restart
sudo sat update-odoo mycompany
# Refresh addons/ symlinks only (after editing whitelist/blacklist, or adding a module)
sudo sat link-addons mycompany
Per-instance resources
Each instance gets its own:
- OS user, PostgreSQL user, and database — all named
<instance> - Build directory at
/opt/odoo/<instance>/odoo/(git repo, virtualenv, parts, addons) - systemd service
odoo-<instance> - nginx vhost with TLS via Let's Encrypt
Build repo and private customisation
Odoo instances are built from a repos.yml file (used by gitaggregate) that lists which Odoo and OCA repositories to aggregate. The public template repo is:
https://forgejo.openeyedev.eu/OpenEyeDev/odoo-build.git
One branch per Odoo version (16.0, 17.0, 18.0, 19.0). The install script clones the matching branch and, if a country code is given, appends the OCA localisation repo to repos.yml.
To maintain instance-specific customisations (extra modules, pinned commits, etc.), point the instance at a private repo:
# In /etc/sat/sat.d/odoo.conf:
# ODOO_MYCOMPANY_BUILD_REPO=https://forgejo.openeyedev.eu/OpenEyeDev/odoo-build.git
# ODOO_MYCOMPANY_PUSH_REPO=git@git.${SAT_DEFAULT_DOMAIN}:myorg/mycompany-build.git
BUILD_REPO— source to clone from (defaults to the public template)PUSH_REPO— private repo to push the build config to after customisation
Once pushed, the public template is kept as a template remote so upstream changes can be merged in.
SSH key
sat install odoo-base generates a single ed25519 keypair at /opt/odoo/.ssh/id_ed25519, shared by all instances on the server (group-readable by the odoo group). Add the public key once to your Forgejo account or org — no per-instance deploy keys needed. The public key is printed at the end of odoo-base install and again on every subsequent run.
Custom modules branch
The install script creates two git branches in every instance repo:
deploy-<version>(e.g.deploy-19.0) — the build config branch; tracksrepos.yml, committed changes go here<version>(e.g.19.0) — an orphan branch for custom Odoo modules, kept separate from the build config
If PUSH_REPO is set, a parts/custom entry is added to repos.yml pointing at the <version> branch of that repo, so sat update-odoo pulls in custom modules on every update.
Module filtering
After gitaggregate runs, link_odoo_addons.sh creates a single addons/ directory of symlinks — one per active module. addons_path in odoo.conf points here instead of enumerating each parts/ subdirectory.
Two optional files in the build directory (/opt/odoo/<instance>/odoo/) control which modules are linked:
whitelist — if present, a repo listed here only contributes the modules that match its entries; repos not listed contribute all their modules.
blacklist — overrides the whitelist; matched modules are excluded regardless. The base module can never be blacklisted.
Entry format: <repo>/<module>, <repo>/*, or <repo>/<glob> — one per line. Repo name is the basename of the parts/ subdirectory (e.g. web, server-tools, odoo, custom).
Odoo dev instances
Dev instances run on a developer laptop/desktop — self-contained under ~/odoo-dev/, started manually with start.sh, no systemd/nginx/TLS. A shared git cache at ~/git/ holds clones of Odoo (OCB) and all OCA repos so they are fetched once and reused across instances and versions.
One-time setup
sudo sat install odoo-dev-base
Installs build dependencies, PostgreSQL, and creates a PG role matching your OS username (peer auth — no password needed).
Create an instance
sat install odoo-dev myproject 17.0
sat install odoo-dev myproject 17.0 nl # with Dutch localisation
No sudo — everything runs as your user. The instance lands in ~/odoo-dev/myproject/.
Start / stop
~/odoo-dev/myproject/odoo/start.sh # foreground, Ctrl-C to stop
Odoo listens on http://localhost:8069 with --dev=all (auto-reload on file changes, Werkzeug debugger). On first visit, the Odoo database manager appears — create a database from the web UI (admin_passwd = admin).
Updating an instance
Re-running sat install odoo-dev <instance> <version> refreshes only the repositories referenced in that instance's repos.yml (fetches new commits, best-effort on network failure) and re-runs gitaggregate to pull the latest code into parts/. Only the instance's own repos are touched — no global cache refresh.
Directory layout
~/git/ # shared git cache (all versions, all instances)
openeyedev/odoo-build # build template (all branches)
oca/ocb # OCB clone (all branches)
oca/web oca/server-tools/ # OCA repos (all branches each)
odoo/odoo # upstream Odoo (if referenced)
~/odoo-dev/ # dev instances root
myproject/
odoo/ # build directory
repos.yml # original (from template, may be edited)
repos.local.yml # generated — repos.yml with local paths
env/ # Python virtualenv
parts/ # gitaggregate output (odoo, OCA, custom)
addons/ # symlinked modules (generated)
etc/odoo.conf # dev-tuned config
start.sh # foreground launcher
logs/
data/ # Odoo filestore + sessions (data_dir)
Offline operation
If there is no internet connection, sat install odoo-dev uses the local cache as-is (best-effort fetch continues on failure). The only hard requirement is that the repos were cloned at least once while online. New instances of the same version can be created fully offline.
Python versions
The python-version file in the build repo requests a specific Python (e.g. 3.10 for Odoo 16/17, 3.12 for 18/19). Resolution order:
- Already on PATH (
python3.12) — use as-is - Previously built in
~/python/python3.12/— reuse - Build from source into
~/python/python3.12/— automatic (takes several minutes, needs internet for the download)
The build dependencies (libssl-dev, libffi-dev, zlib1g-dev, etc.) are installed by sat install odoo-dev-base. Built Pythons are shared across all dev instances.
Configuration
Dev settings are read from ~/.config/sat/dev.conf (optional — all have defaults):
| Variable | Default | Description |
|---|---|---|
SAT_DEV_ROOT |
~/odoo-dev |
Root directory for dev instances |
SAT_DEV_GIT_CACHE |
~/git |
Shared git cache location |
SAT_DEV_BUILD_REPO |
public odoo-build | Build template repo URL |
SAT_DEV_PORT |
8069 |
HTTP port (one instance at a time) |
Dev vs production
Production (sat install odoo) |
Dev (sat install odoo-dev) |
|
|---|---|---|
| Location | /opt/odoo/<inst>/ |
~/odoo-dev/<inst>/ |
| Requires root | Yes (sudo) | No (one-time sudo odoo-dev-base) |
| Process management | systemd service | Manual start.sh (foreground) |
| TLS / proxy | nginx + Let's Encrypt | None (localhost only) |
| Database | Dedicated PG user + DB per instance | Dev user (peer auth), DBs created from web UI |
workers |
2 | 0 (single-process, enables --dev=all reload) |
without_demo |
True | False (demo data on) |
list_db |
False | True (database manager UI) |
data_dir |
XDG default | ~/odoo-dev/<inst>/data/ (self-contained) |
| Push repo / custom branch | Yes | No |
Exim4
Install
# Set required variables in /etc/sat/sat.d/exim4.conf:
# MAIL_PRIMARY_DOMAIN=${SAT_DEFAULT_DOMAIN}
# MAIL_HOSTNAME=mail.${SAT_DEFAULT_DOMAIN}
# MAIL_ALIAS_DOMAINS=alias.${SAT_DEFAULT_DOMAIN},mail.${SAT_DEFAULT_DOMAIN} # optional
sudo sat install exim4
The script installs exim4, dovecot, and spf-tools-perl; creates the vmail user and /home/vmail; patches /etc/exim4/exim4.conf.template for virtual delivery and per-domain DKIM; and prints the DNS records you need to add.
Mailboxes
Manage mailboxes with the sat mail command:
sudo sat mail add contact@example.com # create (prompts for password)
sudo sat mail list # list all users
sudo sat mail list example.com # list by domain
sudo sat mail passwd contact@example.com # change password
sudo sat mail delete contact@example.com # remove (keeps maildir)
sudo sat mail delete --purge contact@example.com # remove + wipe maildir
sat mail add updates both /etc/exim4/domainlist/<domain> (so Exim4 accepts inbound mail) and /etc/dovecot/passwd (so Dovecot authenticates IMAP login). No service reloads needed — both files are read on demand.
All alias domains share the primary domain's mailboxes — mail to contact@example.net and contact@example.com lands in the same maildir.
Domain aliases
/etc/exim4/local_domains maps every accepted domain to the canonical (primary) domain:
example.com: example.com
example.net: example.com
example.org: example.com
To add another alias domain, append a line and reload exim4: systemctl reload exim4.
DKIM
Per-domain 2048-bit RSA keys live in /etc/exim4/dkim/. The keymap at /etc/exim4/dkim/keymap maps domain names to key files. The install script generates keys and prints the TXT records to add to DNS.
To add DKIM for a new domain:
openssl genrsa -out /etc/exim4/dkim/newdomain.com.private 2048
chown root:Debian-exim /etc/exim4/dkim/newdomain.com.private
chmod 640 /etc/exim4/dkim/newdomain.com.private
echo "newdomain.com: /etc/exim4/dkim/newdomain.com.private" >>/etc/exim4/dkim/keymap
# Add DNS TXT record and reload exim4
Health Check
# Run immediately (prints to stdout + saves to /var/log/sat/health-YYYY-MM-DD.txt)
sudo sat health
# Install a daily systemd timer (fires at 06:00, catches up if server was off)
sudo sat install health
Set HEALTH_REPORT_EMAIL in /etc/sat/sat.d/health.conf to receive the report by email each day (requires a working exim4 installation).
The check auto-detects installed sat services and reports on each:
| Check | What it covers |
|---|---|
| Disk | All non-tmpfs filesystems (warn ≥80%, crit ≥90%); journal size |
| Memory | RAM and swap usage |
| Load | 1/5/15-minute averages vs. CPU count |
| Systemd units | Any failed units |
| Package updates | Security updates pending |
| Exim4 | Service state, queue depth, recent rejections |
| Dovecot | Service state, active IMAP sessions |
| Forgejo | Service state |
| Nginx | Service state, recent error-log entries |
| Fail2ban | Service state, current ban counts per jail |
| Odoo | Service state + disk per instance |
Reports older than 30 days are deleted automatically (override with HEALTH_RETAIN_DAYS in sat.conf).
Migrating a Doodba/waftlib Instance to Sat
If an Odoo instance was previously set up with Doodba or its bare-metal variant waftlib, use sat migrate-waft to bring it under sat management. The database is never touched.
# 1. Add the instance domain to /etc/sat/sat.d/odoo.conf:
# ODOO_<INSTANCE>_DOMAIN=odoo.${SAT_DEFAULT_DOMAIN}
# 2. Build the sat structure while the old service stays active (takes a while):
sudo sat migrate-waft <instance> <version> --no-cutover
# 3. Verify the new structure looks correct, then cut over (seconds of downtime):
sudo sat migrate-waft <instance> <version>
What it does:
| Step | Action |
|---|---|
| repos.yaml → repos.yml | Adds parts/ prefix to repo keys; replaces $WAFT_DEPTH_* with 128 |
| OS user | Changes home dir to /opt/odoo/<instance>/; adds to odoo group |
| gitaggregate | Clones all repos from repos.yml into parts/ |
| Private modules | Symlinks parts/custom → original custom/src/private (no data moved) |
| odoo.conf | Written from scratch; key settings (workers, limits, data_dir) copied from waft conf |
| pg_hba | Adds specific peer rule for the instance (same as a fresh sat install) |
| Service | Installs odoo-<instance>.service; on cutover stops the old service and starts the new one |
After verifying everything works, the old waft layout at ~/odoo/ can be removed. The data_dir (filestore, sessions) continues to point at its original path — move it manually if desired.
PostgreSQL Backup
sudo sat install pg-backup
Installs a daily systemd timer (sat-pg-backup.timer) that fires at 02:00 and dumps every non-template PostgreSQL database to /var/backups/postgresql/. Each database gets its own subdirectory; cluster globals (roles, tablespaces) are saved alongside.
| File | Contents |
|---|---|
/var/backups/postgresql/<db>/YYYY-MM-DD.dump |
Per-database dump (pg_dump custom format) |
/var/backups/postgresql/globals-YYYY-MM-DD.sql |
Roles and tablespaces (pg_dumpall --globals-only) |
Restore a single database:
sudo -u postgres pg_restore -d <db> /var/backups/postgresql/<db>/YYYY-MM-DD.dump
Set PG_BACKUP_RETAIN_DAYS in /etc/sat/sat.conf to control retention (default: 7 days). Run immediately without installing the timer:
sudo sat pg-backup
Fail2ban
sudo sat install fail2ban
Installs fail2ban and writes /etc/fail2ban/jail.d/sat.conf with jails for every sat-managed service it finds on the server:
| Jail | Protects | Threshold |
|---|---|---|
sshd |
SSH port 22 | 5 / 10 min (Debian default) |
exim |
SMTP 25/465/587 | 3 / 10 min |
dovecot |
IMAP 143/993 | 5 / 10 min |
nginx-bad-request |
HTTP/S | 5 / 10 min |
nginx-botsearch |
HTTP/S | 2 / 10 min |
nginx-404flood |
HTTP/S | 20 / 1 min |
All bans last 24 hours. Key thresholds:
- exim: 3 (not 5) — common SMTP scanners burst exactly 4 attempts per cycle
- nginx-botsearch: 2 — legitimate clients never probe
/wp-adminetc. - nginx-404flood: 20 per minute — catches PHP shell probes and
.envharvesters that flood non-existent paths; too low to false-positive on real users
The script is safe to re-run after adding new services; it detects what is installed and rewrites the config.
Upgrading to Debian Trixie
# Take a VM snapshot or full system backup first — there is no automatic rollback.
sudo sat upgrade-trixie
Upgrades a sat-managed server in place from Debian 12 (Bookworm) to 13 (Trixie). The script is idempotent (a no-op if already on Trixie) and never reboots automatically — it prints a reminder at the end.
What it does, in order:
- Pre-flight checks (strict — abort unless safe): must be Bookworm, no held packages (
apt-mark showhold), at least 5 GB free on/, andsat pg-backupset up. Requires a typedYESconfirmation. Pass--forceto skip the aborts and the confirmation. - Snapshot: dumps
dpkg --get-selections, tars/etc, and runssat pg-backupfor a fresh database dump. - Fully update Bookworm:
apt update && apt upgrade && apt full-upgrade && apt autoremove. - Swap apt sources:
bookworm→trixiein/etc/apt/sources.listand/etc/apt/sources.list.d/*. Originals are backed up with a.pre-trixiesuffix. The stalebookworm-backports.list(if present) is removed. - Dist-upgrade:
apt update && apt upgrade && apt full-upgradewithNEEDRESTART_MODE=noninteractiveso service-restart prompts don't hang the run. Followed bydpkg --configure -aas a safety net. - PostgreSQL 15→17 migration: runs
pg_upgradecluster 15 mainto copy data into the new PG 17 cluster Trixie installs alongside 15, verifies all sat databases are visible on 17, then drops the old 15 cluster. A freshpg_dumpfrom step 2 is the fallback. - Reconcile sat services: automatically re-runs the idempotent installers for detected components —
sat install exim4orsat install mail-relay(detected via sat markers in config files, not justcommand -v exim4),sat install nextcloud(PHP 8.2→8.4 FPM socket path changes),sat install fail2ban(re-detects services). Re-enables sat systemd timers and reloads nginx. - Verify and report: prints the new Debian version, kernel, PostgreSQL version, and service status, then reminds you to reboot.
# After the script finishes, reboot to start the new kernel:
sudo reboot
# Then verify everything came back up:
sudo sat health
Pre-commit checks
A git pre-commit hook (.githooks/pre-commit) runs check-only syntax and style gates on staged files. Enable it once per clone:
git config core.hooksPath .githooks
vps-bootstrap.sh does this automatically on fresh VPS setups. The hook never modifies files — it only reports violations and blocks the commit.
| Check | Scope | Tool | When |
|---|---|---|---|
| Bash syntax | *.sh + sat (by shebang) |
bash -n |
always |
| ShellCheck | bash files | shellcheck -x |
if installed |
| No tabs | all staged text files | grep -P '\t' |
always |
| EditorConfig | staged files | editorconfig-checker |
if installed |
| Python line-length | *.py |
built-in awk (≤88) |
always |
| Python lint | *.py |
ruff check --line-length 88 |
if installed |
Optional tools (shellcheck, ruff, editorconfig-checker) are auto-detected at runtime and skipped with a note if absent. Indentation is 4 spaces for most files; JSON, YAML, and JS use 2 spaces (see .editorconfig). Bash is exempt from the 88-char line limit — that rule is Python-specific — but bash is governed by ShellCheck and the no-tabs rule.
Configuration
All sensitive values live in /etc/sat/sat.conf and /etc/sat/sat.d/*.conf (never in the git repo). The main sat.conf defines globals (SAT_DEFAULT_DOMAIN, LETSENCRYPT_EMAIL) and includes all *.conf files from /etc/sat/sat.d/. See sample.conf.d/ in the repo for per-component samples.
License
AGPL-3.0-or-later — see LICENSE.
Copyright 2026 Ronald Portier (Open Eye Development)