Build Your Own Always-Up-To-Date Nginx Container with Brotli
/ 7 min read
the problem
In a previous post, we looked at how to minify, compress, and containerize a static Astro site. As it turns out, the runtime container I used, fholzer/nginx-brotli:latest , has a critical security vulnerability, and the nginx version it uses is about a year old. Even worse, the configuration file built into the container doesn’t direct 404 errors to a custom 404 page, using the default nginx page instead, which is totally unacceptable if you’ve gone through the trouble of building a custom 404 page.
The official nginx open source images don’t include brotli support because it’s part of their paid nginx plus offering, which also means they have no incentive to make installing brotli easy for users of the free version (if anything, the opposite). Google provides a brotli module that can be loaded into nginx but which has to be built from source. I do have some experience compiling projects from source, but it’s usually enough trouble that I reach for prebuilt tools unless I encounter some glaring shortcoming. In this case, none of the existing nginx + brotli containers on Docker Hub are consistently updated for the latest nginx release. Many also include capabilities that aren’t relevant for me, like GeoIP or SSL/TLS termination (which I handle in other containers if at all).
I did find this Dockerfile by KiweeEu which almost meets my requirements. It’s an ubuntu-based container, which is heavier and arguably less secure than alpine, but in and of itself that’s not a deal-breaker. Unusually, the author uses echo
and >
to modify the nginx configuration inside the Dockerfile instead of copying in a modified nginx.conf
. Embedding the configuration changes inside the Dockerfile like this strikes me as appealingly self-contained, but for my purposes I would still have to either extend this Dockerfile via FROM
or bind-mount a separate configuration via docker compose
.
Since I want the flexibility to apply my own custom nginx configuration, using the same single-file structure as KiweeEu’s Dockerfile, I wanted to see whether I could use Claude1 to integrate KiweeEu’s Dockerfile into my own codebase and adapt it to my needs. Leveraging AI assistance, it was relatively painless to build my own self-updating nginx + brotli Dockerfile, while also improving upon the original in several key ways.
AI to the rescue
So, with KiweeEu’s Dockerfile as a starting point, here’s what Claude and I came up with:
my closing thoughts
Would I have gone through the trouble of writing this Dockerfile myself? Probably not. I would likely have just accepted the out-of-date dependencies or sub-optimal base image and bind-mounted my own configuration into conf.d
. Having both a Dockerfile and an nginx.conf
wouldn’t be the most elegant solution, but it would get the job done. But this new era of AI assistance means you don’t have to settle for hacky fixes if you know how to find a suitable foundation and articulate the results you want.
This Dockerfile still isn’t perfect. I should really be using a container registry to host the nginx + brotli container image, and then just copy the built static files from this blog into it alongside any additional configuration I want applied. But this approach ensures that I check for the latest nginx version and rebuild the brotli module every time I deploy a new version of this blog. If I want to use the same setup in multiple projects, I can just copy and paste the portions of the Dockerfile that are relevant for that project. Coding with AI has definitely made me more cavalier about code duplication.
Claude is an incredible force multiplier. I can’t say that I would have come up with the exact same solution on my own, but Claude’s solution is easy to follow and stands up to manual verification. Resolving the handful of bugs that arose in intermediate versions went much more quickly than it would have on my own. In the not-so-distant past building something like this would have taken hours of research and debugging, even with a good starting point. Now I can get it done and write about it between my daughter’s and my own bedtime.
Claude’s closing thoughts
Here’s what Claude had to say about the improvements we made from my own and KiweeEu’s initial Dockerfiles:
Here are the key improvements we made to the original Dockerfile:
Automatic Version Detection: Added a dedicated
version-check
stage that queries GitHub’s API to always build against the latest nginx release.Efficient Multi-Stage Build:
- Separated concerns into distinct build stages (static files, version check, nginx compilation, runtime)
- Used the version check output as part of Docker’s cache key
- Each stage is cached independently and only rebuilds when necessary
- Nginx only recompiles when a new version is available
- Static content can be updated without triggering nginx rebuild
Alpine-Based Images:
- Switched from Ubuntu to Alpine Linux for all stages
- Reduced image size and attack surface
- Updated package names and build dependencies for Alpine
Improved Configuration Management:
- Split nginx configuration into logical files
- Separated brotli settings from server configuration
- Added proper error page handling with custom 404 page support
Version Verification:
- Added runtime check to ensure the compiled modules match the nginx version
- Fails fast if versions don’t match, preventing silent failures
The result is a maintainable, secure, and efficient container that automatically stays up to date with the latest nginx releases while keeping our custom configurations intact.
Thanks, Claude!