Repository navigation
v3.0.0
Docker-Sentinel is no longer in maintenance mode. v3.0.0 removes the Portainer connector and fixes well
over a hundred bugs found in a full review of the update engine, cluster mode, tag handling, notifications
and the web UI. Every fix was tested, and the release was checked on real Docker hosts running Engine 25,
28 and 29.
Before you upgrade
- Portainer connector removed. Sentinel no longer scans or updates containers through the Portainer
API. Run a Sentinel agent on those hosts instead (see Cluster Mode). Saved Portainer settings stay in the
database, so going back to v2.16 restores them. The:2image tag stays on v2.16.1. - Single sign-on accounts are now bound to your identity provider (issuer and subject), not matched by
username. A new SSO user can no longer take over an existing account by choosing its username. Existing
accounts are linked once, in one of these ways:- sign in with your password and link SSO from your Account page;
- an admin clicks "Allow SSO link" for your account;
- set
SENTINEL_OIDC_LINK_EXISTING=trueto link non-admin accounts without 2FA by username; - if your admins only ever signed in with SSO, set
SENTINEL_OIDC_LINK_ADMIN=<username>once, sign in,
then remove the variable.
- Keyless signature verification now needs the expected signer: set
cosign_identityand
cosign_oidc_issuerin Settings (orSENTINEL_COSIGN_IDENTITYandSENTINEL_COSIGN_OIDC_ISSUER).
Enforce mode now refuses updates when cosign is not available instead of letting them through. - Cluster mode: upgrade the server first. Agents now renew their certificates; agents older than v3.0.0
do not, so update them before their first year is up ("Auto-update agents" now works). New enrolment
tokens carry the server's certificate fingerprint. - Give Sentinel time to finish an update when it stops:
stop_grace_period: 2min compose, or
--stop-timeout 120withdocker run. remove_volumesnow means "start the updated container on fresh anonymous volumes", like
docker compose up -V. Off (the default) keeps them and their data. It applies to agents only when
"Remove volumes on agents too" is turned on.- History is pruned after 90 days (
history_retention_days,0keeps everything). Successful updates
and rollbacks are always kept. - 2FA: if you turned on 2FA before this release, your recovery codes are unusable. Turn 2FA off and on
again to get working ones. - Built with Go 1.27.2: macOS binaries need macOS 13 or later, and Sentinel exits at startup if
GODEBUGsets a setting Go 1.27 removed to its old value.
Highlights
- Updates and rollbacks never delete a container when something fails, on the server and on agents, and an
update interrupted by a crash or reboot is finished or undone when Sentinel starts again. - Data in anonymous volumes survives updates, and the new image's command, environment and labels apply.
Settings a container inherited from an image Sentinel updated it from before are cleared too, where
Sentinel's history recorded that update; otherwise recreate the container once from its compose file or
docker runcommand. - Containers sharing another container's network (
network_mode: container:X) can be updated. - Self-update works with published ports, using a short-lived helper container.
:latestcontainers keep following:latest, and a broken release is tried once, not every scan.- Private registries work for pulls, and lscr.io and GitLab tags are found.
- Cluster agents no longer drop out a year after enrolment, follow the server's settings, run lifecycle
hooks, and recover from a crash mid-update. - Custom notification templates are used by every provider, and digests name every container.
- The Viewer role can no longer read secrets.
The full list of changes is below.
Security
- Importing a config export dropped the signature verification settings
(verify_mode,cosign_path,cosign_keyless,cosign_key_path), so
an enforce mode came back disabled. A config import now keeps them. - Signature verification checked the tag as the registry served it at
check time, so the image could change between the check and the
create. Updates now check the digest they pulled, in the image's own
repository. - Keyless cosign verification accepted a signature from anyone with a
Sigstore certificate. It now checks the certificate identity and OIDC
issuer set incosign_identityandcosign_oidc_issuer(or
SENTINEL_COSIGN_IDENTITYandSENTINEL_COSIGN_OIDC_ISSUER; a saved
setting wins), matched exactly or, withcosign_identity_regexp/
SENTINEL_COSIGN_IDENTITY_REGEXPset to true, as regular expressions.
Without them keyless checks fail: in enforce mode each refused update
is recorded as failed in the history and a scan sends one notification
for them, and startup logs a warning naming the two settings. They
apply without a restart; switching keyless on or off, the key path and
the cosign path still need one. - With signature verification set to enforce, updates went ahead without
any signature check when cosign was not available (the official image
ships none), with only an info line at startup. Such updates are now
refused and recorded as failed ("signature verification is set to
enforce but cosign is not available"), each scan sends one notification
for them, and startup logs a warning; warn mode still updates and logs
each one. A mode saved in Settings now applies without a restart, and a
sentinel.verifylabel is honoured when the global mode is disabled. - Swarm service updates never had their image signature checked. They
now go through the same gate as containers: enforce blocks an image
that fails the check (or any update when cosign is not available), warn
logs and updates, and asentinel.verifylabel on the service wins
over the global mode. A digest-only service update checks the exact
digest it rolls out. - A cluster host being revoked could keep a valid certificate: a
certificate renewal arriving during the revoke was recorded just before
the host was removed, and its certificate was never revoked. Revoking
now revokes the current and any pending certificate and removes the
host in one step. The server also accepted any certificate its CA had
signed for a host's ID, including superseded renewals; agents must now
present the host's current certificate or its pending renewal. - The server signed certificate renewals whenever an agent asked, for any
key. It now signs one only when the agent's certificate has less than
half its life left, only for a new key, at most once per host every
five minutes, and only for ECDSA P-256 or P-384 keys. - One connection to the cluster port could open any number of streams,
each allowed a 64 MB message before any certificate was checked. The
server now allows 64 streams per connection. - An agent enrolling trusted whatever answered at
SENTINEL_SERVER_ADDR,
so anything on the network path could take the one-time token and
become that agent's server. Enrolment tokens now end with the
fingerprint of the server's CA (<secret>.<fingerprint>), the server
sends its CA in the TLS handshake, and the agent checks the two match
before it sends the token, and again on the CA it receives. Copy the
whole token. A token without the fingerprint (from an older server)
still enrols, with a warning that the server could not be checked;
agents before v3.0.0 can use the new tokens. GET /api/settingsreturned the OIDC client secret, the Nginx Proxy
Manager password, the inbound webhook secret and the agent enrolment
token in plaintext to the Viewer role and to tokens withsettings.view.
They are now left out, and the response carries<key>_set: "true"for
each one that is configured. The same secret list now drives the settings
read and the config export, so an export made without secrets no longer
contains the NPM password. A test fails if a stored setting that ends in
_secret,_passwordor_tokenis missing from the list.GET /api/settingsalso returned environment-sourced URLs unmasked, such
asSENTINEL_WEBHOOK_URLandSENTINEL_GOTIFY_URL, which often carry a
token in the path or query. URL values now lose their credentials (the
webhook and Gotify URLs keep only scheme and host), and a test fails if a
configuration value exposes a credential.GET /api/settings/webhook-info
returned the whole inbound webhook secret when it was 12 characters or
shorter; it now returns at most a four-character prefix.- After an account lockout expired, the failed-attempt count was not reset, so
one wrong password locked the account again for another 30 minutes. The
count now starts again when a lockout expires. - Webhook notification channels now mask every header that looks like a
credential (Basic auth, cookies, passwords as well as tokens and keys) and
the secret path of the webhook URL when channels are shown to the Viewer
role or exported without secrets. - Single sign-on could take over a local account. Sentinel matched an SSO
login to an existing account by username (or email), so anyone who could
set their username at the identity provider toadminwas signed in as the
local admin, skipping 2FA. Accounts are now bound to the provider's issuer
and subject, and login matches on that pair only. A new SSO identity whose
username clashes with an account it is not linked to is refused with a
clear message instead of being logged in as that account.- Signed-in users link their SSO identity from the Account page; an
administrator can remove the link from the user list. - Upgrade note: nothing links to an existing account by username on its
own any more. After upgrading, a user who signed in with SSO under an
older version is told "This account is not linked to your sign-in
provider yet" and needs one of three things: an admin clicks "Allow SSO
link" on the user list (the next SSO login with that username links the
account, once, within 24 hours; both steps are written to the activity
log), the user signs in with a password and links from the Account page,
or you switch onSENTINEL_OIDC_LINK_EXISTING=true(or the matching
toggle in Settings, which wins over the variable), which links unlinked
non-admin accounts by username. Admin accounts, and accounts with 2FA
or a passkey, are never linked by that switch. - Recovery for instances whose admins only ever signed in with SSO (their
password is unknown, so none of the routes above works): set
SENTINEL_OIDC_LINK_ADMIN=<username>and restart. The next SSO login
with that username links that one account, admin or not, once; Sentinel
logs a warning and writes the activity log, and remembers it has been
used for that account. Remove the variable afterwards.
- Signed-in users link their SSO identity from the Account page; an
- The first-run setup wizard is served over HTTPS when TLS is configured
(SENTINEL_TLS_AUTOor a certificate), instead of plain HTTP. - A 2FA code could be used again within its 30-second window, and a password
success cleared the sign-in throttle, so someone holding a password got
many more code guesses than intended. A code is now accepted once,
entering the password no longer resets the throttle, and a pending 2FA
sign-in is thrown away after five wrong codes. - Request bodies are capped (64 KB on the public sign-in endpoints, 8 MB
elsewhere), closing a container log no longer leaks a goroutine, and the
inbound webhook help says the secret goes in theX-Webhook-Secretheader. - Behind a reverse proxy, five failed logins from one attacker blocked
sign-in for everyone, because every client shared the proxy's address. Set
SENTINEL_TRUSTED_PROXIESto the proxy's IP or CIDR range (comma
separated) and Sentinel takes the client fromX-Forwarded-For. With it
unset nothing changes.
Added
DELETE /api/swaps/{name}(admin only) discards the record of an update
Sentinel could not finish on its own, once you have fixed the containers
by hand. Sentinel tells you when that is needed, after three failed
attempts. There is no dashboard button for it yet.- Self-update works when Sentinel publishes a host port, for the hub, a
standalone instance and an agent. A running container cannot free its own
port or database lock before the new one starts, so every earlier attempt
rolled back. Sentinel now sets the replacement up and starts a short-lived
helper container from the new image (the hiddenself-update-helper
subcommand, with the Docker socket mounted). The helper stops the old
container, starts the new one, waits for it to stay up (and healthy, if it
has a health check), puts the old one back if it does not, and leaves the
outcome in the data directory; the new instance logs it on startup. The
replacement is built the same way as every other recreate, so it keeps its
networks, anonymous volumes and the new image's defaults. If the Docker
socket is not mounted in the Sentinel container (a TCP Docker host), the
old in-process swap is used, which still needs no published ports.
Leftover-old-<timestamp>containers and helpers are removed at start,
for agents too. - The new Sentinel image goes through the signature gate before a
self-update: in enforce mode it is refused when cosign is missing or the
signature does not verify, and in warn mode the failure is logged. Agents
rely on the hub's check of the target image. - Sentinel sends the registry credentials it has stored to the Docker daemon
(X-Registry-Auth) for every pull and digest lookup, so a private registry
no longer needsdocker loginon the host, and for Swarm service updates.
For remote hosts the hub sends the credential for the target image's
registry with the update request (new optionalregistry_authfield,
number 20, onUpdateContainerRequest; older agents ignore it). - AWS ECR, Azure ACR and Google GCR/Artifact Registry logins, advertised
since v2.9 but never connected, now work. Configure a provider by saving
JSON under the settingcloud_auth_ecr(region,access_key,
secret_key),cloud_auth_acr(tenant_id,client_id,
client_secret,login_server) orcloud_auth_gcr
(service_account_json). Tag listing, digest lookups and pulls for a
matching host use it when no login is stored for that host. - Settings > Signature Verification now warns when cosign is not found at
the configured path inside the container (the Sentinel image does not
include it), and says what enforce and warn do without it. Approving an
update that the engine refused (signature check or pre-update hook) left
two failed rows in the history; it now leaves one. - Settings > System Settings has a History retention field (days of update
history and activity log to keep; successful updates and rollbacks are kept
for good; 0 keeps everything). A value saved there overrides
SENTINEL_HISTORY_RETENTION_DAYS, and the page says which one is in effect. - Settings shows whether Trivy is available, has a form for cloud registry
logins (ECR, ACR, GCR) and marks registries whose stored login was
rejected. - Signature Verification has Signer identity, Identity is a pattern and OIDC
issuer fields, shown in keyless mode, because keyless verification now
refuses unless the expected signer is set. - A container on another host now has a Rollback button on its page when
the hub knows what it ran before its last update. The confirm names the
host and the image it goes back to.
Changed
- Built with Go 1.27.2 (was 1.26.9). Two effects of Go 1.27: the macOS
binaries need macOS 13 or later, and Sentinel exits at startup if
GODEBUGsets one of the settings Go 1.27 removed to its old value
(tlsrsakex,tls10server,tls3des,tlsunsafeekm,x509keypairleaf,
asynctimerchan,x509sha1). Remove such a setting before upgrading.
Building from source still works with Go 1.26 or later, which fetches
go1.27.2 throughGOTOOLCHAIN=auto. - Docker-Sentinel is actively maintained again. The maintenance-mode notice
in the README andMAINTENANCE.mdare removed. - Sentinel now keeps a journal of every container it is replacing, so
that after a crash, a shutdown or a reboot it can finish or undo the
replacement exactly; each scan retries anything still unfinished. Give
the Sentinel container time to finish an update when it is stopped:
stop_grace_period: 2min compose, or--stop-timeout 120with
docker run(it waits up to two minutes for updates in flight).
Replacement containers carry asentinel.swaplabel so Sentinel can
recognise its own replacement after a crash; it is safe to ignore. - An update that was tried and rolled back (its new container would not
start or failed validation) is not tried again, under auto policy,
until a newer version or a different image digest appears, so a fixed
build pushed under the same tag or version is picked up. Manual
containers still see the update. remove_volumes(and thesentinel.remove-volumes=truelabel) now
works likedocker compose up -V: the updated container starts on
fresh, empty anonymous volumes and the old ones are deleted with the old
container once the new one passes validation. With it off (the default)
the new container keeps the old anonymous volumes and their data. Named
volumes and bind mounts are never touched.- A container that had no hostname of its own now gets a new one when it
is updated (its own short ID, as with docker compose) instead of keeping
the first container's ID for good. Apps that key data on the hostname
see it change once. - Compose
depends_ondependents are restarted after their dependency is
updated only when the entry hasrestart: true, as compose does. The
dependency graph still shows every edge. - The cluster wire format gains two optional fields: an error on the
container list an agent sends, and a list of server features
("cert-renewal", "keepalive") in the reply to an agent's state report.
Servers and agents on v2.16.x ignore them, so either side keeps working
with the other. - Agents keep their swap journal in their database (
SENTINEL_DB_PATH, in
the data directory), next to the settings of their web UI. - The cluster wire format gains optional fields: the images a container
ran before and the server's hooks with an update request, a host name in
an agent's state report, and remove volumes and image backup in the
settings the server sends. Servers and agents before v3.0.0 ignore them. - Enrolment tokens are longer: they end with
.<fingerprint>of the
server's CA. - An agent reported an update as done as soon as the new container
started, and removed the old one: an image that exits right after start
left the service down and "Restarting". Agents now validate a new
container as the server does for its own: they wait the grace period
(the server's, or the agent'sSENTINEL_GRACE_PERIOD, or the
container'ssentinel.grace-periodlabel), check that it is still
running and, with a healthcheck, healthy, and otherwise put the old
container back and report a rollback. The server then notifies the
failure and the rollback and applies its rollback policy to the
container, as for a local rollback. - An agent updated by "Auto-update agents" left no history row, so
Rollback of that agent's own container found nothing to go back to, and
a Rollback of a remote container from the dashboard left no row either.
The server now records the history of a remote update or rollback that
nothing else records (the agent's result, or why it got none). - Setting the rollback policy back to "No change" on the server never
reached agents, which kept showing the previous policy. - When signature verification refused a remote update because the image
could not be checked at all (enforce without cosign, or keyless without
an identity), the server sent "update started" and "update failed" for
that container on its own. Refused remote updates now join the one
notification a scan sends for all the updates it refused, each with a
failed history row, as local and Swarm ones do; one refused from the
dashboard sends no notification, as a local one does. - One precedence rule for image backup, remove volumes and the rollback
policy: a value saved on the Settings page overrides the environment
(SENTINEL_IMAGE_BACKUP,SENTINEL_REMOVE_VOLUMES,
SENTINEL_ROLLBACK_POLICY), at startup and as soon as it is saved; with
nothing saved, the environment value applies. A saved rollback policy of
"No change" also overrides the environment. Settings shows the value in
effect and whether it comes from the saved setting or the environment.
The UI rollback now applies the same policy as the engine. If saving the
Image backup or Remove volumes toggle fails, the toggle now flips back and
shows the server's error instead of reporting success. POST /api/bulk/policyacceptstargets, a list of{name, host}pairs
(empty host means this instance). Thecontainerslist of bare names
still works.- Ignore now works on updates for
:latestand other tags with no version
list. It previously failed with "digest-only update". It stores the
version the tag resolves to, or the new digest when none is known, so the
ignore covers that one update only. The queue and dashboard rows show the
resolved version, or a "new digest" label, instead of "build". - History showed
hostID::namefor a container on another host and linked
it to a page that returned 404. Rows now show the container's own name next
to the host badge and link to its page on that host. - "Update now" on a container on another host left no history row. The
result, success or failure, is now recorded. - The Cluster page warns on a host card when the same Docker engine is also
enrolled as another host, so double-counted containers have a visible
cause. - History and the activity log are no longer kept for ever: records older
than 90 days are pruned daily (at start-up too). Set
SENTINEL_HISTORY_RETENTION_DAYS, or thehistory_retention_days
setting, which wins over the variable; 0 keeps everything. Records of
applied updates and rollbacks are never pruned.
Removed
- The Portainer connector. Sentinel no longer scans or updates containers
through the Portainer API, and the Portainer pages, settings and
/api/portainer/*endpoints are gone. To keep monitoring those hosts, run
a Sentinel agent on each one (see Cluster Mode). Saved Portainer settings
stay in the database untouched, so downgrading restores them; queued
Portainer updates are dropped at startup, andSENTINEL_PORTAINER_URL/
SENTINEL_PORTAINER_TOKENare ignored with a warning.
Fixed
- The queue's "Detected" time was reset by every scan, so it showed the
last scan rather than when the update was first found. A row now keeps
its first time while the target (version or digest) stays the same,
for local, Swarm and remote containers. - A queued update stayed in the queue for good once its container was
filtered out of scans or pinned, because no scan looked at it again.
Such a row now leaves the queue, and the digest, at the next scan, for
containers and Swarm services alike. - The scan summary did not add up ("165 checked" against 162 in the
counts): Swarm task containers were counted in the total but in no
count, and remote containers whose registry check failed, whose update
was ignored or that a dry run passed over were in none either. Every
container and service a scan looks at now lands in exactly one count,
the total is their sum, and Swarm task containers are left to their
service. - A local container on a mutable tag such as
latestthat ran an older
image than its tag was queued without the versions remote containers
show, so ignoring it stored a digest. Its queue entry now carries the
current and target versions, resolved from the image it runs. - A rollback started from the dashboard or the API wrote nothing to the
update history and sent no notification. A successful one is now
recorded as a rollback with both images and notified. - When a new container failed validation (an image that exits straight
after start), the log and the notification said "validation failed:
" and the history kept only a rollback row without images. They
now give the reason (exit code, restart count or health status), the
history records the failed update with its old and new image, and the
rollback row names the images too. - When an update failed and the daemon would not remove the new
container, the service stayed down until a later recovery. The failed
container is now renamed aside (under a temporary name) with its
restart policy off, the old container gets its name back and starts at
once, and recovery removes the set-aside container later. Agents do the
same. - With image backup on, image cleanup deleted the backed-up image (taking
the backup tag with it) for mutable tags, while for versioned tags the
backups piled up for good. A backed-up image is now kept by cleanup, so
a rollback can use it, and only the newest three backups of each
repository are kept. - The history recorded an update as rolled back even when the rollback
itself failed, and a failed final step wrote a second, contradictory
row. A failed rollback is now recorded as failed, with the reason and
the rollback error, in one row. - When Sentinel was down through an interrupted update, recovery put the
old container back without a trace in the update history. Each
interrupted update, rollback or recreate that recovery resolves now
gets a history row saying what it did (finished it, restored or rebuilt
the old container, or kept the replacement). - Post-update hooks ran in the updated container just before Sentinel
replaced it to remove the maintenance label, so anything a hook did
inside the container (and a hook still running) was lost. They now run
in the container that stays, after that step. - Validation gave up on a healthcheck after a fixed 60 seconds and rolled
back containers that were still starting, and every stop Sentinel made
cut a container's own longer stop timeout to 30 or 10 seconds. The
health wait now follows the healthcheck (start period, interval,
timeout and retries, at least 60 seconds and at most 30 minutes), and a
container's stop timeout is used when it is longer than Sentinel's,
with the time to wait it out in full. If a stop still does not return,
the old container is started again and recovery watches it. - Maintenance windows: a weekly window over several days (such as
Fri 22:00-Mon 06:00) was closed on the days between, aSat 22:00-02:00window ran on the wrong night, and a window shorter than
the poll interval could be missed altogether because scans ran only at
their usual times. Weekly windows are now evaluated across days, and the
scheduler also wakes when a closed window opens. - Containers updated outside Sentinel, or removed, stayed in every digest
for good. A container found up to date now leaves the digest, and the
digest state of removed containers and services is pruned at each scan. - Compose sync turned an image from a registry with a port, such as
registry.local:5000/app:1.0, into an invalid reference. The tag is now
taken after the last path segment, digest-pinned images are left alone
and quotes are kept. - With a digest interval under a day, the digest fired every minute from
the day's last slot until midnight. The next slot is now always ahead. - A scan counted an update that a pre-update hook skipped (exit 75), or
whose pull found the image already running, as updated, and the scan
summary in the history printed the rate-limited count as "skipped".
Hook skips now count as skipped, unchanged images as up to date, and
the summary prints skipped and rate-limited counts separately. - A scan updated containers in the order the daemon listed them, so a
container could be recreated before the container it depends on
(sentinel.depends-on, composedepends_on, acontainer:network
mode) had its own update. Scans now update dependencies first. A
dependency cycle is broken in name order and logged as a warning. - Containers that an older Sentinel had updated kept the environment,
labels and command of the first image they ran, because that Sentinel
copied the whole config and only the current image's defaults were
left out on the next update. Sentinel now also leaves out values equal
to a default of any image the container ran before, found through its
update history (the last 10 updates that changed its image, same
repository only) and read from the host or, when the image is gone,
from its registry with the stored credentials. An image it cannot read
is skipped. - Updating a container could delete it. Sentinel stopped and removed the
old container before it knew the new one could be created, so a create
the daemon rejected (or a missing image) left the service gone when the
rollback failed the same way. Updates now swap containers: the old one
is renamed aside, the new one is created under the
real name while the old one still runs, the old one is stopped and the
new one started. The old container is removed only after the new one
passes validation; on any failure it gets its name back and is started
again, with its own image and volumes. A stop that fails while the old
container keeps running fails the update rather than starting a second
copy next to it, and the old container's restart policy is set tono
while it is stopped so a reboot cannot start both. - Updating or rolling back a container that shares another container's
network (network_mode: container:Xorservice:X, for example
qbittorrent behind gluetun) deleted it: the daemon rejected the create
because Sentinel sent back the provider's hostname and the image's
exposed ports. Those fields are now dropped for that network mode. - After a network provider such as gluetun was updated, rolled back or
restored after a failed update, the containers sharing its network were
left with no network while the update reported success. They are now
recreated on (or restarted with) the provider container. - Containers that keep their data in an image
VOLUMEor a bare
-v /pathcame back from an update on an empty volume (postgres re-ran
initdb) and the old data was orphaned, or deleted withremove_volumes
on. The new container now takes over the old anonymous volumes, as
docker compose does. - After an update the container kept the old image's environment, labels,
entrypoint, healthcheck and similar defaults, so the new image's values
never applied and the dashboard kept showing the old image version.
Values that came from the old image are now dropped so the new image's
apply; values you set yourself are kept, including an--entrypoint
that clears the image's command. One limit: an environment variable or
label you set to exactly the old image's value cannot be told apart from
the image's own and follows the new image. Rollbacks still restore the
snapshot exactly, and post-update hooks declared as image labels now
run. - A dashboard rollback stopped and deleted the running container before
checking that the old image was still there; with image cleanup on, a
rollback after a version update failed and left the container deleted.
After a:latestupdate the rollback recreated the new image and
reported success. Rollbacks now check the snapshot's image first (it
must be on the host, or its tag must pull back to the same image),
recreate exactly that image on the running container's current
volumes, and keep the running container if anything fails. A rollback
that would have to move a tag another running container uses refuses
and says which container. A failed dashboard rollback is now shown on
the dashboard and notified instead of only being logged. - A broken
:latestupdate was pulled, started, failed and rolled back
again on every scan, with a notification each time. It is now tried
once (see Changed). - A dashboard rollback after a later failed update attempt restored the
version already running, because every attempt replaced the rollback
snapshot. The snapshot is now saved only when an update succeeds. - Stopping Sentinel or rebooting the host during an update could leave
the container stopped, deleted, doubled or stuck with the maintenance
label. An interrupted update (including one started from the dashboard)
now puts the old container back, and at startup Sentinel waits briefly
for Docker, then finishes or undoes whatever the journal lists and
clears stale maintenance flags. A container is never left as two copies:
a failed restore is reported rather than rebuilt next to a container
that still exists. When Sentinel was killed while Docker was still
stopping the old container, Docker finished that stop after the old
container was put back and nothing started it again; Sentinel now
watches a container it put back until that stop has timed out, starts
it again if it stops, and notes this in the update history. - Containers started with
--rmwere deleted by an update, because the
daemon removed them on stop and the update then gave up. They are now
replaced like any other container. - Containers that depend on an updated container (
sentinel.depends-on
or composedepends_on) were never restarted after it, whatever
SENTINEL_DEPSsaid. They are now restarted, composedepends_on
entries are matched to their service in the same project (every
container of a scaled service), and a dependent that is being updated
itself is left alone. - Containers on
:latestor another mutable tag such as:alpinewere
recreated pinned to the version the tag pointed at (nginx:latest
becamenginx:1.29.2), so they quietly stopped following the tag.
Updates to a mutable tag now re-pull the same tag. The version the tag
resolves to is still shown for information. - A container on a stable tag (
gitea:1.24,app:1.2.3) was offered a
release candidate such as1.25.0-rc0as its update, and on the auto
policy was moved onto it. Pre-release tags (rc, beta, alpha, dev and
similar) are now only offered to a container that is itself on a
pre-release. - Pre-release tags were ordered as text, so
rc10counted as older than
rc9. They are now compared by number. - Containers pinned to a build-counter tag such as
4.3.3-ls190or
7.2.4-debian-12-r9were shown as up to date forever, because the
trailing counter made every tag its own variant. Trailinglsandr
counters are now ignored when matching variants and used to order tags
within one. - When two containers ran the same mutable tag and one was updated, the
other was reported up to date, and from the next scan Docker reported
its image as an ID, so it was skipped as a local image for good. Pull-only
mode dropped every container the same way after its first pull. The scan
now follows the image a container was created with, reports a container
that runs an older image than its tag as having an update, and pull-only
no longer pulls a tag it has already pulled. - Swarm services on a mutable tag never rolled out a digest-only update:
the old digest pin was resubmitted, so Sentinel reported a false success
or waited ten minutes and reported a timeout, on every scan. The update
now drops the old pin, re-pins to the digest the registry serves and
forces a rollout, and the rollout check ignores a status left over from
an earlier rollout. - Self-update on a hub or standalone install never took effect, because
the old Sentinel container was left running: the replacement either could
not open the locked database and crash-looped, or never started. The old
container is now stopped once the replacement is running, a replacement
that cannot start is removed and the old container gets its name back
(with a hint when published ports are the cause), a failed pull no longer
blocks an update to an image already on the host, and leftover exited
-old-<timestamp>containers are removed at startup. The replacement
waits up to a minute for the database lock instead of exiting after five
seconds, and if the old container cannot be stopped the replacement is
removed and the old one gets its name back. - A Swarm digest-only update now fails with a clear error, leaving the
service unchanged, when the new digest cannot be looked up (private
registry or outage), instead of resubmitting it without its digest pin. - An ignored update on
:latestor another mutable tag was notified and
auto-applied again on the next scan. It now stays ignored until the tag
moves to a new version or digest. - A pull-only container that was already pulled but still behind its tag
showed as an update and counted as automatic on every scan. It is now
skipped. - Version checks failed against registries that answer with a Bearer
challenge (lscr.io, registry.gitlab.com). Sentinel now follows the
challenge to the registry's token endpoint. - Images with many tags (linuxserver/* on GHCR) lost their newest versions
because tag listing stopped after 10,000 tags, and registries with pages
under 1,000 tags were read only to the first page. Listing now follows the
Linkheader and stops after 100,000 tags. - An expired or revoked Docker Hub or GHCR token made every version check
for that registry stop quietly. The rejection is logged as a warning and
the token request is retried anonymously. - Registry failures (rate limit, outage) for official Docker Hub images such
asnginxwere logged at debug level and counted as local images. They
now record a failed check. Swarm service lookups report their failure the
same way. - A container on a floating tag such as
nginx:1.27was offered the pinned
nginx:1.29.2instead ofnginx:1.29, which stopped it floating. A
floating tag now gets the tag of its own precision for a version line
first, when the registry publishes one. - The scanner settings (mode, threshold, Trivy path) only applied at startup,
and in the stock image, which has no Trivy, "Pre-update" scanning checked
nothing and said nothing. The saved settings are now read when each update
runs, Trivy is looked for again at the saved path, and a pre-update scan
that cannot run is logged as a warning. - The new Sentinel instance could delete the old container within seconds
of starting, while the self-update helper was still deciding whether to
keep the new one. If the new version then crashed, nothing was left to
restore. The helper now marks the swap as in progress, removes the old
container itself on success, and startup cleanup leaves old containers
alone while a helper is active or the swap is in progress, and otherwise
only removes one whose replacement is running and healthy. - The self-update helper ran the target image, so rolling an agent back to
a v2.16 image (or updating to any image without the helper subcommand)
started a full Sentinel instead and the rollback never happened. The
helper now runs the image the running instance uses. - An agent told the hub its self-update succeeded when the helper started.
If the swap was rolled back, the agent that comes back now sends a failed
result for it, so the hub's history and notifications show what happened. - A network error or a 5xx from a registry's token endpoint marked a valid
login as rejected on the settings page. Only a 401 or 403 does, and the
mark clears when the login works again. - Starting a self-update no longer force-removes a container that merely
has the helper's name, and the helper ignores SIGTERM until the swap is
settled. The hub now sends agents cloud (ECR, ACR, GCR) registry logins
with updates as well as stored ones. - A host restart between the self-update helper stopping the old Sentinel
and starting the new one left no Sentinel running. The helper now sets the
old container's restart policy toalwaysfor the swap (putting the
original back on rollback), so the old instance returns with the daemon;
finding itself parked as-old-<timestamp>with a replacement that never
started, it removes the replacement, takes its name and restart policy
back, and an agent tells the hub. - A self-update helper that was created but never started no longer blocks
the cleanup of old containers for ever; after 15 minutes it is removed. - A self-update's outcome was never reported on the instance that came back:
the helper writes it about twenty seconds after the new instance starts, so
the new instance saw "in progress" at boot and logged nothing, and after a
rollback the old agent never told the hub. The instance now waits in the
background (up to two minutes) for the final outcome and reports it once;
an agent sends a failure to the hub at once instead of at the next
reconnect. - If the helper was killed during the settle period and the daemon then
restarted, the old and the new Sentinel both came back and the new one
crash-looped on the database lock. The replacement now has no restart
policy until the swap is committed (it gets its real policy then, and the
old container is set not to return), and an old instance that finds a
running or crash-looping replacement next to it with its own unfinished
marker and no helper alive removes it and takes its name back. - Startup cleanup on an agent removed another Sentinel's
-old-leftover on
the same host; it now touches only containers named after itself. After an
upgrade from a pre-v3 agent the old container lingered until the next
restart; the cleanup now runs again two minutes after start. - With two Sentinel containers on one host, a self-update picked the first
container labelledsentinel.self, swapped that one, and the cleanup that
followed removed it entirely. A Sentinel now finds the container it runs
in by its own container ID, for the scheduled self-update, the dashboard's
self-update and update-to-version, and an agent's self-update; if it
cannot tell which container it is, it refuses and changes nothing. An agent
asked to update a Sentinel container updates its own, whatever name the hub
sent. - The self-update no longer removes anything on the strength of the helper
having exited: it follows the helper's outcome marker. An unfinished or
unknown outcome leaves both containers as they are and logs it loudly. - A Sentinel finds its own container by the ID in
/proc/self/mountinfo, then
in/proc/self/cgroup(cgroup v1docker/<id>, v2docker-<id>.scope),
before falling back to the hostname. If none of these works, for example
with a custom hostname on a runtime that hides both, set the new
environment variableSENTINEL_SELF_CONTAINERto this container's name or
ID. It is used only when the ID cannot be read from the system, and is
checked against the Docker API: the container must exist and carry the
sentinel.self=truelabel, or the self-update is refused with an error
that names the variable. - When the signature gate refused Sentinel's own self-update (enforce mode
without cosign, or an image that fails the check), it sent no notification
and left no history row, only a log line. A refused self-update now records
a failed update in the history and sends one failure notification. - Cluster agents dropped out of the cluster for good a year after they
enrolled: an agent certificate lasts 365 days and nothing renewed it, so
the only way back was a re-enrolment that gave the host a new ID. Once
less than a third of its certificate's life is left, an agent now asks
the server for a new one over its live connection and switches to it.
The server keeps the old certificate valid until the agent connects with
the new one, and never drops the reply. Agents ask only a server that
says it renews safely, so a v3.0.0 agent keeps its working certificate
(and warns) when its server is older. The server also renews its own
certificate before it expires, so a server left running for a year
keeps accepting agents. Agents older than v3.0.0 do not renew; update
them before their first year is up (with "Auto-update agents" on, the
server does this for you). - An agent whose clock ran more than an hour behind the server could not
connect after a server restart, and was then told to re-enrol, which
would not have helped. Certificates are now valid from 24 hours back,
and an expired or not-yet-valid certificate gets its own explanation
in the agent log, pointing at the clocks where that is the cause. - Turning Cluster Mode off while any agent was connected left the settings
request hanging for ever, cluster mode could not be turned back on until
a restart, and every shutdown of the server ran into Docker's stop
timeout and was killed. The server now ends the agents' connections
itself when it stops; agents go back to reconnecting. - When a connected agent could not list its containers (its Docker daemon
restarting, a socket proxy down), it sent no reply and the server's
scheduled scan waited on that host for ever, so no host was updated
again until a restart. The agent now always replies, with the error, and
every call the server makes to an agent has a time limit. A server
older than v3.0.0 shows such a host with no containers until its next
successful list. - A host with many containers or large labels sends a container list over
gRPC's 4 MB default; the server refused it, so the host dropped out of
the cluster. The limit is now 64 MB, and the server log names a host
that goes over it. - A network partition took about a minute and a half to notice. Both ends
now send keepalive pings on a quiet connection and drop it after 40
seconds without an answer. An agent pings that often only a server that
says it accepts it; with an older server it pings every five minutes,
which gRPC's default policy allows. - A remote update whose new container could not be created or started (a
bad mount, a port clash, a broken image) deleted the container. The agent
now builds the replacement while the old container is parked under a
temporary name, removes the old one only after the new one has
started, and puts the old one back on any failure. A--rmcontainer is
recreated from its old configuration and image. - Remote updates could leave work for the user: a stopped
parked container from an interrupted update blocked later updates
(it is now removed when the container under the real name has run;
otherwise the update still stops and names it), a replacement that
could not be removed kept the container's name (it is now moved aside
under a temporary name), a failed removal of the old container was
reported as plain success, and parked containers showed on the
dashboard as containers of their own while their update ran. A parked
container left behind with no update running is still listed, under
its own name, since it may be the only copy. - A stopped remote container that was started during its update could be
force-removed while running. The agent now checks the container's state
once it has parked it and only removes a container it stopped itself. - When the server retried a slow remote update, the agent could run two
recreates of the same container at once and leave the live container
parked. A second update of the same container is now refused while the
first runs, and a result that arrives after the server stopped waiting
is saved to the history. - Containers on several networks came back from a remote update with
their static IP, aliases and MAC address on one network only, chosen at
random. Every network now keeps its settings. - Remote updates now build the replacement container exactly as local
updates do. Before, an updated container on an agent host kept the old
image's command, environment and labels (a new release's defaults never
applied), kept the old container's short ID as its hostname, and
collected old IDs in its network aliases. Anonymous volumes (an image
VOLUME or a bare-v /path) were replaced by new, empty ones on every
update, so a database without a named volume came back empty and its
data was left in a detached volume; they are now carried over and kept
when the old container is removed. A container sharing another's
network stack (network_mode: container:X) could not be updated at all. - Updating a container on an agent host that other containers share the
network of (network_mode: container:<it>, composeservice:<it>) cut
those containers off: they were left with only loopback, still pointing
at the removed container, and could not even be restarted. As on the
server, a consumer that pointed at the old container is now recreated
on the new one, and one that names it is restarted; the same happens
when a failed update brings the old container back. - On an agent host,
sentinel.remove-volumes=trueon a container, or
SENTINEL_REMOVE_VOLUMES=trueon the agent itself, now starts the
updated container on fresh anonymous volumes and removes the old ones
with the old container, as on the server. The server's own Remove
volumes setting does not reach agents yet. - Same-tag updates (a new image behind the tag a remote container already
uses) always failed on the agent with "invalid reference format", both
when approved and when applied by the scheduled scan. - Remote updates started containers that had been stopped, or one-shot
jobs that had exited. A stopped container now stays stopped. - Queued updates for a remote host were deleted whenever the host was not
connected at scan time, for example just after a server restart. They
are now kept while the host is still in the cluster, and pruned once it
lists without them, once it is removed, or when cluster mode is off.
Approving a remote update whose host is gone (or with cluster mode off)
is refused instead of being run against the local Docker host. - A queued update for a remote Sentinel agent could never be applied: its
row offered "Update Sentinel", which updated the server instead, and
approving it was refused. The row now sends the update to that agent.
Restart, stop, start, update, rollback, policy and GHCR actions also
protect only the server's own Sentinel container now, not any remote
container with the same name. - Remote update history was stored under three different keys, so
History showed a raw host ID for scheduled updates and Rollback found
no history after an approved update. All remote history now carries the
host; rows saved before this release are moved to the same key when the
cluster starts. A rollback that finished after the server stopped
waiting is saved as a rollback, so the next Rollback does not undo it. - After an agent host crashed or lost its network and reconnected quickly,
the dashboard showed the host offline while it was working, and remote
actions in flight at that moment failed with "agent disconnected". - A removed or revoked cluster host could reappear after a restart, when a
save that was already under way wrote it back to the database. One
slow save also no longer holds up every other host. - "Auto-update agents" never updated an agent: it asked for an image tag
such as:v2.16.1, but releases publish:2.16.1. - Enrolling the same Docker daemon twice gave two hosts with the same
containers and racing approvals, unnoticed. The server now logs a
warning naming both hosts (it does not remove either). - An agent whose certificate the server did not recognise (for example a
server restored from a backup taken before the agent renewed) logged
only a bare error. It now explains the cause and that re-enrolling is
the fix. - An agent killed part way through an update (a restart, a crash, a host
reboot) could leave the container stopped, or renamed
aside, with nothing to bring it back. Agents now run
every update through the same journalled swap as the server's own
containers, with the journal in the agent's database: an interrupted
update is finished or undone when the agent starts again, and retried
every minute until it is resolved. Until then the agent refuses other
updates of that container. One it cannot resolve on its own is
discarded from the server, once the containers are right on the host,
withDELETE /api/swaps/<name>?host=<hostID>(agents before v3.0.0
cannot, and the server says so). After an update, an agent also restarts the
container's dependents when dependency awareness is on, as the server
does. - Signature verification (enforce, warn, or a container's
sentinel.verify
label) applied only to the server's own containers: an update dispatched
to an agent went ahead unchecked. The server now checks the image before
it asks an agent to update, with the same rules, whether the update
comes from a scan, the dashboard or the API. A rollback is not checked. - A container on an agent that an older Sentinel had updated kept the
environment, labels and command of the first image it ran, through every
later update. The server now sends the images a container ran before
(from its update history) with each update, and the agent leaves out
whatever came from any of them that it can read, so the new container
takes the new image's defaults. An earlier image no longer on the agent's
host is fetched from its registry (with the credentials the server sends
for the update); one that cannot be fetched is skipped. - Lifecycle hooks never ran for containers on agents: neither the
sentinel.hook.*labels nor the hooks saved on the server for a remote
container. They now run around a remote update exactly as around a local
one: the hooks the server stores for that container win over its labels,
the pre-update hook runs in the old container before anything changes
(exit 75 skips the update, which is then counted as skipped and leaves
no history row; under a server before v3.0.0 the agent reports the skip
as a success with a note, as that server would count it as a failure;
any other failure aborts it), and the post-update hook
runs in the new container (a failure is reported with the result). They
follow the hooks setting, which the server now sends to its agents. A
rollback runs no hooks. - Hooks saved for a remote container were also listed for a local
container whose name equals that host's ID. - Agents never received the server's settings, so remote updates ignored
image cleanup, image backup, lifecycle hooks and dependency awareness as
set on the server. The server now sends them
when an agent connects and again within a heartbeat (30 seconds) of a
change, and agents keep the last ones received for when the server is
away (and follow them from the start, including when they finish an
interrupted update). Rule: a setting given in an agent's own environment
(SENTINEL_REMOVE_VOLUMES,SENTINEL_IMAGE_CLEANUP,
SENTINEL_IMAGE_BACKUP,SENTINEL_HOOKS,SENTINEL_DEPS) wins on that
agent's host; the cluster settings page says so. Remove volumes is the
exception: agents before v3.0.0 never removed volumes, so the server
sends it only when the new cluster setting "Remove volumes on agents
too" is on (off by default). Check it before turning it on: an agent's
containers then start on new anonymous volumes and the old ones are
deleted. Update delay, rollback
policy and the update policy itself stay with the server, which decides
when a remote update runs. - Remote updates left the old image on the agent's host. Agents now remove
it after an update when image cleanup is on, as the server does, and
tag it as a backup first when image backup is on. SENTINEL_CLUSTER_PORTwas ignored: the cluster listener used only the
port saved in the settings (saved with every change to the cluster
settings) or 9443. Set in the environment, it now wins, as
SENTINEL_CLUSTER_ENABLEDdoes.- An agent started its web UI even with
SENTINEL_WEB_ENABLED=false. - Stopping an agent logged an error ("context canceled") and exited with
status 1. A stopping agent now waits up to 90 seconds for commands it is
running (an update half way through) before it closes its database, and
exits cleanly. - One enrolment token could enrol more than one agent when they used it
at the same moment, and a refused second use left nothing in the
server's log. The token is now checked and used in one step, and every
refused token (used, expired, not matching) is logged with the host name
and address that presented it. - An agent restarted with a new
SENTINEL_HOST_NAMEkept its old name in
the cluster. Agents now send their name with their state report and the
server takes it. - Agent versions were never stored, so the cluster page and the hosts API
showed none, and after a server restart "Auto-update agents" waited for
the scan after next: the version came only with an agent's first
heartbeat. The version in an agent's state report is now stored with the
host and used at once. - An image pushed to more than one repository showed a same-tag update
that was not there, again and again: Sentinel compared the digest of
whichever repository Docker listed first with the registry of the
container's own. It now uses the digest of the container's repository
(agents and the server alike). - An update that finished on an agent while its connection to the server
was down was lost from the history: its result could not be sent, and
the offline journal meant to carry it was never written. The agent now
keeps such a result and sends it when it reconnects; the server records
it in the history, in place of the "agent disconnected" failure recorded
when the connection dropped (a late result of an update the server had
stopped waiting for also replaces that failure, instead of adding a
second row; and if the result arrives first, the failure is not written
over it). - A scan that ran while a remote update was in progress saw the old image
and queued (or started) the update again, leaving a queue entry for an
update that had already happened. A container whose update the server
is waiting for is now left to it. - A queued remote update stayed in the queue after its container became
up to date, or was filtered out or pinned. The scan now removes it, as
it does for a local container, and counts up-to-date remote containers. - Remote containers did not follow the rules local containers do. A scan
now applies, to a container on an agent: the update delay (remote auto
updates with a delay never ran), the maintenance window and the
sentinel.schedulelabel, pull-only (global orsentinel.pull-only:
the agent pulls the image, with the server's registry credentials, and
reports how it went, and the container is left alone; remote containers
were recreated. An agent before v3.0.0 is asked to pull but cannot say
how it went, so nothing is recorded), and "Ignore version" (saved for the remote
container instead of a local container of the same name). - Remote updates were silent: notifications are now sent when an update
of a remote container is available, starts, succeeds or fails, named
"container on host". A failed remote auto update is in the history with
its error (it was missing, or recorded without the error), and the scan
no longer sends a second, host-less event that refreshed a local row. - Pausing a host did nothing, the cluster page's Drain button called an
endpoint that does not exist, and a paused host could not be resumed. A
paused host now gets no new updates from any path (a rollback still
goes through), the button pauses it, and a Resume button ends the pause. - Turning cluster mode off waited behind any remote update, action or log
fetch in progress, and every cluster page request waited behind that. - "Check for updates" on a remote container compared the server's own
copy of the image, which it usually does not have (an error), instead of
the digest the agent reported, and a new image behind the same tag was
reported as "up to date (newer versions ignored)" and not queued. Both
now behave as the scan does. - The dashboard showed remote containers under the server's default
policy, while the scan used the cluster's default remote policy and the
:latestrule; the cluster settings showed "manual" as the remote
default when none was saved. They now show the policy the scan applies. - After an approved or "update to version" remote update, the dashboard
row stayed on "Updating" until the page was reloaded. - When the cluster CA could not be read, the server replaced it with a new
one without a word, locking out every agent. It still creates a new one
(the cluster cannot run without), but keeps the old files aside and logs
how to restore them. - An agent whose enrolment was cut short after the key was written could
not start ("read host id"). The host ID is now written before the key,
and an agent without the file takes it from its certificate. - An enrolment whose answer never reached the agent used up the token, so
the agent could not enrol again without a new one. For 10 minutes after
a token is used, the same agent (the same key, which it now keeps until
enrolment finishes) may enrol with it again and gets the same host; the
server logs the retry. Any other agent is refused as before. - Autonomous mode (while the server is unreachable) was stopped before
every reconnect attempt and started again after it, logging "entering
autonomous mode" every 30 seconds and never reaching its own poll. It now
runs until the agent connects again, and counts stopped containers too. - The server's view of an agent's containers went stale between scans (6
hours by default): a container stopped or started by hand kept its old
state on the dashboard. Agents now send their container list every 5
minutes, and two lists sent close together can no longer arrive out of
order and leave the older one in place. - Remote updates started from the dashboard or the API (approve, "Update
now", update to version) now send the same notifications as local ones:
started, then succeeded or failed. - A Rollback of a remote container from the dashboard or the API sent no
notification. It now sends "rollback succeeded" or "rollback failed" for
"name on host", as a local rollback does, and records one rollback row
from the image the container ran to the one it went back to. - An agent's update of itself was recorded as a success, and notified as
one, as soon as its helper took over, before the swap; a self-update
that was then rolled back added a failed row and no notification. The
server now waits for the outcome, which the agent that comes back (the
new version, or the old one put back) reports, and records one row and
sends one notification with it. After 10 minutes without an outcome it
records the update as failed: "the agent did not come back on the new
version". Servers before v3.0.0 are told "success" as before. - After an agent updated itself, the dashboard and the queue kept its
parked old container and its self-update helper, already gone from the
host, until the next scan (up to 10 minutes). The server now asks the
agent for its containers as soon as the outcome is known, and no longer
shows or queues self-update helpers. - Remote rollbacks were recorded as updates (outcome success, type
rollback), so History showed them as Updated and its Rollback filter
left them out. They are now recorded as local ones are: rolled back, or
failed with "rollback failed: ". Rows from before v3.0.0 still
count when Rollback looks for the update to undo. - A request to update another Sentinel container on an agent's host
updates the agent's own container, but the history row named the
container requested, which did not change. It now names the container
that changed, from the image it ran, and so do its "update started" and
outcome notifications (agents now say which container they run in). - When an agent rolled back an update because the new container did not
stay up, the reason was only "the new container did not stay up". It now
says why, as a local update does: the exit code, the restart count, or
the healthcheck's output. - A remote update whose agent dropped its connection failed with "agent
37770888... disconnected". Errors from a request to an agent now name
its host, and the live events for an agent connecting or disconnecting
give the host's name, not its ID, as the host name. - Saving Settings > Notifications overwrote a webhook channel's real header
(for exampleAuthorization: Bearer ...) with its masked form, so the
channel started failing with 401. Masked values, including those inside
webhook headers, now keep the stored secret. - Importing a configuration exported without secrets replaced every
notification channel's real token with a masked value and still reported
success. The import now keeps the stored secret for channels that already
exist, and skips (and reports) imported channels that only hold masked
values. - Turning on 2FA showed eight 64-character hashes as recovery codes, and none
of them could sign in. Confirming 2FA now generates the recovery codes,
stores their hashes and shows the plain codes once. If you turned on 2FA
before this release, your codes are unusable: turn 2FA off and on again
to get working ones. - A wrong password at login reloaded the login page with no message, a wrong
2FA code dropped you back to the username step, and a wrong current
password on the account page navigated away. Sentinel now redirects to the
login page only when the session is gone. - With authentication disabled, saving Web Port, TLS mode, Log Format or
Self-update mode, or switching role, always showed "Failed to save". - In cluster mode, ticking an agent's container in Manage mode and choosing
Restart, Stop or Start acted on the hub's container with the same name.
Bulk policy changes had the same problem. Selections are now keyed by host
and name, the confirm dialogs show the host, and a ticked row stays ticked
only on its own host. - Settings showed Remove volumes and Image backup as Off while the engine
had them On from the environment, and the UI rollback ignored
SENTINEL_ROLLBACK_POLICY. - The Sentinel container's own row shows its real state instead of always
"Running". Fonts are now bundled and served by Sentinel, because the
page's CSP blocked Google Fonts. The Cluster page's Server card no longer
overflows on a long version string. - Failed stop, start and restart now show an error, settings toggles no longer
report success when the save failed, History rows open their details, and
a remote container's notification setting is kept under its host. - After the first connection, changing the NPM URL or login, or switching NPM
off, changed nothing until a restart, and Test connection reported success
against the old target. Test now rebuilds from the saved settings and the
old connection is dropped. - The 2FA QR code renders again (its library was blocked by the page's CSP),
escapeHTMLescapes quotes, and a refused image removal is counted as
failed in Images bulk Remove. - A channel's Test button now tests what is on screen (a channel just added or
edited but not saved too); it used to test the saved copy or fail with
"channel not found". - Several small dashboard and settings fixes: adding or removing a
notification channel, registry login or release source no longer wipes what
was typed on the others; a failed stop, start, restart or update clears its
spinner; failed policy and cluster changes are put back as saved; the
Cluster page no longer reloads away a new enrolment token; in cluster mode
collapsing a stack no longer changes the stat cards and a stack's
select-all works; the queue's Enter key opens the focused row; All Registry
Tags shows errors properly. - A manual Check for updates on a local container reported a new image
behind the same tag (for example :latest) as "up to date (newer versions
ignored)" and did not queue it. It is now queued, as a scan does. - Saving other OIDC settings no longer pins "Link existing accounts by
username" overSENTINEL_OIDC_LINK_EXISTING: the toggle is stored only
when you change it. The opt-in also skips accounts with 2FA or a passkey. - NPM Test connection uses the same settings as startup (the environment,
then saved settings), and the NPM connection built at startup stops
syncing when it is replaced or switched off. - An abandoned "Link single sign-on" no longer breaks the next SSO sign-in,
and only messages that begin with an action failure show as errors. - "Update to version" and the GHCR switch on a container on another host
left no history row (the notification still went out). Both now record
success or failure under the host-scoped key, like Update now. - Approving a local update that failed no longer risks a second failed row
in the history: local updates record their own history in the engine,
and only remote updates are recorded by the web layer. - Settings > Scanning showed the poll interval and grace period as a blank
"Custom..." and Show stopped as Off while the environment set them, so
saving there could overwrite the real values. Every setting the
environment can set now shows the value in effect and whether it is
saved or from the environment, and an untouched Apply or Save sends
nothing, so it can no longer turn an environment value into a saved
override. Toggles the environment can also set say when they override it. - The dashboard, pending-updates and history pages fit a phone screen:
container names get the free width and end in an ellipsis with the full
name as a tooltip, the Approve, Ignore and Reject buttons are always in
view, times no longer overlap names, outcome badges are whole, and scan
summary rows keep their Summary badge. A "self" badge no longer makes a
name that fits end in an ellipsis. - Cluster host cards showed an agent's version with a doubled "v" and let
the commit hash overflow the card; the version is shown once and the full
text is in a tooltip. - The page of a container on another host showed a blank Container ID and
did not say which host it is on; it now shows both. - The service page's Policy and Notification Preferences sections were
unstyled and named their options differently from the container page;
they now match it. A Swarm task on a registry with a port showed its image
tag cut short. - The Logs page showed raw host IDs and one line named a user by ID.
Activity-log lines and failure messages now name the host, and the SSO
unlink line names the user. - The history's scan summary counts did not add up: containers whose
registry check failed, ones skipped for a swap in progress, ones whose
newer versions were all ignored, rolled back before, or held back by a dry
run were in no count. Each now lands in exactly one, and "check failed" is
shown as its own figure. - The queue page kept a row for an entry removed elsewhere (approved from
another client, the API, an auto-update or a scan finding it up to date)
until you reloaded; it now refreshes on any queue change. - The container and service pages now show the pending-updates count in
the navigation, like the other pages. - Rollback is offered, and allowed, only when there is something to go back
to: after a rollback the button is hidden (or offers the update before
it), including for containers rolled back before v3.0.0, and the local
and remote rollback refuse with "nothing to roll back to: the container
already runs" instead of recreating the container on the same
image. The Snapshots section's Rollback button follows the same rule. - A failed notification was recorded as delivered whenever the built-in
log record succeeded, so if the only real channel (Gotify, say) failed
on the scan that first saw an update, Sentinel never tried again and you
were never alerted. Only real channels now decide whether an update was
notified about. Batched notifications still count as sent once queued;
a failed flush is logged. - Batched notifications could be postponed for ever: the batch window
restarted on every new event, so a steady trickle of failures never
flushed. The window now runs from the first queued event. The first
retry delay was also unbounded; it is now capped at 30 seconds like
the later ones. - The daily digest and batched summaries named one container (the digest
named only the alphabetically first one, a batch said "3 containers")
on every provider but webhook. They now list every container, and MQTT
payloads carry acontainer_nameslist. - Telegram rejected failure messages whose error text held an unmatched
_or*(a Swarm service calledstack_web, for example), so the
alert never arrived. The error now goes in a code span. Combined
failure errors are capped at 1000 characters, Discord fields at 1024
and Pushover messages at 1024, which those services reject otherwise. - Custom notification templates saved in Settings validated and previewed
but every real notification used the built-in text. Templates are now
loaded at start-up and after each save or reset, and rendered by every
provider. A template that fails to run falls back to the built-in text.
Telegram shows a custom template as plain code text.{{.ContainerNames}}
is available for digests and batched summaries. - An SMTP server that accepted the connection and then never answered
(port 465 with TLS left off, for example) blocked the scan for well past
the advertised 30 seconds. The socket now carries the deadline. - The per-container "Notify on every scan" mode behaved like the default
and went quiet after the first alert. It now alerts on every scan that
finds the update, and those containers are left out of the scheduled
digest, as the Settings text says. - With
SENTINEL_WEB_ENABLED=falsethe daily digest never ran (so
containers in digest-only mode produced no notifications at all), a
fresh install still served the setup wizard on the web port, and after
it finished no scan ever started. The digest now runs headless, a fresh
headless install skips the wizard and takes theSENTINEL_MODErole
(server if unset), and scans start straight away. - The Home Assistant discovery switch in Settings only took effect after
a restart, and a sensor that had gone ON stayed ON after its update was
applied or dismissed. Discovery now connects and disconnects when the
setting changes and sets sensors back to OFF when a container leaves the
queue. After a restart Sentinel does not know which sensors it left ON,
so a stale ON clears only the next time that container is pending. - Home Assistant discovery gave containers whose names differ only in
-,.or_the same topic and unique ID, so one hid the other. Names
with such characters now carry a short suffix. Their Home Assistant
entities are created again under the new ID once, and the old entities
can be deleted in Home Assistant. - History "Load more" repeated rows (time zones east of UTC) or skipped
hours of records (west of UTC) from the third page on whenTZwas not
UTC. The cursor is now normalised to UTC before the lookup. - Deleting a user left their passkeys behind, so the login page kept
offering passkey sign-in with nothing that could use it. Their
credentials and indexes are now removed with the user. - Notification channels migrated from the pre-2.x single-config format got
a new random ID on every read, so the first save from the UI stored the
masked Gotify token as the real one. They keep fixed IDs now. - Environment values that were set but could not be parsed fell back to
the default without a word:SENTINEL_POLL_INTERVAL=86400became six
hours andSENTINEL_WEB_ENABLED=maybestayed on. Sentinel still starts
with the default, but now logs an error at start-up naming the variable,
the value it could not read and the default in use (values of tokens,
passwords, secrets, keys and URLs with credentials are never logged).
yes,no,onandoffare accepted as booleans. - A headless agent (
SENTINEL_WEB_ENABLED=falsewith
SENTINEL_ENROLL_TOKENandSENTINEL_SERVER_ADDR) became a standalone
server and never joined its hub. Those variables now select the agent
role on a fresh start; an explicitSENTINEL_MODEstill wins. - If the tag moved to a new digest while every notification channel was
down, the update was never reported: the earlier notification time was
kept with the new digest, so the next scan thought it was announced.
It is now retried until it is delivered. - History retention also pruned
partialupdate records, which are
applied updates. They are kept like successful updates. - Telegram rejected a failure whose error was over about 3,900
characters, because the cut left its code span open. The error is now
trimmed inside the span. - Discord ignored custom notification templates. A saved template now
fills the embed description. - Saving the Home Assistant discovery settings twice in quick succession
(on, then off) could leave discovery connected, and each save waited on
the broker connection. Saves now hand the change to one background
worker, so the last save always decides the final state and the page
never waits on the broker. - When a pre-update hook exited 75 to skip an update, "update started"
had already gone out and nothing followed, locally or on a cluster
host. A new "Update Skipped" notification now closes it, naming the
container (and host) and the reason. It is on for channels that receive
all events and can be switched per channel under the channel's event
list; a channel that already has a saved event list needs "Update
Skipped" ticked to get it. It also appears in the template editor. - The scheduled digest listed far more containers than were queued (173
against 88 in a test cluster): every remote update appeared twice, once
by bare name, states of removed containers and hosts were never cleared,
and remote names showed raw host IDs. Each remote update is now listed
once as "name on host", and a scan clears the stored state of remote
containers that are gone, whose host left the cluster, or that were
updated outside Sentinel (a host that is only offline keeps its own). - "Update available" notifications carried no new image, so a custom
template's{{.NewImage}}rendered empty. It is now the target image
(the newer tag, or the same tag for a digest-only update), for local,
Swarm and remote updates. - An update you ignored left the queue but stayed in the scheduled
digest, so the digest could list one more than the queue held. Ignoring
an update (from the queue, or when a scan finds every newer version
already ignored, locally or on a cluster host) and dropping a stale
remote queue row now also clear its stored notification state.