🇩🇪 Diesen Artikel gibt es auch auf Deutsch.
From Linux project to native port, a development chronicle
Sometimes it all starts with a simple question: Which WAF can I run on FreeBSD in production?
The answer I found: none that I really liked. So I solved the problem myself, and brought BunkerWeb into the FreeBSD Ports Collection. This article describes the road there: architecture decisions, setbacks, solutions, and everything that happened between the first make and the final commit.
1. Motivation: Why BunkerWeb?
While looking for a modern open-source WAF for FreeBSD, I had a clear list of requirements:
- Reverse proxy
- OWASP Core Rule Set with ModSecurity
- Automatic Let’s Encrypt
- Security headers
- Rate limiting & bot protection
- Central, clear configuration, ideally with a web UI
BunkerWeb ticked all those boxes, on Linux. There was no official port for FreeBSD. The decision was made quickly: I would create the port myself and maintain it long-term.
2. Analyzing the upstream project: what are you in for?
Before you create a port, you have to understand what you are dealing with. I looked at:
BUILD.mdand the Dockerfiles- The Linux packages and their contents
- The build system
- The Python components and their dependencies
- The Lua components
- The entire directory layout
The result was sobering, and at the same time a clear to-do list:
- Very Linux-centric, lots of assumptions about the file system
- Hard-coded paths like
/usr/share/bunkerweband/etc/bunkerwebeverywhere - Numerous Python dependencies, not all of which existed as FreeBSD ports
- Several independent services: OpenResty, scheduler, API, UI, each with its own requirements
One thing was clear: this was not going to be a quick weekend project.
3. The first architecture decision: nginx or OpenResty?
My first approach was the obvious one, BunkerWeb is based on NGINX, so build on top of www/nginx.
That did not work.
BunkerWeb relies on extensive Lua components for its security logic. Stock NGINX does not ship that Lua environment. You could bolt on the Lua modules individually, but that would be a fragile construction with a lot of maintenance overhead.
The solution: switching to OpenResty.
OpenResty is essentially NGINX with a fully integrated LuaJIT environment and all the required lua-nginx-module extensions. With it, BunkerWeb runs its entire Lua logic cleanly, without workarounds. That decision made the port more maintainable in the long run, even though it meant starting over from scratch.
4. Missing ports: construction sites before the actual construction site
While porting, it quickly became apparent that not all dependencies existed in the FreeBSD ports tree yet.
Before www/bunkerweb could be finished at all, additional ports had to be created or adapted, among others:
- Adjustments around the nginx/OpenResty integration
- Various Python dependencies as ports of their own
py-defusedcsv: born out of the BunkerWeb effort, but useful on its own by now
This is a typical situation when porting complex software: you end up contributing to the ports tree on the side, without ever having planned to.
5. The actual port: everything that belongs to it
A complete FreeBSD port is more than a Makefile. For www/bunkerweb, the following came into being:
www/bunkerweb/ ├── Makefile # Build logic, dependencies, options ├── distinfo # Checksums of the source files ├── pkg-plist # All installed files ├── pkg-message # Post-install notes └── files/ # rc.d script, patches (patch-*), config templates
Each of these files has a story of its own.
6. Linux paths: the biggest recurring annoyance
BunkerWeb expected its files exactly where Linux expects them:
/usr/share/bunkerweb/→ core, plugins, UI files/etc/bunkerweb/→ configuration/var/lib/bunkerweb/→ runtime data
On FreeBSD the convention is unambiguous: third-party software belongs under /usr/local/: i.e. PREFIX in ports jargon. Practically every path reference in the code had to be replaced:
- /usr/share/bunkerweb/core
+ ${PREFIX}/share/bunkerweb/core
- /etc/bunkerweb/variables.env
+ ${PREFIX}/etc/bunkerweb/variables.env
That sounds mechanical, but it was tedious, because these paths were not only in configuration files, but buried deep in the Python code, in Lua scripts, and in startup scripts. For every occurrence: write a patch, test, repeat.
7. Python: several construction sites at once
The Python side of the project was a chapter of its own:
Dependencies: All runtime dependencies had to be wired up as py3xx-* ports, no pip install inside a port.
pkg_resources and setuptools: BunkerWeb uses pkg_resources internally for path discovery. On a cleanly installed FreeBSD port, that behaves differently than in a Linux virtualenv. This required adjustments.
No virtual environment: In Docker deployments, BunkerWeb typically runs in an isolated container with its own Python. For a native port, the rules are: system Python, modules from ports, no venv. That required carefully aligning the dependencies.
File permissions: Some Python files generated during the build were missing the execute bit. As a result, the scheduler and the configuration generator would not run, a bug that only showed up at runtime, not at build time.
8. rc.d instead of systemd
BunkerWeb ships with systemd units for Linux:
bunkerweb.service bunkerweb-scheduler.service bunkerweb-ui.service
FreeBSD has no systemd. Instead: rc(8). Each service got its own rc.d script that follows the FreeBSD conventions for PID files, runtime directories and logging:
#!/bin/sh
# PROVIDE: bunkerweb
# REQUIRE: NETWORKING DAEMON
# KEYWORD: shutdown
. /etc/rc.subr
name="bunkerweb"
rcvar="bunkerweb_enable"
pidfile="/var/run/${name}/${name}.pid"
logfile="/var/log/${name}/${name}.log"
command="/usr/local/bin/bunkerweb"
command_args="--pid ${pidfile}"
load_rc_config $name
run_rc_command "$1"
The services are then enabled with sysrc:
sysrc bunkerweb_enable="YES" sysrc bunkerweb_scheduler_enable="YES" sysrc bunkerweb_ui_enable="YES" # optional
9. OpenResty: more than just renaming NGINX
Switching to OpenResty was the right architecture decision, but OpenResty had its own quirks:
- nginx.conf and includes: BunkerWeb generates its NGINX configuration dynamically. The generated paths for includes, log files and the socket had to be adapted to FreeBSD conventions.
- HTTP/2: Required specific OpenResty compile options.
- Prefix and startup parameters: OpenResty has to be started with the correct
--prefixso it can find its configuration and modules.
10. A chronicle of the problems
An honest overview of the most important hurdles, from start to finish:
| Problem | Cause | Solution |
|---|---|---|
| Architecture unclear | Upstream designed for Linux only | Worked out a FreeBSD port concept |
| nginx is not enough | Missing Lua integration | Switched to OpenResty |
| Missing ports | Dependencies not in the ports tree | Created and committed new ports |
| Wrong runtime paths | Hard-coded Linux paths | ${PREFIX} patches for all components |
| Scheduler will not start | Missing execute bits on generated files | Fixed permissions in the port install |
| API will not start | Port 8888 already taken by OpenResty | Adjusted the configuration |
| Logs/PIDs missing | Directories were never created | Created automatically in the rc.d script |
The API port conflict was particularly tricky: it looked like the API was crashing, but it was not. OpenResty had USE_API=yes and API_HTTP_PORT=8888 set and was occupying the very port the BunkerWeb API wanted to use as well. Not a crash, but a configuration question.
11. Testing: no merge without a fresh jail
A successful build is not yet a working port. That theme ran through the entire project.
The testing process was accordingly thorough:
# Iterative build cycle make clean make extract make patch make stage make package # Validation poudriere testport www/bunkerweb
Tests in fresh jails were the crucial part, only there does it become clear whether all dependencies are really declared and whether the port works without leftovers from earlier installations. Runtime tests (scheduler running? API reachable? UI showing up?) always came as the last step.
12. Installation: how to use the port
Via pkg (recommended)
pkg install bunkerweb
From the ports tree
cd /usr/ports/www/bunkerweb make install clean
Enable and start the services
sysrc bunkerweb_enable="YES" sysrc bunkerweb_scheduler_enable="YES" service bunkerweb start service bunkerweb-scheduler start
Initial configuration
The central configuration lives in /usr/local/etc/bunkerweb/variables.env:
SERVER_NAME=www.example.com USE_REVERSE_PROXY=yes REVERSE_PROXY_URL=/ REVERSE_PROXY_HOST=http://127.0.0.1:3000 USE_LETS_ENCRYPT=yes EMAIL_LETS_ENCRYPT=admin@example.com
Accessing the web UI
The UI is reachable at http://127.0.0.1:7000 by default. For remote access, an SSH tunnel is recommended:
ssh -L 7000:127.0.0.1:7000 user@my-server
13. Lessons learned
This is the chapter I recommend to anyone who wants to tackle a complex port themselves:
Not every Linux project can be ported 1:1. BunkerWeb carries Linux as a base assumption deep in its code. That is not a criticism of the upstream project, but you have to understand it before you start.
Architecture decisions are allowed to change. The switch from nginx to OpenResty did not happen on day one. Sometimes you need time and failure to recognize the right solution. That is not a step backwards.
A successful build does not mean a working port. The hardest problems, wrong paths, missing permissions, port conflicts, only show up at runtime. Build and runtime are two different worlds.
Poudriere and fresh jails are indispensable. No port should go into the tree without tests in a clean environment. Period.
Conclusion
Months of work, a few architecture changes, dozens of patches and a couple of new ports later: BunkerWeb is now a native FreeBSD port. No Docker wrapper, no Linux compatibility layer, but www/bunkerweb, cleanly integrated into the ecosystem.
The port is fresh. Feedback is very welcome, bug reports, success stories, or suggestions for improvement. The best places are the FreeBSD forum thread or bugs.freebsd.org.
If you would like to support my work on this and more than 40 other ports, you will find the option to do so in the sidebar.
Have you already tried BunkerWeb on FreeBSD? Let me know in the comments!