Networking
Clouduler networking answers three questions: how do your containers talk to each other across machines, how do they reach the outside world, and how does the outside world reach them. You answer them by declaring networks in the cluster config and attaching services to them in your stack files.
This page assumes you know what a container is, but not what VXLAN is. New terms are in the glossary.
How it works
A cluster has a handful of named networks. Each one has a kind that says what it is for.
| Kind | In one sentence | Subnet | Domain | Per-node setting |
|---|---|---|---|---|
internal | The one trusted network nodes use for raft, drive sync and the container overlay. Exactly one is required. | required | optional | an IP, on every node |
private | An extra trusted LAN-style network with a subnet. | required | optional | an IP, on nodes that use it |
public | Reachable from the internet. No subnet. Hostnames resolve to each node's public address. | none | needed for web routes and DNS | an ipv4 / ipv6 / domain block and optional open_ports |
egress | Forces a service's outbound traffic out of one named host interface (for example a VPN), with a kill-switch. | none | not allowed | the host interface name |
wireguard | Like egress, but Clouduler creates the WireGuard tunnel from a config you import. | none | not allowed | set by importing a config |
nat | Like egress, but uses the node's normal routing (ordinary masquerade). No setup. | none | not allowed | nothing |
The container overlay
Containers on different machines reach each other through an overlay: a virtual network built on VXLAN (a tunnel that carries ethernet frames over UDP port 4789). You do not set it up. For each stack and each internal, private or public network it uses, every node builds a bridge plus a VXLAN device, with a static list of peers (no multicast). Containers get addresses from a cluster-wide pool, 10.244.0.0/16, carved into one /24 per overlay. The gateway is .1 and is the same on every node.
Containers find each other by name. The cluster DNS answers on the overlay gateway:
| Name | Answer |
|---|---|
SERVICE.STACK | every running instance, round robin |
tasks.SERVICE.STACK | the same set (Swarm style) |
NODE | that node's internal IP |
| a proxy hostname on a public network | this node's internal IP, so containers see the same service the outside sees |
| anything else | forwarded to the host's upstream resolvers |
Inside a stack, the bare service name also works (db), because each container gets a search list of STACK.DOMAIN and DOMAIN. Answers have a 5 second TTL.
The overlay MTU is the underlay MTU minus 50 bytes, so 1500 becomes 1450. This is derived automatically from the link that holds the node's internal IP. Egress bridges always use 1500.
Declare a network
Networks live in the cluster config, not in a stack. Each node says how it is attached to each network.
- Web UI
- CLI / config
Open Settings, tab Network, page Networks, and click Add network. Rename the card with the pencil button, then choose Network type: Internal, Private, Public, Egress, WireGuard or NAT. Depending on the type you also fill in Address range (CIDR), DNS domain, and Publish to DNS provider (Cloudflare). Save the page; risky changes ask for confirmation.
Then, in the node settings, a Network addresses section has a field for each network the node is on: Address on NETWORK for internal and private, Public IPv4, Public IPv6, Alias hostname and Open ports for public, and Interface, Use a backup interface, Fallback interface, Health check address and Open ports for egress. A nat network shows no field at all.
A read-only overview of each network is at /networks/NAME, listing the nodes on it and whether they are reachable.
Edit /etc/clouduler/cluster.yaml, then push it:
networks:
tailscale: # internal: node-to-node and the overlay
type: internal
subnet: 10.10.0.0/24
domain: int.example.com
lan: # private: an extra trusted network
type: private
subnet: 192.168.1.0/24
domain: lan.example.com
publish_dns: false # keep lan.example.com out of Cloudflare
wan: # public: no subnet
type: public
domain: example.com
vpn: # egress: a host interface must exist on the node
type: egress
home: # nat: ordinary masquerade, no per-node config
type: nat
tunnel: # wireguard: Clouduler brings the tunnel up
type: wireguard
nodes:
atlas:
role: manager
network:
tailscale: 10.10.0.1
lan: 192.168.1.10
wan:
ipv4: 203.0.113.5
ipv6: "2001:db8::5"
open_ports: [{port: 25, protocol: tcp}] # your router forwards 25/tcp here
vpn: wg0 # bare interface name
boreas:
role: manager
network:
tailscale: 10.10.0.2
lan: 192.168.1.11
wan:
domain: boreas.example.net # a CNAME instead of an IP
vpn:
interface: wg0
fallback:
interface: end0
health_check_ip: 192.0.2.1
clouduler check # validate the file
clouduler config reload # push it into the cluster
There is no CLI command for networks themselves.
Network options
networks.NAME.typerequiredenumno defaultCIDRno defaultDNS nameno defaultbooldefault trueNode address options
IPno defaultIPv4no defaultlist of {port, protocol}default []interface nameno defaultstrno defaultstrno defaultIPno defaultset by importno defaultforbiddenno defaultu16default 7373Use a network from a stack
Declare the networks at the top of the stack file, then list them on each service.
networks:
lan: {} # empty is fine: no allowed list means unrestricted
home: {} # a nat network, so the service can reach the internet
services:
web:
image: example/web:1
networks: [lan, home] # one overlay plus at most one egress, wireguard or nat
Clouduler picks the stack's overlay network (the one that provides DNS and an overlay address) as the first internal or private network in alphabetical order of network name (not declaration order), otherwise the first public network, otherwise none. A stack that only uses egress, wireguard and nat networks has no overlay: no internal DNS, no proxy gating, and Docker's default resolver.
map or emptyno defaultlist of strdefault unrestrictedlist of namesdefault overlay onlynameno defaultnameno defaultu16no defaultbooldefault falseStack networks must exist in the cluster, and some node must implement each one (except nat). Add the node's address first, then deploy. The CLI says no network named NET declared in the cluster or no node implements network NET, this service could never be placed. The web UI does not check this at deploy time; the same problem shows up on the stack's Placement tab (see Placement).
Publishing a service
There are four ways to let the outside reach a service. Pick by protocol and by whether the outside needs a name.
| Method | Config | What the outside sees | Needs |
|---|---|---|---|
| Proxy route (HTTP/HTTPS) | x-clouduler.proxy on a network with a domain | host.domain with TLS | the proxy on ports 80/443, and for a public network a public address per node |
| Raw TCP/UDP port | x-clouduler.expose | NODE_IP:published | on a public network, a node whose router forwards the port (open_ports) |
| Port through a WireGuard tunnel | a WireGuard ingress rule | the tunnel's far end forwards to the node | the container publishes target_port with ports: |
Docker ports: | plain publish | the node's host address and port | nothing managed by Clouduler |
A proxy route on a stack looks like this. A bare host gets the network's domain appended:
networks:
wan: {}
services:
app:
image: example/app:1
networks: [wan]
x-clouduler:
proxy:
- host: app # becomes app.example.com
network: wan
port: 8080
For HTTP details (middlewares, TLS, authentication) see Proxy and TLS and for public DNS records see DNS publishing.
Who can use a route. A route on a subnetted network (internal, private) only accepts a client whose connection comes from inside that network's subnet (or a trusted overlay). Anyone else gets 403 this route is only reachable from its own network. A route on a public network has no such gate. To publish externally, use a public network.
Raw ports with expose are for things like SSH:
services:
git:
image: example/git:1
networks: [wan]
x-clouduler:
expose:
- port: 22
published: 2222
network: wan
proxy_protocol: false
expose forwards in user space, so the backend sees the node's bridge address as the client. Use proxy_protocol: true (TCP only) and enable PROXY protocol v2 on the backend first, or the connection breaks. A published port with no running instance stays bound and refuses connections.
Where DNS and proxy settings live
web.networkrequirednameno defaultall-nodes or owning-nodedefault all-nodesbooldefault falselist of namesdefault every subnetted network the node is onu16default 80, 443Allowed hosts and ports
By default a stack's containers can reach whatever their networks let them. An allowed list on a network narrows that to a short list.
networks:
lan:
allowed:
- auth.example.com # hostname only: enforced by the proxy
- "*.example.net" # wildcard host, no port
- db.example.com:5432 # host:port, enforced by the firewall
- 192.0.2.10:9000-9010/udp # IPv4 literal with a port range and protocol
services:
app:
image: example/app:1
networks: [lan]
- Entries are
host,*.suffix,host:port,host:lo-hi,host:port/udp, or an IPv4 literal.allowed: []denies everything. A bareallowed:with no value is an error. - On an overlay network (internal, private or public), hostname-only entries are enforced by the proxy: a blocked request gets
403 stack STACK may not reach HOST through network NET: it is not in the network's allowed list. Entries with ports are enforced by the host firewall (CLDR-ALLOW). - On an egress, wireguard or nat network, every entry is enforced by the firewall, with hostnames resolved through the stack's DNS. The stack must also declare an internal, private or public network, otherwise you get
network NET has an allowed list ... the stack needs an internal, private or public network as well. - A wildcard host cannot take a port on an overlay network:
a wildcard host is checked by the proxy here and cannot take a port. - An allowed entry can only narrow what routing already carries. It cannot open a host port the firewall keeps closed.
WireGuard networks
A wireguard network lets Clouduler bring up a tunnel for you on each node, and send a service's traffic through it. It is the easy way to route a service through a VPN provider.
- Declare the network with
type: wireguardin the cluster config. - For each node that should have a tunnel, import a wg-quick config. The config is stored as a sealed secret and the node's interface is named
wg-NETWORK. - Attach services to the network in the stack.
- Web UI
- CLI / config
In the node settings the network appears as a card titled NETWORK (wireguard) with Config source (Paste or File), a text area and an import button. After import it reads "Imported. Tunnel interface: wg-NETWORK", with an Open ports editor and a Remove button. The import applies immediately, like a secret.
To let a port in through the tunnel, open Proxy, tab Ingress rules (/proxy/ingress), click New ingress rule, and fill in Network, Port (on the tunnel), Protocol and Target port (on this node). The target is a host port that a container publishes with ports:.
There is no CLI command for importing configs or ingress rules. Use the web UI or the HTTP API:
# base64 of a wg-quick file; returns {"interface": "wg-tunnel"}
curl -X PUT https://cloud.example.com:8443/api/wireguard/tunnel/atlas \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{\"content\": \"$(base64 -w0 wg0.conf)\"}"
See the HTTP API reference.
body {content}no defaultno default[Interface] PrivateKey, Addressrequiredstrno defaultno defaultIngress rule: portrequiredu16no defaultIngress rule: target_portrequiredu16no defaultImport errors you may see (wrapped as parsing wireguard config: ...): missing PrivateKey in [Interface], missing Address in [Interface], no [Peer] section found, a [Peer] section is missing PublicKey, invalid AllowedIPs entry ..., invalid Address ..., malformed line: .... Other errors: NETWORK is not a wireguard network, wireguard config must be utf8.
The firewall chains
Clouduler manages host firewall rules with iptables. It only touches chains with its own names, rebuilds them every 10 seconds (so a hand edit to them is overwritten), and re-checks the jumps so they survive a Docker restart.
| Chain | What it does |
|---|---|
CLDR-IN (filter, INPUT) | Stops containers reaching the node's own listeners. From container addresses it accepts only: replies, DNS, the proxy's HTTP and HTTPS ports, every expose port, and ping. Everything else, including the API, raft, drive and relay ports, is dropped. Docker -p publishes bypass it. |
CLDR-OVL-SNAT (nat, POSTROUTING) | Masquerades overlay traffic to another node's proxy ports (and ping), so the remote proxy sees a node address and can apply its network gate. |
CLDR-NAT-BRIDGE (nat) | Masquerades an egress bridge out of its interface. |
CLDR-FWD-BRIDGE (filter) | The egress kill-switch: allow bridge to interface, drop everything else from the bridge. If the interface has no route, the bridge is locked down. |
CLDR-WGIN-IFACE (filter, nat) | Default deny from a WireGuard tunnel, then each ingress rule. Adds a DNAT to the node's internal IP when the target port differs. |
CLDR-ALLOW (filter, DOCKER-USER) | Enforces allowed entries with ports. Exists only when some stack has an allowed list. |
CLDR-ALLOW-NAT (nat, PREROUTING) | Sends a restricted stack's proxy traffic to this node's own proxy so the proxy can tell stacks apart. |
In plain words: a container can reach the proxy on any node, the DNS on its gateway, and any expose port on any node. It cannot reach anything else on the host, and it cannot reach the internet unless it is on an egress, wireguard or nat network.
For a host process to reach a container on another node, Clouduler uses a relay on the node's rpc_port + 2 (7375 by default), authenticated with the cluster key. This port must be open between nodes on the internal network.
Rename a network
Renaming rewrites the network everywhere: the cluster config, node address maps, web.network, every deployed stack, redirects, static routes, WireGuard ingress rules and sealed WireGuard secrets. Use the pencil button on the network card in Settings (it calls POST /api/config/networks/NAME/rename with {"new_name": "..."}). There is no CLI command.