Software to create services on Debian based systems
Find a file
Ronald Portier 9bdf875f55
Add import-mailbox-org.sh utility script
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.
2026-07-24 12:40:01 +02:00
.githooks [IMP] Add pre-commit hook 2026-07-09 15:13:02 +02:00
config/fail2ban/filter.d Add fail2ban jail for Nextcloud, HSTS includeSubDomains, explicit loglevel 2026-07-23 14:56:24 +02:00
sample.conf.d [IMP] Split configuration files per component 2026-07-14 12:40:49 +02:00
scripts exim4: auto-create and subscribe standard IMAP folders 2026-07-24 12:16:43 +02:00
utils Add import-mailbox-org.sh utility script 2026-07-24 12:40:01 +02:00
.editorconfig [IMP] Add pre-commit hook 2026-07-09 15:13:02 +02:00
.gitignore Move secrets from .env-secret to /etc/sat/env-secret 2026-06-11 11:48:32 +02:00
.shellcheckrc [ADD] New bookwork to trixie migration 2026-07-09 15:57:09 +02:00
AGENTS.md Fix nextcloud install: HEAD check before download, SHA256 first-line, database-pass for peer auth 2026-07-23 12:49:40 +02:00
LICENSE Initial commit: sat dispatcher + install_forgejo script 2026-06-09 13:11:46 +02:00
local-bootstrap.sh Add local-bootstrap.sh for developer machines 2026-07-13 11:06:00 +02:00
README.md [IMP] Split configuration files per component 2026-07-14 12:40:49 +02:00
sat [ADD] New bookwork to trixie migration 2026-07-09 15:57:09 +02:00
sat.conf.sample [IMP] Split configuration files per component 2026-07-14 12:40:49 +02:00
vps-bootstrap.sh [IMP] Split configuration files per component 2026-07-14 12:40:49 +02:00

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.conf file (see sat.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; tracks repos.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:

  1. Already on PATH (python3.12) — use as-is
  2. Previously built in ~/python/python3.12/ — reuse
  3. 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-admin etc.
  • nginx-404flood: 20 per minute — catches PHP shell probes and .env harvesters 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:

  1. Pre-flight checks (strict — abort unless safe): must be Bookworm, no held packages (apt-mark showhold), at least 5 GB free on /, and sat pg-backup set up. Requires a typed YES confirmation. Pass --force to skip the aborts and the confirmation.
  2. Snapshot: dumps dpkg --get-selections, tars /etc, and runs sat pg-backup for a fresh database dump.
  3. Fully update Bookworm: apt update && apt upgrade && apt full-upgrade && apt autoremove.
  4. Swap apt sources: bookwormtrixie in /etc/apt/sources.list and /etc/apt/sources.list.d/*. Originals are backed up with a .pre-trixie suffix. The stale bookworm-backports.list (if present) is removed.
  5. Dist-upgrade: apt update && apt upgrade && apt full-upgrade with NEEDRESTART_MODE=noninteractive so service-restart prompts don't hang the run. Followed by dpkg --configure -a as a safety net.
  6. PostgreSQL 15→17 migration: runs pg_upgradecluster 15 main to 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 fresh pg_dump from step 2 is the fallback.
  7. Reconcile sat services: automatically re-runs the idempotent installers for detected components — sat install exim4 or sat install mail-relay (detected via sat markers in config files, not just command -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.
  8. 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)