Run it as a service
The installer leaves a systemd unit enabled and running. This page reads that
unit line by line, shows how to drive it, and describes what the two failure
shapes look like from the outside: a server that will not start and a
configuration the running server refuses.
🧾 What the unit does
Section titled “🧾 What the unit does”systemctl cat pingclairThe keys that matter are these:
[Service]Type=notifyNotifyAccess=mainUser=pingclairGroup=pingclairAmbientCapabilities=CAP_NET_BIND_SERVICECapabilityBoundingSet=CAP_NET_BIND_SERVICEEnvironment="RUST_LOG=info"ExecStart=/usr/local/bin/pingclair run /etc/Pingclair/PingclairfileExecReload=/bin/kill -USR1 $MAINPIDWorkingDirectory=/var/lib/pingclairRestart=on-failureRestartPreventExitStatus=1RestartSec=5sLimitNOFILE=1048576LimitNPROC=512ProtectSystem=fullPrivateTmp=trueNoNewPrivileges=trueRead them in order:
Type=notifyandNotifyAccess=main: the server tellssystemdwhen its listeners are bound, sosystemctl startblocks until the proxy can answer rather than until the process exists.User=pingclairwithAmbientCapabilities=CAP_NET_BIND_SERVICE: the server runs unprivileged and can still bind ports 80 and 443.- There is deliberately no
PINGCLAIR_TLS_STOREhere. The service account’s home is/var/lib/pingclair, so certificates live at/var/lib/pingclair/.local/share/pingclair: the binary’s own default, the directory the installer creates and migrates into, and the pathpingclair environprints. Naming a store here would be a second answer to a question that already has one. - There is deliberately no
ExecStartPrerunningvalidate. It looks like the safe place for that check, and it is the trap:systemdappliesRestartPreventExitStatus=to the main process, not to a failing pre-command, so a configuration the compiler refuses was retried every five seconds instead of leaving the unit failed. The server compiles the file itself before it binds anything and exits 1 when it refuses it, which is the exit code the restart policy above was written for —pingclair runexists to be that process. ExecReloadsendsSIGUSR1, which is the signal the server treats as “read the file again”.SIGHUPis deliberately ignored, and a unit that sent it reported success while the old configuration kept serving (issue #66). Becausesystemdcan only observe thatkillexited, the server publishes what it made of the file on this unit’s status line —Serving (reloaded 1 listener(s) in 323.341µs), orReload rejected: …— whichsystemctl statusshows. The reload section below is the long version.Restart=on-failurewithRestartPreventExitStatus=1andRestartSec=5s: exit code 1 means the configuration or the certificate store could not be used at all, so the unit is leftfailedfor an operator to look at rather than retried every five seconds. Any other failure is restarted.ProtectSystem=full,PrivateTmp,NoNewPrivileges,LimitNPROC, andLimitNOFILE: the server gets the filesystem view and the process limits it needs, and nothing beyond them.
Both install paths write this same file. The one-liner embeds a byte-for-byte
copy of scripts/pingclair.service — just repo-lint fails when the two drift —
so a fresh curl | bash install and a checkout install produce the same unit,
and systemd-analyze verify /etc/systemd/system/pingclair.service says
nothing about this unit on either path.
🎛️ Driving the service
Section titled “🎛️ Driving the service”pc service wraps systemctl for this unit, so the two are interchangeable:
| Task | With pc |
With systemctl |
|---|---|---|
| Start | sudo pc service start |
sudo systemctl start pingclair |
| Stop | sudo pc service stop |
sudo systemctl stop pingclair |
| Reload the configuration | sudo pc service reload |
sudo systemctl reload pingclair |
| Restart, after a listener or a process-wide change | sudo pc service restart |
sudo systemctl restart pingclair |
| State | pc service status |
systemctl status pingclair |
| Follow the log | — | journalctl -u pingclair -f |
pc service status prints the unit’s own view, including the readiness line the
server sent:
● pingclair.service - Pingclair High-Performance Web Server Loaded: loaded (/etc/systemd/system/pingclair.service; enabled; preset: enabled) Active: active (running) since Tue 2026-09-22 05:57:21 UTC; 18s ago Docs: https://github.com/dorianverlaine/pingclair Main PID: 27630 (pingclair) Status: "Serving"🔁 What a reload means
Section titled “🔁 What a reload means”An edited /etc/Pingclair/Pingclairfile reaches the running server through one
signal, and two commands send it.
SIGUSR1 is the reload signal, and it needs no configuration of its own:
sudo kill -USR1 "$(systemctl show -p MainPID --value pingclair)"pc service reload — or sudo systemctl reload pingclair, which is the same
call — sends that signal for you. The unit’s ExecReload is
/bin/kill -USR1 $MAINPID, so the obvious command is now the working one; a
unit that sent SIGHUP instead reported success and applied nothing, which is
what issue #66 recorded.
pingclair reload reaches the same code through the Admin API and reports what
the server thought of the file, which needs the admin option from the global
options block:
✅ Configuration reloaded successfullyError: ❌ Reload failed (400): HTTP/1.1 400 Bad Requestsystemctl reload can report one thing only: that kill delivered the signal.
The server reads the file afterwards, so its verdict goes to the unit’s status
line and to the journal instead. pc service reload says so rather than
claiming the configuration was applied:
$ sudo pc service reload✅ Reload signal delivered to pingclair.serviceℹ️ The result lands a moment later: `systemctl status pingclair` or `journalctl -u pingclair -n 20`$ systemctl status pingclair --no-pager | grep Status Status: "Serving (reloaded 1 listener(s) in 323.341µs)"When the running server cannot apply what the file asks for, the old
configuration keeps serving and the status line says which change was refused.
Moving the site from :80 to :8080 is the common case, because listener
topology is rebuilt with the sockets at startup:
Status: "Reload rejected: listener topology changed (added: ["[::]:8080"], removed: ["[::]:80"]); restart Pingclair to rebuild H1, H2, H3, and TLS together"Whatever path you use, a configuration that does not compile leaves the previous one running, so the site keeps answering. Validate first:
sudo pingclair validate /etc/Pingclair/PingclairfileProcess-wide policy is the exception. Options that are established at startup,
such as trusted_proxies, only take effect after a restart:
sudo pc service restart. A configuration that adds or moves a listener is
refused the same way — the status line names the addresses that were added and
removed — because reload applies policy, not a new listening socket.
📜 Logs
Section titled “📜 Logs”The unit sets RUST_LOG=info and sends everything to the journal:
sudo journalctl -u pingclair -fsudo journalctl -u pingclair --since '10 min ago'Startup, reloads, certificate work, and one access line per request appear there:
INFO pingclair::run: 🚀 Starting Pingclair v0.2.0-rc.3INFO pingclair::run: 📄 Loaded configuration from: /etc/Pingclair/PingclairfileINFO pingclair::run: 🔔 Received SIGUSR1, reloading configuration from: /etc/Pingclair/PingclairfileINFO pingclair::run: ✅ Configuration reload completed successfully in 323.341µsINFO pingclair::run: 📊 1 listener(s) updatedINFO pingclair_proxy::server: 📝 Access request_id="65c09fa25d457-6" method="GET" host="localhost" path="/" status=200 bytes=18747 duration_ms=0 remote_ip=::1 user_agent="curl/8.18.0"A reload the server refuses is logged the same way, with the reason and a note that nothing changed:
ERROR pingclair::run: ❌ Configuration reload rejected after 414.491µs: listener topology changed (added: ["[::]:8080"], removed: ["[::]:80"]); restart Pingclair to rebuild H1, H2, H3, and TLS together kind=RestartRequiredERROR pingclair::run: 💡 Previous configuration remains active, unchangedFor a log of its own, with rotation, configure a log sink and write it under
/var/log/pingclair, which the installer creates and gives to the service user.
⚠️ When the service will not come up
Section titled “⚠️ When the service will not come up”is-activesaysactivatingandNRestartskeeps climbing. That is the retired behaviour of an older unit, and it had two causes. It carriedRestart=alwayswith noRestartPreventExitStatus, and it ranvalidateas anExecStartPrecommand, whichRestartPreventExitStatusdoes not cover — so a configuration the compiler refuses was retried every five seconds and looked like a unit that never settles rather than one that failed. The installed unit carriesRestart=on-failure+RestartPreventExitStatus=1and no pre-command, and a refused start leavesis-activeatfailedwithNRestartsat zero. On an older install, stop the loop before debugging:sudo systemctl stop pingclair, fix the file, thensudo systemctl reset-failed pingclair.Job for pingclair.service failed because the control process exited with error code. The server refused the configuration before it bound anything, and the compiler’s reason is in the journal, for exampleError: ❌ Configuration Error: Compile error: Unsupported feature: `encode br`: Brotli is not implemented for proxied responses; use `encode zstd gzip`.TLS store /var/lib/pingclair/.local/share/pingclair is not writable: Permission denied. The store belongs to the service account. Checksudo ls -ld /var/lib/pingclair/.local/share/pingclair; it should be owned bypingclair.systemd-analyze verifyreportsMissing '=', ignoring linefor the installed unit. An older one-liner install wrote a unit whose comments had been expanded by the shell — 25 lines of--helpoutput, whichsystemdignores. Reinstalling from the current installer writes the unit verbatim and the report goes away.- Nothing answers although the unit is running. The listeners are bound and the requests do not arrive. Check the provider’s firewall and then the host’s, as on the install page.
🧭 Next steps
Section titled “🧭 Next steps”- Upgrading and removing: what a re-run preserves, and how to take it all out again.
- HTTPS: certificates, including where the store lives and why
pingclair trustneedsPINGCLAIR_TLS_STORE. log: the access-log sink this page reads from the journal.
