Reloading the configuration
acme-proxy reloads its configuration on SIGHUP, without restarting:
sudo systemctl reload acme-proxy
# or, directly:
kill -HUP "$(pidof acme-proxy)"
Nothing is dropped. Connections stay open, in-flight ACME orders keep their state, and queued background work keeps its place. What changes is what the server does with the next request — and, if you moved a listener, on which port it answers it.
A reload is a rebuild and a swap, not a patch. Both routers, every profile’s filter chain and challenge registry, the notification backends, the job registry and both TLS acceptors are constructed fresh from the file on disk — and only once every one of them has succeeded is any of it published.
All or nothing
A reload either applies completely or changes nothing at all.
Three things stop one, and each says so in the log:
| Log event | What happened |
|---|---|
server_config_reload_refused | A key that cannot change while the process runs did change. |
server_config_reload_failed | The file did not load, or what it asks for could not be built. |
server_config_reloaded | It applied. |
In the first two cases the server carries on with exactly the configuration it already had. That matters more than it sounds: a reload that applied the half it understood would leave a running server that no file on disk describes, and “what is this thing actually running?” would stop having an answer.
Every success carries a generation — 1 for the configuration the process
started with, and one higher for each reload that landed. It is the quickest
way to answer “did my SIGHUP take?”:
journalctl -u acme-proxy | grep server_config_reloaded
What a reload cannot change
One key. A refusal names it, what the server is running, and what the file now says:
`database.url` cannot be changed while the server is running
(running with `sqlite://sqlite.db`, the file now says `sqlite://other.db`):
restart to apply it
| Key | Why a restart is needed |
|---|---|
database.url | The connection pool is open, and the accounts and orders this CA has issued against it do not follow it elsewhere. A different database is a different CA. |
Everything else reloads. That was not always true, and the four keys that came
off this table most recently are worth knowing about because they are the ones
an operator is most likely to remember as frozen: the set of enabled profiles,
each profile’s [signer] section, dns.resolver and [proxy]. See
Adding and removing endpoints below.
What a reload does change
Everything else, including the things operators reach for most:
- Access policy —
[filter]rules and checks, and the[ipam]inventory they consult. Tightening a rule takes effect on the next request. - The set of endpoints, and each one’s
[signer]— see below. - Egress —
dns.resolverand[proxy]. Every outbound client is rebuilt, including the signer backends, which used to be the reason these two were frozen. - Notification backends — a new webhook, a changed template directory, a
different
eventslist. - Challenge settings, order and account policy,
[meta],[eab]. - TLS certificates —
server.tls.cert_pathandkey_path(and the admin listener’s own pair) are re-read, so a renewed certificate is served to the next connection while established ones are undisturbed. - The listeners themselves — see below.
- The panel’s templates —
admin.template_diris recompiled. A template that does not parse fails the reload, so a mistake never reaches a browser. - Retention —
audit.retention_daysandjobs.retention_days. A sweep already scheduled keeps its current time and picks the new cutoff up on its next run. - Background work — every
[jobs]key, so slowing a retry storm, widening a lease or raising concurrency mid-incident costs nothing. See below. - Logging — every
[logging]key, so raising the level or switching to JSON mid-incident costs nothing. The swap is the first thing a reload publishes, so theserver_config_reloadedline that confirms it is already under the new settings. One caveat:RUST_LOGstill outrankslogging.filter, exactly as it does at startup, so with it set an edited filter changes nothing — the server says so withserver_logging_filter_overridden.
For what each key means, see the configuration reference.
Adding and removing endpoints
Add a [profiles.<name>] section and signal, and that endpoint starts serving:
server_config_reloaded generation=2 profiles=["le", "staging"]
profile_mounted profile=staging directory=https://ca.example/profile/staging/directory
Remove it and signal again, and it stops. Endpoints that were already running are not disturbed either way — no connection is dropped and no in-flight order loses its state, which is the whole reason this is worth doing without a restart.
Three things to know.
profile_mounted fires only for an endpoint that was not there before. It
is a lifecycle event, delivered to whichever [notify] backends are configured,
so re-firing it for every endpoint on every SIGHUP would make the notification
surface noisiest in exactly the config-managed deployments that would least want
it.
Unmounting keeps the data. The accounts and orders belonging to that
endpoint stay in the database and come back exactly as they were if you mount it
again — the profile name is what they are keyed on. Setting enabled = false is
the same thing as removing the section.
Drain a relay endpoint before removing it. Unmounting the last profile a
relaying backend serves takes its job handler with it, so any issuance still
waiting on the upstream has nothing left to finish it. Those rows sit in the
queue until the endpoint is mounted again, and the orders behind them expire.
Nothing else is affected, and no other backend has background work to lose.
Editing a signer
A profile’s [signer] section reloads, including the one an endpoint is
actively issuing with. The obvious worry — that a rebuilt local CA would forget
what it had revoked — does not arise: revocations and the CRL live in the
database, which the running CA and its replacement both read, so a revocation
that lands during the reload is not lost either, and GET /crl answers
identically across the swap. A relay serving http-01 keeps its published key
authorizations in the database the same way, so an upstream CA fetching one
mid-reload still gets it.
A backend whose section did not move is not rebuilt at all. That matters most
with key_source = "pkcs11": an ordinary reload does not log in to the token
again.
Two edges:
- Changing where the CA material lives is a new CA. Point
crl_pathat a different file and the endpoint starts from that file’s revocation history, not the old one’s. That is the intended reading of the key, but it is worth saying, because the certificates already issued do not move with it. [dns]and[proxy]rebuild every backend. They are not[signer]keys, but an outbound client caches them, so a change to either has to reach the signers to mean anything. Nothing is lost — the same handover applies.
Moving a listener
All seven keys that decide where a socket is, or whether there is one, reload:
server.bind_address, server.tls.enabled, admin.enabled,
admin.bind_address, admin.tls.enabled, metrics.enabled and
metrics.bind_address. A reload that moved one says which:
server_config_reloaded generation=2 listeners_rebound=["acme"]
Three things are worth knowing before you use it.
A bad address refuses the reload; it does not take the socket down. Every
new socket is bound before anything is published, so a port already in use, a
name that does not resolve or a privileged port you no longer have the
capability for is a server_config_reload_failed — with the listener that is
already running still answering on the address it always had. Fix the file and
signal again.
Established connections are not disturbed. A rebind replaces the socket new connections arrive on; a request already in flight finishes, and a keep-alive connection opened before the move stays usable until its client closes it. The old socket stops accepting immediately, so nothing new arrives there.
Turning TLS on or off does not move the socket at all. The mode is decided
per connection, like the certificate: the next client to connect speaks the new
protocol on the same port, and listeners_rebound stays empty. Remember to move
server.base_url with it, or every signed request fails RFC 8555 §6.4’s URL
check — the server warns tls_base_url_mismatch when it can see the two
disagree.
One caveat on the panel. Switching admin.enabled off releases the socket and
empties its router, so nothing answers on it — but it does not sign anybody out:
sessions live in the database and are waiting when you switch it back on. Use
acme-proxy admin session revoke --all if that is what you meant. Switching it
off and on again does clear the login-attempt lockout, since the limiter goes
with the panel.
Retuning the job runner
All seven [jobs] keys reload. These are the knobs you reach for while
something is going wrong — an upstream CA rate-limiting you, a backlog draining
too slowly — so a restart to apply them would have dropped exactly the in-flight
orders you were trying to save.
The runner does not restart; it picks the new values up on its next pass and says so:
job_runner_retuned poll_interval_ms=250 lease_seconds=120 max_concurrent=16
That line is the confirmation worth grepping for. server_config_reloaded means
a generation was published; this means the runner is actually running under it.
It is only emitted when the pacing really moved, so reloads that touch other
sections stay quiet.
Each key lands at its own grain, and the differences are all in the same direction — nothing already in flight is disturbed:
poll_interval_mstakes effect immediately, without waiting out the old interval first.max_concurrentwidens at once when raised. Lowered, it takes back the slots that are free and reaches the new figure as running jobs finish; no job is cancelled to get there sooner.lease_seconds,retry_base_secondsandretry_max_secondsapply to the next job claimed. One already running keeps the budget and the backoff it started under.max_attemptsis frozen onto each job when it is queued, so a change applies to work queued from then on. Raising it is not a way to rescue a backlog that is about to give up — those rows keep the budget they were queued with.retention_daysrebuilds the sweep, including registering it when it goes from0to a real value.
Systemd
Add ExecReload to the unit so systemctl reload works:
[Service]
ExecReload=/bin/kill -HUP $MAINPID
See Deployment for the rest of the unit.
Two things to expect
Neither is a problem, but both look odd if you are not expecting them.
Startup warnings repeat. A reload re-emits the advisories that describe the
configuration it just applied — challenge_validation_bypassed,
filter_disabled, tls_disabled and the rest. That is deliberate: they are
written to stay visible for as long as the condition holds.
profile_mounted does not repeat. It is a lifecycle notification meaning
“this endpoint came up”, delivered to whichever [notify] backends are
configured. Firing it on every reload would make the notification surface
noisiest in exactly the config-managed deployments that would least want it.
Reloads a restart still handles better
Two edges, both brief and both identical to what a restart does:
- A request that was already in flight when the reload landed finishes under the old configuration. That includes the notification it may queue.
- For the moment it takes in-flight requests to drain, the old and new
admission limits are both in force, so concurrency can briefly reach twice
server.max_concurrent_requests.
If a change is important enough that neither is acceptable, restart.