Sudo That Survives Every Container Recreate
The upstream image ships no sudo and /etc is ephemeral. A tiny entrypoint override in compose reinstalls passwordless sudo on every boot.
The container just got recreated from the upstream image, I opened a shell, and sudo was gone again. The container user needs sudo for host ops; every image re-pull wipes whatever got installed in /etc, and that directory is ephemeral.
Old thinking: just install sudo manually every time the container runs. Sometimes I remembered, sometimes not; when forgotten, tools needing sudo silently fail mid-run. Tedious, but it seemed like the only way.
Wrong. The fix isn't installing once, it's a setup that runs on every boot. This commit changed docker-compose.yml and added a small bootstrap-root.sh script under docker/, in the same compose file that once made two containers clash on port 8642.
Its shape in compose:
services:
hermes:
# Root bootstrap — see the entrypoint override below.
volumes:
- ./docker/bootstrap-root.sh:/opt/data/bin/bootstrap-root.sh
# Runs as root before the image's entrypoint drops privileges,
# then hands control to the original entrypoint from this script.
entrypoint: ["/opt/data/bin/bootstrap-root.sh"]
The script is short. Runs as root, installs sudo if missing, writes the NOPASSWD rule into /etc/sudoers.d/, fixes permissions, then execs the image's original entrypoint:
printf 'hermes ALL=(ALL) NOPASSWD: ALL\n' > /etc/sudoers.d/hermes
chmod 0440 /etc/sudoers.d/hermes
Those two lines are the whole point. The rest is safety: apt-get install sudo if the image lacks it, then exec into the image's entrypoint-dispatch.sh so s6 keeps working and privileges drop to the normal user.
Execution order detail easy to miss: the script ends with exec, not just calling the original entrypoint on the last line. With exec, the script's shell process is replaced by the image entrypoint in the same PID, so container lifecycle signals reach the right process. Invisible when healthy, felt at clean shutdown.
To be sure the bootstrap worked, two cheap checks after the container is up: sudo -l from the regular user shows the effective NOPASSWD rule, and visudo has a check mode (-c) that can target a specific file, handy to call from the script before that file is used. Both are documented in the sudoers.d article linked next.
The sudoers file is picky about permissions
Files in /etc/sudoers.d/ must be root:root mode 0440, or sudo silently ignores them. No error, they just don't load. Red Hat documents this in their sudo access management guide, including the detail that #includedir in sudoers is syntax, not a comment. The sudoers.d and visudo practice piece on dev.to adds another trap: filenames containing a dot get ignored too. So chmod 0440 is mandatory, not optional, and the filename stays plain with no extension.
The wire connecting it all is one compose rule: entrypoint overrides the image's ENTRYPOINT, and when it's non-null, the image default command is ignored compose services reference. Our script takes control first, handles the root stuff, then hands over.
Why not bake sudo into the image
Forking the image is possible, but every upstream release means rebasing. With a host-side bootstrap mounted at each boot, upgrading the image is a plain docker compose pull, and the bootstrap runs again on top of the new image. Bonus: the bootstrap file gets reviewed in git next to the compose file, so there's a trail of when the sudo rule changed, not hidden in an image layer.
My pick is the entrypoint override that ends with exec into the original entrypoint. The bootstrap is idempotent, safe to run repeatedly. And passwordless sudo in a personal container is a deliberate decision, not negligence; this container is an extension of the host user, not a multi-tenant server. The one thing I keep: the bootstrap script lives on the host, so /etc can stay ephemeral while the setup never disappears with it.