#!/usr/bin/env perl # Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) # SPDX-License-Identifier: MIT # # install.pl: install nuntius as a systemd service. # # nuntius ships as a single static binary. Build it on your workstation, upload # the binary, the systemd unit, and the .env template to the server, then run # this script there as root from that directory: # # scp bin/nuntius nuntius.service .env.example user@host:/tmp/nuntius/ # ssh user@host # cd /tmp/nuntius && sudo perl install.pl # # It expects ./nuntius and ./nuntius.service (and optionally ./.env.example) in # the current working directory. Idempotent: re-running is safe. It skips the # user, config, and .env when they already exist, and never starts the service. use strict; use warnings; my $nuntius_user = 'nuntius'; my $bin_dest = '/usr/local/bin/nuntius'; my $config_dir = '/etc/nuntius'; my $config_file = '/etc/nuntius/config.toml'; my $env_file = '/etc/nuntius/.env'; my $data_dir = '/var/lib/nuntius'; my $service_dest = '/etc/systemd/system/nuntius.service'; sub note { print qq{==> $_[0]\n} } sub warn_ { print qq{WARN: $_[0]\n} } sub fail { print qq{ERROR: $_[0]\n}; exit 1 } # run executes one child in list form, so no argument is ever word-split or # globbed, and dies naming the attempt when the child fails. sub run { my (@cmd) = @_; system(@cmd) == 0 or die qq{ERROR: could not run '$cmd[0]': exit code } . ($? >> 8) . qq{\n}; } # silenced runs a block with STDOUT and STDERR pointed at /dev/null, restored # afterwards, so the caller sees only what it decides to print. sub silenced(&) { open(my $save_out, '>&', \*STDOUT) or die qq{ERROR: could not save STDOUT: $!\n}; open(my $save_err, '>&', \*STDERR) or die qq{ERROR: could not save STDERR: $!\n}; open(STDOUT, '>', '/dev/null') or die qq{ERROR: could not silence STDOUT: $!\n}; open(STDERR, '>&', \*STDOUT) or die qq{ERROR: could not silence STDERR: $!\n}; my $result = $_[0]->(); open(STDOUT, '>&', $save_out) or die qq{ERROR: could not restore STDOUT: $!\n}; open(STDERR, '>&', $save_err) or die qq{ERROR: could not restore STDERR: $!\n}; return $result; } sub require_root { $> == 0 or fail('Run as root: sudo perl install.pl'); } sub require_files { -f './nuntius' or fail('./nuntius binary not found: copy it here first (e.g. rsync bin/nuntius).'); -f './nuntius.service' or fail('./nuntius.service not found: copy it from the repository root.'); } sub create_user { if (silenced { system('id', $nuntius_user) == 0 }) { note("User $nuntius_user already exists."); return; } note("Creating system user $nuntius_user..."); run('useradd', '--system', '--shell', '/usr/sbin/nologin', '--home-dir', $data_dir, '--user-group', $nuntius_user); } sub create_data_dir { note("Setting up $data_dir..."); unless (-d $data_dir) { mkdir($data_dir, 0750) or die qq{ERROR: could not create $data_dir: $!\n}; } my $uid = getpwnam($nuntius_user) or die qq{ERROR: could not look up user $nuntius_user\n}; my $gid = getgrnam($nuntius_user) or die qq{ERROR: could not look up group $nuntius_user\n}; chown($uid, $gid, $data_dir) or die qq{ERROR: could not chown $data_dir: $!\n}; chmod(0750, $data_dir) or die qq{ERROR: could not chmod $data_dir: $!\n}; } sub install_binary { note("Installing binary to $bin_dest..."); run('install', '-m', '0755', './nuntius', $bin_dest); } sub install_service { note("Installing systemd unit $service_dest..."); run('install', '-m', '0644', './nuntius.service', $service_dest); } sub generate_config { if (-f $config_file) { note("Config $config_file already exists; leaving it untouched."); return; } note("Generating $config_file..."); unless (-d $config_dir) { mkdir($config_dir, 0755) or die qq{ERROR: could not create $config_dir: $!\n}; } # nuntius writes the template config on first start; run it briefly, then # let `timeout` send SIGTERM (graceful shutdown). The write happens at # startup, before the timeout elapses. timeout exits 124 on SIGTERM and # 137 on SIGKILL, so its status is deliberately not checked here. silenced { system('timeout', '-k', '5s', '4s', $bin_dest) }; if (!-f $config_file) { warn_("nuntius did not create $config_file; check the uploaded binary."); } # Fix ownership in case nuntius wrote files as root. run('chown', '-R', "$nuntius_user:$nuntius_user", $data_dir); } sub install_env { if (-f $env_file) { note("$env_file already exists; leaving it untouched."); } elsif (-f './.env.example') { note("Creating $env_file from .env.example (edit it!)..."); run('install', '-m', '0600', './.env.example', $env_file); run('chown', "root:$nuntius_user", $env_file); } else { warn_(".env.example not found; skipping $env_file. Create it before starting."); } } sub enable_service { note('Reloading systemd and enabling nuntius...'); run('systemctl', 'daemon-reload'); run('systemctl', 'enable', 'nuntius.service'); } sub final_notes { print <<"END_NOTES"; ============================================================ nuntius installed and enabled, but NOT started. Next manual steps: 1. Set the SMTP password: sudo \$EDITOR $env_file 2. Edit the auto-generated config (smtp.host, smtp.user, allowed_origins): sudo \$EDITOR $config_file 3. Start the service: sudo systemctl start nuntius sudo journalctl -u nuntius -f 4. Add the Caddy reverse_proxy directive (see README.md), then: sudo caddy validate && sudo systemctl reload caddy ============================================================ END_NOTES } require_root(); require_files(); create_user(); create_data_dir(); install_binary(); install_service(); generate_config(); install_env(); enable_service(); final_notes();