Volumes instead of bind mounts (+ DKIM-key generation) - #58
Open
chrisblech wants to merge 3 commits into
Open
Conversation
Replaces ./db, ./pgp, ./upload, ./dkim.key bind mounts with named volumes
(db, sldata, upload). sldata is mounted once at /sl with subdirectories
for pgp/dkim/tmp/unsent, rather than one volume per bind mount.
DKIM_PRIVATE_KEY_PATH, GNUPGHOME, TEMP_DIR and SAVE_UNSENT_DIR are now
fixed in the sl-defaults anchor's `environment:` instead of .env.example -
there's no reason for these container-internal paths to be user
configurable, and setting them as literal `environment:` values (which
take precedence over env_file-provided ones) means any stale value in an
existing .env is automatically overridden, with no breaking change
required for upgraders.
Also adds DKIM key auto-generation, moved into `init` rather than
`migration` (checked: nothing in init_app.py, any migration script, or
migrations/env.py touches DKIM/GNUPGHOME/filesystem paths at all, so
there's no dependency requiring it to run as part of the DB migration
step - `init` is the more fitting home, since `migration`'s job should
stay limited to schema changes). Runs on every start but only ever acts
once: the mkdir -p and the missing-key check are both no-ops afterwards.
This also means it's the mechanism that provisions sldata correctly even
when migrate-volumes-compose.yaml has been commented out or was never
used.
New file migrate-volumes-compose.yaml (included by default, meant to be
commented out of docker-compose.yaml once migration is confirmed) adds a
`volume-migrate` one-shot service and gives `postgres` an extra
depends_on: volume-migrate: service_completed_successfully (merged
in via Compose's cross-file service merge, verified with `docker compose
config` - postgres's own definition elsewhere doesn't need touching). The
whole project directory is bind-mounted read-only as a single mount
(`.:/legacy:ro`) rather than mounting each legacy path individually -
mounting a single nonexistent file/dir source directly would make Docker
silently create an empty directory there, which matters most for
./dkim.key (frequently absent on a fresh setup) but avoids the same
footgun for ./db/./pgp/./upload too. Copies are skip-if-target-non-empty,
so safe to run on every start; never overwrites.
Verified locally (no changes needed to the app image - purely compose
orchestration):
- `docker compose config` renders cleanly, no stray variable-interpolation
warnings (had to escape shell variables in the new entrypoints as $$,
same as elsewhere in this file - a bare $DKIM_PRIVATE_KEY_PATH gets
swallowed by Compose's own interpolation and rendered empty, since
Compose interpolates the whole file's string values before service
definitions are even parsed, with no way to mark a block as "literal
shell, don't touch")
- init's entrypoint against a real sl-app image + fresh volume: creates
/sl/{pgp,dkim,tmp,unsent} with correct permissions (700/600), generates
a working RSA key; second run correctly skips regeneration
- app's entrypoint against the generated key: produces a valid DKIM
public key in the expected DNS TXT format
- volume-migrate against fake legacy content (db/pgp/upload dirs +
dkim.key): copies all four correctly; second run skips all four
(already-migrated, idempotent)
Replaces ./unbound/conf.d/:/config/:ro with an inline heredoc in the dns service's entrypoint, so it has no host-file dependency at all, matching the direction of the previous commit (avoid bind mounts). Recipe: springcomp#46 (comment) Removes the now-unused unbound/conf.d/ directory. Verified in isolation: the resolver container actually resolves and DNSSEC-validates (query for example.com returns a real answer over the new port-53 listener); the injected /config/00-listen-port.conf is picked up via the image's own `include: /config/*.conf`, additively alongside its default listener on 5053 - so the image's built-in healthcheck (which probes 5053) is unaffected and `dns: condition: service_healthy` on postfix still gates correctly.
The previous bind-mounted unbound/conf.d/00-unbound.conf deliberately set these three; dropping them (silently, by omission) when switching to the heredoc-generated config would have weakened functionality the upstream maintainer specifically established. Restoring them keeps this change a pure mechanism swap (bind-mount -> heredoc) rather than also being a behavior change, which matters for getting this merged cleanly upstream. - module-config: "validator iterator" - trims the cachedb module the image loads by default (unused without a configured cache backend) - identity: "DNS" - cosmetic (only visible via CHAOS TXT id.server queries), but kept for parity - root-hints: "/usr/share/dns-root-hints/named.root" - pins the image's own dns-root-hints package file, which it refreshes monthly via its own cron (/etc/periodic/monthly/dns-root-hints); without this Unbound falls back to whatever root hints are compiled into that unbound version, which then only changes when the base image itself is rebuilt Verified empirically before making this change (comparing the previous bind-mounted config against the image's defaults via 'unbound-checkconf -o <option>', not just reading docs): - interface: ::1 in the old config was actually dead - do-ip6: no (inherited from the image, present in both old and new configs) suppresses all IPv6 listening regardless, confirmed via /proc/net/tcp6+udp6 showing no IPv6 sockets even with the old config loaded, so no functional loss from not carrying it over - module-config/identity/root-hints, by contrast, all differed from the image defaults and are real, so those are the ones restored here After this change: unbound-checkconf -o confirms all three now match the old config exactly; DNSSEC validation still works (drill against a signed test zone still returns the 'ad' flag); normal resolution still works; the image's own healthcheck (port 5053) remains unaffected.
Owner
|
That looks awesome. Note to @springcomp (self): consider documenting upgrade path: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Volumes instead of bind mounts (+ DKIM-key generation)
Motivation
The current setup relies on bind mounts (
./db,./pgp,./upload,./dkim.key,./unbound/conf.d/) for anything that needs to persist or bepre-provisioned. That works fine when you deploy from a shell on the host,
but it quietly assumes host filesystem access: someone needs to
mkdirthe right directories and drop a
dkim.keyfile in the right place beforedocker compose upwill actually work correctly.That assumption breaks down for Portainer's Web editor / Git deployment
without SSH access to the host - a fairly common setup (managed Portainer,
restricted hosts, teammates who only get Portainer access). Named Docker
volumes are created and populated entirely by the Docker daemon itself, so
switching to them means the whole stack - including first-time DKIM key
generation - can be deployed and managed purely through the Portainer UI,
no host shell required.
This PR switches every bind mount to a named volume, adds a migration path
for existing deployments, and folds in a small related improvement
(DKIM key generation on first start) that enables to bring up the whole setup
without fiddling anything on the host.
What changed
1. Named volumes replace bind mounts
x-sl-defaults: &sl-defaults image: simplelogin/$SL_IMAGE:$SL_VERSION env_file: .env volumes: - - ./pgp:/sl/pgp - - ./upload:/code/static/upload - - ./dkim.key:/dkim.key + - sldata:/sl + - upload:/code/static/uploadsldatais mounted once at/sl, withpgp/,dkim/,tmp/andunsent/as subdirectories, rather than one volume per bind mount.postgresgets adbvolume the same way.DKIM_PRIVATE_KEY_PATH,GNUPGHOME,TEMP_DIRandSAVE_UNSENT_DIRarenow fixed in the
sl-defaultsanchor'senvironment:instead of being.env-configurable - these are container-internal implementationdetails of the volume layout, not something a deployer needs (or should
want) to change. Setting them as literal
environment:values means theytake precedence over anything
env_file:provides, so an existing.envthat still has the old
DKIM_PRIVATE_KEY_PATH=/dkim.keyis automaticallyoverridden rather than breaking anything.
2. Migration path for existing deployments
New
migrate-volumes-compose.yaml, included by default fromdocker-compose.yaml, adds a one-shotvolume-migrateservice:The whole project directory is bind-mounted read-only as a single mount
(
.:/legacy:ro) rather than mounting each legacy path individually -mounting a nonexistent single file/directory path directly would make
Docker silently create an empty directory there instead of leaving it
absent, which matters most for
./dkim.key(commonly absent on a freshinstall) but is avoided for all four legacy paths this way.
It's safe to leave this file included even on a fresh install with none
of the legacy paths present - it just creates the
sldatadirectorystructure and otherwise does nothing. Comment out its
include:line indocker-compose.yamlonce you've confirmed an existing deploymentmigrated successfully.
3. DKIM key generation, key echoed on
appstartupA entrypoint override in the init container ensures presence of all
paths (regardless of whether
migrate-volumes-compose.yamlwas used), and generates a DKIM-key if there is none present.
appnow also prints the DKIM public key on every start, as aconvenience for setting up the DNS TXT record:
4. DNS resolver config written at container start instead of bind-mounted
As we cannot rely on bind-mounts from the host any more, there is another
approach to inject unbound configuration into dns container: an entrypoint
override writes our config file on startup.
dns: image: crazymax/unbound:latest - volumes: - - ./unbound/conf.d/:/config/:ro entrypoint: - /bin/sh - -ec - | + cat > /config/00-listen-port.conf <<'EOF' + server: + interface: 0.0.0.0@53 + log-queries: yes + verbosity: 2 + module-config: "validator iterator" + identity: "DNS" + root-hints: "/usr/share/dns-root-hints/named.root" + EOF unbound-anchor -a /var/run/unbound/root.key || true exec su -s /bin/sh unbound -c "sh /entrypoint.sh"Testing
Everything below was checked with real containers, not just
docker compose config:init's entrypoint against a real sl-app image and a fresh volume:creates
/sl/{pgp,dkim,tmp,unsent}with correct permissions(700/600), generates a working RSA key; a second run correctly skips
regeneration
app's entrypoint against the generated key produces a valid DKIMpublic key in the expected DNS TXT format
volume-migrateagainst fake legacy./db,./pgp,./upload,./dkim.keycontent: copies all four correctly on first run; a secondrun skips all four (idempotent, never overwrites)
returns the
adflag);unbound-checkconf -oconfirmsmodule-config/identity/root-hintsnow match the old configexactly; the image's built-in healthcheck (port 5053) is unaffected by
the new port-53 listener, since both are registered additively via the
image's own
include: /config/*.confdocker compose configrenders cleanly end-to-end with novariable-interpolation warnings (had to escape the new shell variables
as
$$, same convention used elsewhere in this file - Composeinterpolates the whole file's string values, including inside
multi-line shell script blocks, before service definitions are parsed)
No changes to the SimpleLogin app image itself - this is purely compose
orchestration.