Ga naar inhoud

Run as a service

Deze inhoud is nog niet beschikbaar in uw taal.

An install started from a shell dies with the shell. The project ships three systemd unit files under etc/examples/systemd/ — onetimesecret-web.service, onetimesecret-worker.service and onetimesecret-scheduler.service. Use them as the starting point, then add the storage permission below if you use full authentication with SQLite.

They assume a finished bare-metal install, so work through Install on Linux first.

From the application checkout, copy the units:

Use your administrative shell for this section, not the onetime service account. Install on Linux enters that account’s login shell; run exit to return to your administrative shell, then change to /var/lib/onetimesecret before running the commands below. The service account does not need sudo rights. Keep the SQLite directory grant below when using the default full-mode database.

Terminal window
sudo cp etc/examples/systemd/onetimesecret-*.service /etc/systemd/system/

Full mode with SQLite: allow the account database directory

Section titled “Full mode with SQLite: allow the account database directory”

The default AUTH_DATABASE_URL is sqlite://data/auth.db. With the shipped working directory, this is /var/lib/onetimesecret/data/auth.db. The units use ProtectSystem=strict and grant writes to tmp and log only. Without an additional grant, SQLite cannot create or update the database or its journal.

Before starting any service that uses this database, add a drop-in for all three units:

Terminal window
for unit in onetimesecret-web onetimesecret-worker onetimesecret-scheduler; do
sudo install -d -m 0755 "/etc/systemd/system/$unit.service.d"
printf '[Service]\nStateDirectory=onetimesecret/data\nStateDirectoryMode=0750\n' |
sudo tee "/etc/systemd/system/$unit.service.d/sqlite.conf" >/dev/null
done

StateDirectory creates the directory with the unit’s onetime ownership and allows writes there despite ProtectSystem=strict. It adds to the shipped tmp and log entries; do not reset that list or disable filesystem protection. See the systemd directory settings.

This drop-in is for the default SQLite path. For a custom path, create its parent directory with onetime ownership and grant that directory with ReadWritePaths= in each unit instead. SQLite needs directory access for journals as well as access to the database file. PostgreSQL deployments do not need this SQLite directory grant.

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now onetimesecret-web

enable --now starts the service and enables it at boot. If applying a drop-in to running services, restart the web service and any worker or scheduler you already run so they receive the new permissions.

Inspect systemctl cat onetimesecret-web and journalctl -u onetimesecret-web if it fails. In full mode, create the first account, sign in, restart the service, and sign in again to check database writes and persistence. Merely reaching the homepage does not exercise those writes.

Each unit runs as the user and group onetime, with WorkingDirectory set to /var/lib/onetimesecret, and expects Valkey or Redis to be up already — the web unit orders itself After=network.target redis-server.service valkey.service, hedging across both common service names. The worker and scheduler additionally order themselves after onetimesecret-web.service.

The environment comes from .env in the working directory, sourced inside ExecStart rather than through systemd’s EnvironmentFile:

ExecStart=/bin/bash -lc 'set -a; source .env; set +a; exec bundle exec puma -C etc/puma.rb'

The worker and scheduler use the same form around bin/ots worker and bin/ots scheduler.

The -l in bash -lc is load-bearing beyond .env. It is what puts a Ruby installed for the onetime user on the service’s PATH — but only through the files a login shell actually reads, which are /etc/profile and then the first of ~/.bash_profile, ~/.bash_login and ~/.profile. ~/.bashrc is not among them, so a version manager initialized there is invisible here and the unit dies with bundle: command not found. Install on Linux writes the rbenv line into ~/.profile for that reason; if you set Ruby up some other way, put its initialization in one of those three files, or in an /etc/profile.d/ snippet that /etc/profile picks up.

Setting PATH through systemd’s Environment= or EnvironmentFile= is not a substitute. Those are applied to the process systemd starts, which here is the login shell, and /etc/profile then runs and sets its own PATH over the top.

If the app is not at /var/lib/onetimesecret

Section titled “If the app is not at /var/lib/onetimesecret”

WorkingDirectory is a literal path, not a search. An install that sits in the onetime user’s home directory, or under /opt, will not be found, and systemd fails the unit before Puma runs. Move the install to /var/lib/onetimesecret rather than editing the units: it is the layout the shipped files are written against, and it keeps all three of them correct as delivered. If you keep the app elsewhere, change WorkingDirectory in all three files and keep them identical — the web unit’s own header tells you to adjust it.

ProtectHome=true is a second reason to keep the shipped layout. It makes /home inaccessible to the service, so an install — or a version manager — under a user’s home directory in /home stays unreachable even once WorkingDirectory points at it. /var/lib/onetimesecret is not under /home and is unaffected.

When you need the worker and the scheduler

Section titled “When you need the worker and the scheduler”

The worker and scheduler unit headers say they are required for full authentication mode with PostgreSQL and RabbitMQ. That is wrong in both halves, and the real rule is simpler: background jobs are off unless you set JOBS_ENABLED=true. While jobs are off, the web process sends email synchronously in-process, nothing publishes work for the worker to consume, and both extra services would sit idle. RabbitMQ matters in exactly the same case — it is the broker the worker consumes from, and nothing needs it until jobs are on.

Authentication mode does not decide this. Full mode adds a SQL account database, which defaults to SQLite, not a job queue; a full-mode instance with jobs off runs on the web unit alone.

JOBS_ENABLED is read from the environment, so it goes wherever the units get their environment from: .env in the working directory, or /etc/default/onetimesecret. Set it, then enable both units:

Terminal window
sudo systemctl enable --now onetimesecret-worker onetimesecret-scheduler

While jobs are off, the health endpoint reports the job queue as not_configured rather than as a failure, so an instance running on the web unit alone does not look broken there.

All three restart on-failure after 5 seconds, with a start limit of five attempts per 60 seconds so a genuinely broken instance stops flapping and stays down where you can see it. Stop timeouts differ deliberately: 30 seconds for the web and scheduler units, 60 seconds for the worker so in-flight jobs can drain before it is killed.

Each unit runs with NoNewPrivileges=true, ProtectSystem=strict, ProtectHome=true and PrivateTmp=true, and declares StateDirectory=onetimesecret/tmp onetimesecret/log. Under ProtectSystem=strict the filesystem is read-only apart from what the unit is granted, so any additional path the app has to write to needs granting deliberately.

EnvironmentFile=-/etc/default/onetimesecret is present in all three units and optional — the leading - means the service starts whether or not that file exists. It is a second, additive source of environment variables, for values you would rather keep outside the app directory or vary between hosts.

Procfile.example defines the same processes for any Procfile runner — foreman, hivemind or goreman. Only the web line is uncommented; the worker and scheduler lines are shipped commented out. Export the environment first, the same way the units do:

Terminal window
set -a; source .env; set +a
foreman start -f Procfile.example

In a container none of this applies. The main image runs a single process, and multi-process supervision is the S6 variant’s job.

Once the services are running, confirm the instance actually serves: Verify your install.