Background
I am setting up a server. The design of it is this (also here: https://codeberg.org/BlastboomStrice/SelfHostedPlan/) :

I have my configuration.nix under the nixos-base host here: https://codeberg.org/BlastboomStrice/dotfiles/src/branch/main/.config/nixos-config/hosts/nixos-base
I am using nixos-containers (which are systemd-nspawn containers essentially). Some of them, especially the *arr stack, need to be interconnected. Lidarr needs to talk to prowlar, qbittorrent and jellyfin for example. I am using the option privateNetwork = true; which, as per its description, gives the container its own private virtual Ethernet interface, the interface is called eth0, and is hooked up to the interface ve-«container-name» on the host.
Here is a test container module I’ve made: https://codeberg.org/BlastboomStrice/dotfiles/src/branch/main/.config/nixos-config/modules/nixos/containers/test-container.nix
This helps a bit with my set up, because each container gets its own hostname and it can exist as a seperate machine on tailscale. Thus, each container has its own address in my tailnet. (Tailscale, unlike headscale, does not let you create subdomains and the new feature “services” doesn’t really help me.) This isn’t an issue, but I’m just mentioning it.
Also, as per the wiki https://wiki.nixos.org/wiki/NixOS_Containers, I’ve enabled nat to let each container have access to the internet:
networking.nat = {
enable = true;
# Use "ve-*" when using nftables instead of iptables
internalInterfaces = [ "ve-+" ];
externalInterface = "wlp5s0"; # my wifi interface
# Lazy IPv6 connectivity for the container
enableIPv6 = true;
};
I use wifi btw (not ethernet).
In my configuration.nix I use this option
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
Also this may be useful https://www.calculator.net/ip-subnet-calculator.html
Issue
What I can’t figure out is how to connect two or more containers together (not with the host, the host can already access them). I want the container A to be able to ping the container B. I don’t want any container to be able to ping container B, only A. I may also want container C ping D. I want to keep the privateNetwork = true; option and I don’t want to make so many bypasses that will render it useless.
What the docs say
The wiki https://wiki.nixos.org/wiki/NixOS_Containers has a bridge section, which from what I understand, it bridges the container with the host internet interface. It’s confusing me a lot as not only I’m not sure whether it is what I need, but also it has options that are not very self-explanatory. I copied the snipped and added some comments with questions:
networking = {
# what do I put here? any interface that I want to be bridged? if so, why does it only have one interface "eth0s31f6"?
# two containers may have interfaces starting with "ve-", can I put these interfaces? (probably no)
# does it create a new interface named "br0"? (probably yes)
bridges.br0.interfaces = [ "eth0s31f6" ]; # Adjust interface accordingly
# Get bridge-ip with DHCP
# why do we deactivate that? doesnt that affect every thing on the machine?
useDHCP = false;
# why do we activate that?
interfaces."br0".useDHCP = true;
# Set bridge-ip static
interfaces."br0".ipv4.addresses = [{
# where does this point to?
# I suppose it creates a vlan with the address below and a netmask of prefixLength
address = "192.168.100.3";
# whats the length for? I just want to bridge 2 IPs together, could I use 31 bits?
prefixLength = 24;
}];
# what is this about?
# in my containers I set hostAddress to "192.168.100.10" etc.
defaultGateway = "192.168.100.1";
# is that the Broadcast Address?
# do I need that? what do I do with it? what is it??
nameservers = [ "192.168.100.1" ];
};
containers.<name> = {
privateNetwork = true;
# I suppose this connects the container to the bridge
# hostAdress is not used when you use hostBridge from what the option description says
hostBridge = "br0"; # Specify the bridge name
# the option description says to use the /netmask when you use the hostBridge option
localAddress = "192.168.100.5/24";
config = { };
};
As you can see it has confused me a lot:/ To add to this, the description of the option networking.interfaces says “Please note that systemd.network.netdevs has more features and is better maintained. When building new things, it is advised to use that instead.” which makes me think that the whole snippet may not be the best idea. But setting up systemd.network seems to be another huge thing to do (https://budimanjojo.com/2025/03/06/systemd-networkd-nixos-pain/?).
Also here https://forum.manjaro.org/t/setting-up-a-bridge-with-nmcli/186663/11, they say in a related (I think) attempt:
True bridging happens at Layer 2, and standard wifi client mode generally doesn’t support that.
Does that mean my whole set up won’t work?
https://microvm-nix.github.io/microvm.nix/advanced-network.html this here is just too confusing for me and I don’t know if it fits my case
https://www.geeksforgeeks.org/linux-unix/disabling-and-enabling-an-interface-on-linux-system/ this tells me that I probably can’t create a bridge when the interface I connect each container is disabled, so I guess I can’t use the eth interface.
https://unix.stackexchange.com/questions/363332/how-do-i-configure-a-network-interface-bridge-from-wifi-to-ethernet-with-debian I suppose I could bridge the ethernet and the wifi interfaces as well, but at this point I think that’s not needed.
https://oneuptime.com/blog/post/2026-03-04-configure-networking-for-systemd-nspawn-containers-on-rhel/view this gives me slop vibes, but if this snippet is correct it may point a serious issue in my config:
[Network]
# Use private veth
VirtualEthernet=yes
# Or use a bridge
# Bridge=br0
Can I use a bridge if I use privateNetwork = true;??
Other stuff I found that may be related:
- https://www.spinics.net/lists/systemd-devel/msg01465.html “How to add a second bridge to a nspawn container?”
- https://discourse.nixos.org/t/kinda-solved-combine-bridges-and-vlans/59966/ “Combine bridges and VLANs”
- https://wiki.archlinux.org/title/Network_bridge “Network bridge”
- https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/configuring_and_managing_networking/configuring-a-network-bridge “Chapter 5. Configuring a network bridge” (that was too long, didn’t read it much)
- https://www.baeldung.com/linux/bridging-network-interfaces (gives slop vibes hmm)
- https://discourse.nixos.org/t/bridged-vm-networking-setup/49976/ “Bridged VM networking setup”
- https://discourse.nixos.org/t/network-bridge-with-static-ip-on-host/15580 “Network bridge with static IP on host”
- https://discourse.nixos.org/t/nixos-networking-configuration-for-vms-on-notebook-use-ethernet-bridge-until-unplug/39487 “NixOS Networking Configuration for VMs on Notebook: Use ethernet bridge until unplug”
- https://discourse.nixos.org/t/setup-networking-between-multiple-vms/44910 “Setup networking between multiple VMs”
- https://old.reddit.com/r/NixOS/comments/knjxsb/adding_virtual_machines_to_physical_network/ “Adding Virtual Machines to Physical Network”
- https://github.com/NixOS/nixpkgs/issues/355450 “DHCP on network bridges”
- https://github.com/NixOS/nixpkgs/issues/16230 “When creating a bridge interface, the bridge doesn’t appear”
- https://wiki.nixos.org/wiki/Systemd/networkd
Things I’ve tried
I created 2 test containers to play around, lets call them containerA and containerB.
I tested by pinging the localAddress of containerA from containerB (which probably wouldn’t work as they are private networks?) and also I think I tried pinging the open port on the hostAddress or bridge of containerA from containerB. Many times the rebuilding failed before I even got to test the setup.
I tried the following:
# configuration.nix
networking = {
bridges.br0.interfaces = [ "wlp5s0" ]; # Also tried [ "enp4s0" ], [ "ve-containerA" "ve-containerB" ] and [ "wlp5s0" "ve-containerA" "ve-containerB" ]
# Get bridge-ip with DHCP
useDHCP = false; # also tried it with this commented out
interfaces."br0".useDHCP = true;
# Set bridge-ip static
interfaces."br0".ipv4.addresses = [{
address = "192.168.100.10";
prefixLength = 24; # also tried 30
}];
defaultGateway = "192.168.100.1"; # also tried it with this commented out
nameservers = [ "192.168.100.1" ]; # also tried it with this commented out
};
# containerA
privateNetwork = true;
localAddress = "192.168.100.11";
hostBridge = "br0"
# containerB
privateNetwork = true;
localAddress = "192.168.100.12";
hostBridge = "br0"
I tried skipping the bridge and simply using the same hostAddress in both containers
# containerA
privateNetwork = true;
hostAddress = "192.168.100.10";
localAddress = "192.168.100.11";
# containerB
privateNetwork = true;
hostAddress = "192.168.100.10";
localAddress = "192.168.100.12";
I tried using containers.<name>.interfaces, but the way I did it it failed badly
# containerA
privateNetwork = true;
hostAddress = "192.168.100.10";
localAddress = "192.168.100.11";
interfaces = "ve-containerB";
# containerB
privateNetwork = true;
hostAddress = "192.168.100.12";
localAddress = "192.168.100.13";
interfaces = "ve-containerA";
Some other stuff that may or may not work is using the following options:
containers.<name>.extraVeths
networking.ipvlans
networking.wlanInterfaces
I thinking that maybe I should somehow create an interface on the host and somehow bind/bridge the container interfaces (of the containers I want to communicate with each other) to that. Maybe containers.<name>.extraVeths would be helpful?
Other solutions
Again, we assume I created 2 test containers to play around, lets call them containerA and containerB. I don’t like any of these solutions as they aren’t real solutions, they simply bypass the issue.
We assume:
# containerA
privateNetwork = true;
hostAddress = "192.168.100.10";
localAddress = "192.168.100.11";
# containerB
privateNetwork = true;
hostAddress = "192.168.100.12";
localAddress = "192.168.100.13";
- ContainerA listens on port 8000
- ContainerB listens on port 8000
- Tailscale
Since I have tailscale set up already, my containers can work as if they weren’t on the same machine. I don’t like this, as it adds an extra point of failure (tailscale) and would be very problematic if I needed low latency or fast speeds. Steps to do this:- Forward-proxying a port from containerA localhost (http://localhost:8000) to the tailnet address of containerB (containerB.mytailnet.ts.net)
- Forward-proxying a port from containerB localhost (http://localhost:8000) to the tailnet address of containerA (containerA.mytailnet.ts.net)
- Many proxies (Two sounds as Too, makes a good pun)
This is very messy and essentially uses the host instead of tailscale servers. I do 4 proxyings per 2 containers…- Forward-proxying the hostAddress with the specified port of containerA (http://192.168.100.11:8000) to a specified port of containerB (http://192.168.100.12:8000)
- Reverse-proxying the hostAddress with the specified port of containerB (http://192.168.100.12:8000) to the localhost with the specified port of containerB (http://localhost:8000)
- We also need to do the reverse
- Forward-proxying the hostAddress with the specified port of containerB (http://192.168.100.12:8000) to a specified port of containerA (http://192.168.100.11:8000)
- Reverse-proxying the hostAddress with the specified port of containerA (http://192.168.100.11:8000) to the localhost with the specified port of containerA (http://localhost:8000)
- Single container
Another option is to put everything that needs access to another container into a single big container. While this is simple, it has some extra issues:- It reduces the isolation
- Since tailscale doesn’t support subdomains, I won’t be able to use subdomains and I will have to use subpaths (handles in caddy terms) or ports, if I want to have each service available to other other devices (so that I can control lidarr from my laptop for example)
- I want to have qbittorrent in a seperate container which will be behind a vpn. This solution would force me to somehow redirect only the traffic of qbittorrent throught the vpn and leave the other services intact.