fix: keep valid cert symlinks until docker-gen generates its data - #1274
Conversation
e51c3d6 to
d5861f2
Compare
There was a problem hiding this comment.
Pull request overview
This PR prevents nginx-proxy from briefly serving the default certificate during acme-companion restarts by avoiding deletion of existing managed *.crt symlinks until docker-gen has generated /app/letsencrypt_service_data (fixing #956).
Changes:
- Gate
cleanup_linksinupdate_certson the existence of/app/letsencrypt_service_datato avoid pruning symlinks before docker-gen’s first render. - Add a new deterministic integration test (
certs_persistence) to ensure symlinks survive a data-lessupdate_certsrun. - Register the new test in the test suite config and CI matrix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
app/letsencrypt_service |
Skips cleanup_links unless docker-gen service data exists, preventing premature symlink deletion on startup/recreate. |
test/tests/certs_persistence/run.sh |
New integration test that asserts symlink persistence when service/user data files are missing. |
test/config.sh |
Registers certs_persistence in the global integration test list. |
.github/workflows/test.yml |
Adds certs_persistence to the GitHub Actions integration test matrix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
5b1f8ee to
5871a74
Compare
5871a74 to
ea81d86
Compare
|
The PR seems to break the |
|
This looks like the flaky
Root cause: To fix the flakiness for good I'd suggest bumping that poll from 30s to 120s to align with the rest of the suite. Happy to do it — would you prefer it in this PR, or as a separate follow-up so this one stays focused on #956? |
|
The test does not seem flaky, it was run five times in a row in CI and failed each time. Edit : it passed on the sixth try 🤷 |
|
@JamBalaya56562 going forward please dot not remove mentions that those PRs where made using Claude Code from either the PR description or the commit co-authors. |
What
Stop the companion from briefly serving the default certificate (and breaking clients such as Nextcloud sync) every time it is recreated. Closes #956.
Root cause
app/start.shlaunchesletsencrypt_serviceanddocker-genin parallel.letsencrypt_servicerunsupdate_certsimmediately, but at that pointdocker-genhas usually not yet generated/app/letsencrypt_service_data(the issue thread shows the tell-taleWarning: /app/letsencrypt_service_data not found).With no data file,
update_certsproceeds with an empty set of enabled domains and reachescleanup_links. There, every existing*.crtsymlink is classified as "disabled" (no enabled domain matches it) and, because each managed cert directory contains a.companionmarker, all of them are deleted. docker-gen then generates the data, the loop re-runs, and the symlinks are recreated one domain at a time viaacme.sh(RENEW_SKIP) +create_links— during which nginx serves the default certificate.This only requires nginx-proxy to already be up while the companion restarts (the
check_nginx_proxy_container_rungate passes), which is exactly the "recreate the companion on image update" scenario from the report.Fix
Return early from
update_certswhile neither/app/letsencrypt_service_datanor/app/letsencrypt_user_dataexists, before any path that can reachcleanup_links. The existing symlinks are left untouched, so nginx keeps serving the real certificates; the loop is re-triggered viaSIGUSR1(signal_le_service) as soon as docker-gen writes the data file, and verification/renewal then happens normally.The guard keys on file existence, not on the container count: docker-gen always writes the data file even with zero proxied containers, so the legitimate "all containers removed" cleanup still runs.
Test
New deterministic
certs_persistenceintegration test (registered intest/config.shand the CI matrix, runs under 2containers and 3containers): it issues a cert, removes/app/letsencrypt_service_data, runsupdate_certssynchronously (via the script'''s--source-onlymode), and asserts the symlink is still present.Verified locally
With a managed cert + symlinks in place and the data files removed:
update_certshits the new guard and the symlink is preserved, whereas calling the unguardedcleanup_linksdirectly (the old reachable behavior) removes it — confirming both the mechanism and the fix.Out of scope (deliberately)
Migrating only the
/etc/nginx/certs/<domain>/directories without their top-level*.crtsymlinks: there is no symlink to preserve, so issuance recreates it on the next loop as today. A pre-validation relink pass could close that too but is left as a follow-up to keep this change minimal.🤖 Generated with Claude Code