For a while, this “cluster” had a dirty secret. The web tier was genuinely three nodes — three copies of WordPress and Nginx, load-balanced, any of which could serve you. But behind them sat a single MariaDB and a single Redis, both on one node. Lose that node and the whole thing goes dark. A cluster with one heart isn’t a cluster; it’s a single point of failure wearing a crowd for a costume. This is the story of fixing that — making the database and the cache into real, synchronous clusters where any node can be written to and every node has the same data.
The goal: three that act as one
We wanted a MariaDB Galera cluster — three database instances kept in lock-step by synchronous replication, so a write committed on any node is instantly present on all three — and a 3-master Redis Cluster for sessions and cache, where the keyspace is shared across all three and a client reaching any node finds the same data. Both images already had what they needed: MariaDB shipped with the Galera provider, Redis 8 speaks cluster natively. It should have been a config exercise. It was not.
The wall: you cannot cluster across NAT
The first attempt to add a Galera node failed instantly with a cryptic message: Will never receive state. Need to abort. The cause was the network. Each node’s containers lived on the same private subnet (10.88.0.0/24), isolated behind that node and reachable only through port-forwards on the node’s public IP. Galera’s state transfer (SST) needs a joining node to advertise an address the donor can connect back to — but the container could only bind its private IP, not the public one it had to advertise. That mismatch is fatal, and it’s a well-known wall: you can’t run a peer-to-peer cluster protocol across one-to-one NAT.
The fix: give every container a real address
The clean solution was to stop hiding the containers behind NAT and make them directly routable. We renumbered each node’s pod network onto its own distinct subnet — 10.88.1.0/24, 10.88.2.0/24, 10.88.3.0/24 — turned on IP forwarding, and added routes between the nodes so a container on one node can reach a container on another by its own address, with no NAT in the middle. Suddenly every database and cache instance had a real, reachable identity. It touched the whole web tier’s wiring too, and it was worth every minute.
With that in place, Galera formed on the first try. We converted the existing database in place — bootstrapping it as the first cluster member without losing a single row — then brought up the other two, which pulled a full copy over the now-routable network and joined. Three members, status Primary, fully synced.
Proof, not promises
A cluster you haven’t tested is a rumor. So we wrote a row directly into the database on node 2 and immediately read it back from nodes 1 and 3 — there it was, identical, on both. We set a key in Redis on node 2 and fetched it from node 1 and node 3 — the cluster redirected each request to the right shard and returned the same value. Then we pointed each node’s WordPress at its own local database and the shared Redis Cluster. Every node now reads and writes locally, and the data is everywhere at once.
Born from one file
The last piece was making all of this reproducible. The entire cluster — the routed networks, the Galera and Redis instances, the web tier on every node, the load balancer in front — is now described in a single Ensemble manifest, and ocifbsd stack up -f deploys it across the nodes. We proved it by wiping a node’s containers and running that one command: they came back, in the right place, from the file. A cluster should be able to describe itself and rebuild itself. Now this one can — and there isn’t a single heart left to stop it.