You’re sitting in your homelab. You have a few services set up, everything seems to be running fine, and then you add one more service and suddenly things start breaking.
I was in that position a few months ago.
At the time, I was mostly researching, testing, and learning different services before deciding what I actually wanted my homelab to become. I spun up some VMs, installed a few tools with mostly default settings, and quickly learned why having a plan in place before building out a server environment matters. Even with a background in IT, I quickly found that spinning up services without a plan creates the same headaches in a homelab as it does in a production environment
The services themselves were already enough to learn. Adding DNS issues, routing problems, and service-to-service communication on top of that made the whole thing harder to troubleshoot than it needed to be.
That was when I decided to tear the lab down, start from scratch, and build the network foundation first.
The biggest problem in my first attempt was communication between services. Once I had a dedicated server to build around, I decided to learn and configure OPNsense as the firewall/router foundation for the homelab instead of continuing to add services onto a messy network.
Why OPNsense?
I chose OPNsense for a few reasons: security, centralized services, and organization.
Security
The most obvious reason was security.
I did not want a flat network where every service could talk to every other service by default. A service should only have access to the other services it actually needs, and nothing more.
If a dashboard VM only needs access to a PostgreSQL server, then that should be the only access it has.
Centralized Services
I wanted a central place to manage some of the major networking services and functions: DNS, DHCP, routing, and static reservations.
When a VM is having a DNS issue, or is not pulling an IP reservation correctly, I want an obvious first place to look. For me, that place is OPNsense.
It becomes the source of truth for the active network configuration and gives me a cleaner way to diagnose issues before I start making unnecessary changes on individual VMs.
This also lets me manage IP reservations centrally instead of setting static IPs manually on every machine. That helps avoid duplicate IPs, keeps the network easier to track, and feels like a much cleaner long-term strategy.
Organization
I am a firm believer that everything should have its place.
Storage systems should be in one area, monitoring tools should be in another, and temporary test systems should not be treated the same way as core services.
If I am adding a new service, I should be able to tell you where that service belongs in the network.
This makes security easier to manage, but it also helps me avoid getting lost in the weeds later.
Why I Wanted This in Place Early
The worst thing you can try to do is build a complex system and manage it from memory. Not only is this why documentation is so vital in IT, but it is also why coming into a problem with a solid foundation is so important.
I implemented OPNsense first so I would not need to come back and do a major overhaul of all my systems and their dependencies later.
While it may be more complex and require more work than just installing a VM and calling it a day, it is the only way to make sure the system is logical and secure.
Instead of worrying about setting up a firewall and breaking things later, I have the comfort of knowing that it is already in place, working as I intend, and will not be a major pain point later.
How This Looks in My Homelab
As of writing this article, I have a few services running: JupyterHub, PostgreSQL, SeaweedFS, Forgejo, an automation stack using n8n and Kestra, and an ebook library called Kavita.
For my implementation, only a few of the VMs need each other’s services. I am keeping the network areas broad here rather than listing exact VLANs or internal addressing, but the structure looks something like this:
| Service | Network Area | Communicates With |
|---|---|---|
| JupyterHub | Data | PostgreSQL, SeaweedFS, Forgejo |
| PostgreSQL | Storage | JupyterHub, Kestra, n8n |
| SeaweedFS | Storage | JupyterHub, Kestra, n8n |
| Forgejo | Apps | JupyterHub, Kestra, n8n |
| Automation Stack | Apps | PostgreSQL, SeaweedFS, Forgejo |
| Kavita | Apps | N/A |
Most of the services use PostgreSQL and SeaweedFS in some way. A lot of them also touch Kestra, JupyterHub, n8n, or Forgejo depending on what I am building.
The first thought may be, “Why not just create a broad allow rule between those services?” That would technically work, but we should still absolutely implement the principle of least privilege.
If we only give those apps access to exactly what they need, the setup is much more secure. An example would be creating a rule that allows traffic from JupyterHub to PostgreSQL without giving JupyterHub broad access to everything else.
Another thing you may have noticed is Kavita. In this case, a rule would not need to be created because I work from a deny-by-default system.
If traffic does not match an intentional allow rule, it should be blocked by default.
Where This Left Me
Setting up OPNsense early has made the rest of the homelab easier to plan. I have a central place for DNS, DHCP, static reservations, and firewall rules, which gives me a better starting point when something is not communicating the way I expect.
There is still plenty left to build and clean up, but I would rather have the network structure in place now than try to add it later after more services depend on each other.