MySQL NDB Cluster — 3-Node Setup Guide (Ubuntu/Debian)
Step-by-step guide to set up a 3-node MySQL NDB Cluster on Ubuntu/Debian with co-located roles.
MySQL NDB Cluster — 3-Node Setup Guide (Ubuntu/Debian)
Hardware assumed: 3 servers, 2 vCPU / 4GB RAM each.
A note on sizing before you start
MySQL Cluster (NDB) needs a minimum of 2 data nodes for redundancy, plus a management node, plus at least 1 SQL node to run queries. With only 3 physical servers, you can't dedicate a whole machine to each role — so we'll co-locate the lightweight management process with one of the data/SQL nodes. This is a completely standard pattern for small clusters.
4GB RAM per node is workable for development, testing, and light production workloads, but it's tight. Real production NDB deployments usually run with much more RAM per data node, because all data and indexes live in memory. Treat this guide as "small but correct" — you can scale DataMemory up later if you add RAM.
Topology
| Server | Role(s) | Hostname (example) | IP (example) |
|---|---|---|---|
| Server 1 | Management node (ndb_mgmd) + SQL node (mysqld) | ndb1 | 10.0.0.11 |
| Server 2 | Data node (ndbd) + SQL node (mysqld) | ndb2 | 10.0.0.12 |
| Server 3 | Data node (ndbd) + SQL node (mysqld) | ndb3 | 10.0.0.13 |
This gives you: 2 data nodes (1 replica each = NoOfReplicas=2, so data survives one data node failing), 1 management node, and 3 SQL nodes you can load-balance reads/writes across (or point an app at any of them).
Replace the IPs/hostnames below with your actual ones throughout.
1. Prerequisites — run on ALL three servers
1.1 Set hostnames and /etc/hosts
# On each server, set its own hostname:
sudo hostnamectl set-hostname ndb1 # ndb2 / ndb3 on the other two
# On ALL three servers, add entries for all three nodes:
sudo tee -a /etc/hosts <<'EOF'
10.0.0.11 ndb1
10.0.0.12 ndb2
10.0.0.13 ndb3
EOF1.2 Update OS and install basics
sudo apt update && sudo apt upgrade -y
sudo apt install -y wget gnupg lsb-release chrony curl1.3 Time sync (important — clusters are sensitive to clock drift)
sudo systemctl enable --now chrony
chronyc tracking # confirm it's syncing1.4 Swap and memory tuning
With only 4GB RAM, avoid the kernel aggressively swapping out NDB's in-memory data:
sudo sysctl -w vm.swappiness=1
echo 'vm.swappiness=1' | sudo tee -a /etc/sysctl.confIf you don't already have swap configured, add a small swap file as a safety net (not for NDB data, just OS headroom):
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab1.5 Firewall — open the required ports
# Run on all 3 servers, allowing traffic between the cluster nodes only:
sudo ufw allow from 10.0.0.0/24 to any port 1186 proto tcp # ndb_mgmd
sudo ufw allow from 10.0.0.0/24 to any port 2202 proto tcp # data node comms
sudo ufw allow from 10.0.0.0/24 to any port 3306 proto tcp # mysqld (SQL)
sudo ufw allow from 10.0.0.0/24 to any port 33060 proto tcp # mysqlx (optional)
sudo ufw reloadAdjust 10.0.0.0/24 to your actual subnet. If your app servers live elsewhere and need direct SQL access, allow port 3306 from those too.
2. Install MySQL Cluster packages
Use Oracle's official APT config tool — it lets you pick the MySQL Cluster repo channel interactively. Run this on all three servers.
cd /tmp
wget https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.33-1_all.debWhen the dialog appears: select "Ubuntu Bionic/Focal/Jammy..." → "MySQL Cluster", then choose the latest 8.0 / 8.4 (LTS) Cluster release. If the package version above is stale by the time you read this, grab the current
.deblink from https://dev.mysql.com/downloads/repo/apt/.
sudo apt update<details> <summary><strong>Fix: Expired/invalid MySQL GPG key</strong></summary>⚠️ GPG key error after
apt update? MySQL's APT repo key expires periodically. If you see:EXPKEYSIG B7B3B788A8D3785C MySQL Release Engineeringfollow the fix below before continuing.
Step 1 — Remove the old key and fetch the current one:
sudo apt-key del B7B3B788A8D3785C
# Option A: from keyserver (may be slow in some regions)
sudo gpg --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
sudo gpg --export B7B3B788A8D3785C | sudo tee /usr/share/keyrings/mysql.gpg > /dev/null
# Option B: direct download from MySQL CDN (more reliable)
curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | \
sudo gpg --dearmor -o /usr/share/keyrings/mysql.gpgStep 2 — Rewrite /etc/apt/sources.list.d/mysql.list cleanly.
The sed-based approach can corrupt the file if run more than once. Always overwrite the whole file:
sudo tee /etc/apt/sources.list.d/mysql.list <<'EOF'
### THIS FILE IS AUTOMATICALLY CONFIGURED ###
# You may comment out entries below, but any other modifications may be lost.
# Use command 'dpkg-reconfigure mysql-apt-config' as root for modifications.
deb [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian bookworm mysql-cluster-8.0
deb [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian bookworm mysql-tools
deb [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian bookworm mysql-tools-preview
deb-src [signed-by=/usr/share/keyrings/mysql.gpg] http://repo.mysql.com/apt/debian bookworm mysql-cluster-8.0
EOFIf mysql-tools-preview gives a 404, comment it out:
sudo sed -i 's|^deb \[signed-by.*mysql-tools-preview|# &|' /etc/apt/sources.list.d/mysql.listStep 3 — Re-run update:
sudo apt update2.1 On Server 1 (management node)
sudo apt install -y mysql-cluster-community-management-server2.2 On Server 2 and Server 3 (data nodes)
sudo apt install -y mysql-cluster-community-data-node2.3 On ALL three servers (SQL node)
sudo apt install -y mysql-cluster-community-server mysql-cluster-community-clientThis will prompt you to set a root password for mysqld — set one, you'll need it.
3. Configure the management node (Server 1)
Create the config directory and the cluster config file:
sudo mkdir -p /var/lib/mysql-cluster
sudo nano /var/lib/mysql-cluster/config.ini[ndbd default]
NoOfReplicas=2
DataMemory=1536M
IndexMemory=256M
ServerPort=2202
[tcp default]
SendBufferMemory=2M
ReceiveBufferMemory=2M
[ndb_mgmd]
NodeId=1
HostName=10.0.0.11
DataDir=/var/lib/mysql-cluster
[ndbd]
NodeId=2
HostName=10.0.0.12
DataDir=/usr/local/mysql/data
[ndbd]
NodeId=3
HostName=10.0.0.13
DataDir=/usr/local/mysql/data
[mysqld]
NodeId=4
HostName=10.0.0.11
[mysqld]
NodeId=5
HostName=10.0.0.12
[mysqld]
NodeId=6
HostName=10.0.0.13Notes on the values:
DataMemory=1536M+IndexMemory=256Mleaves roughly ~2GB of headroom on each 4GB data-node box for the OS and the co-locatedmysqldprocess. Don't push this much higher without adding RAM, or you risk OOM kills.- The three blank
[mysqld]sections are "slots" reserving NodeIds for SQL nodes — they don't need a config file of their own here, that's handled per-host in step 5.
Create the data node directories on Server 2 and Server 3 now (run on each):
sudo mkdir -p /usr/local/mysql/data4. Start the management node (Server 1 only)
sudo ndb_mgmd -f /var/lib/mysql-cluster/config.ini --configdir=/var/lib/mysql-cluster --initialCheck it's listening:
sudo ss -ltnp | grep 1186To make this persistent across reboots, create a systemd unit:
sudo tee /etc/systemd/system/ndb_mgmd.service <<'EOF'
[Unit]
Description=MySQL NDB Cluster Management Server
After=network.target
[Service]
Type=forking
ExecStart=/usr/sbin/ndb_mgmd -f /var/lib/mysql-cluster/config.ini --configdir=/var/lib/mysql-cluster
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable ndb_mgmd(Run the manual ndb_mgmd ... --initial command above the first time only — --initial rewrites the management config cache. After that, use systemctl start ndb_mgmd.)
5. Configure and start the data nodes (Server 2 and Server 3)
On each data node server, edit /etc/mysql/my.cnf (or add a file under /etc/mysql/conf.d/):
[mysql_cluster]
ndb-connectstring=10.0.0.11Start the data node for the first time with --initial (this formats the data files — only use --initial on first start or when you intentionally want to wipe local data):
sudo ndbd --initialWatch the management node console (next section) to confirm both data nodes connect and reach "started" status before moving on — this can take anywhere from a few seconds to a couple minutes.
Set up systemd for future starts (without --initial):
sudo tee /etc/systemd/system/ndbd.service <<'EOF'
[Unit]
Description=MySQL NDB Data Node
After=network.target
[Service]
Type=forking
ExecStart=/usr/sbin/ndbd
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable ndbdWith 2 logical cores,
ndbd(single-threaded) is actually fine —ndbmtd(multi-threaded) only pays off with more cores. Stick withndbdhere.
6. Configure and start the SQL nodes (all three servers)
On each of the three servers, edit /etc/mysql/my.cnf:
[mysqld]
ndbcluster
ndb-connectstring=10.0.0.11
innodb_buffer_pool_size=128M
key_buffer_size=32M
max_connections=150
bind-address=0.0.0.0
[mysql_cluster]
ndb-connectstring=10.0.0.11innodb_buffer_pool_size is kept small deliberately — your actual cluster data lives in NDB's own memory (DataMemory), not InnoDB. InnoDB here is only used for MySQL system tables.
Start/restart mysqld:
sudo systemctl restart mysql
sudo systemctl enable mysqlSecure the installation (run once, e.g. on Server 1):
sudo mysql_secure_installation7. Verify the cluster is up
From the management node:
sudo ndb_mgm -e showYou want output like:
Connected to Management Server at: 10.0.0.11:1186
Cluster Configuration
---------------------
[ndbd(NDB)] 2 node(s)
id=2 @10.0.0.12 (mysql-8.x, Nodegroup: 0, *)
id=3 @10.0.0.13 (mysql-8.x, Nodegroup: 0)
[ndb_mgmd(MGM)] 1 node(s)
id=1 @10.0.0.11 (mysql-8.x)
[mysqld(API)] 3 node(s)
id=4 @10.0.0.11 (mysql-8.x)
id=5 @10.0.0.12 (mysql-8.x)
id=6 @10.0.0.13 (mysql-8.x)All nodes should show as connected (not blank/missing). If a mysqld API node shows missing, that SQL node's mysqld either isn't running or isn't pointed at the right ndb-connectstring.
8. Testing
8.1 Create an NDB table and confirm cross-node replication
On Server 1:
mysql -u root -p
CREATE DATABASE clustertest;
USE clustertest;
CREATE TABLE items (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=NDBCLUSTER;
INSERT INTO items (name) VALUES ('widget'), ('gadget'), ('gizmo');Important: tables must use
ENGINE=NDBCLUSTER(orENGINE=NDB) to be distributed across the cluster. A plainInnoDBtable will only exist on that one mysqld instance, not the cluster.
Now from Server 2 or Server 3, query the same table — you should see the same rows instantly, with no manual replication step:
mysql -u root -p -h 10.0.0.12 -e "SELECT * FROM clustertest.items;"8.2 Failover test
While running a continuous read loop from one SQL node:
watch -n1 'mysql -u root -p"YOURPASS" -e "SELECT COUNT(*) FROM clustertest.items;"'On Server 3, kill the data node to simulate a hardware failure:
sudo systemctl stop ndbdYour SELECT should keep working without interruption (served from the surviving data node), confirming redundancy. Check status:
sudo ndb_mgm -e showYou should see node 3 marked not connected, node 2 still up. Bring it back:
sudo systemctl start ndbd(No --initial here — that flag wipes local data; the node will resync automatically from its peer.)
8.3 Basic load test (optional)
If you want a quick throughput sanity check:
sudo apt install -y sysbench
sysbench oltp_read_write \
--mysql-host=10.0.0.11 --mysql-user=root --mysql-password=YOURPASS \
--mysql-db=clustertest --tables=4 --table-size=10000 \
--threads=2 prepare
sysbench oltp_read_write \
--mysql-host=10.0.0.11 --mysql-user=root --mysql-password=YOURPASS \
--mysql-db=clustertest --tables=4 --table-size=10000 \
--threads=2 --time=30 --report-interval=5 runGiven the 2-vCPU/4GB spec, expect modest numbers — this confirms things work end-to-end, not raw performance.
9. Troubleshooting
Quick diagnostic commands
sudo ndb_mgm -e show # overall cluster + node status
sudo ndb_mgm -e "all status" # node startup phase detail
sudo ndb_mgm -e "all report memoryusage" # DataMemory/IndexMemory usage %Log file locations
| Node | Location |
|---|---|
| Management node | /var/lib/mysql-cluster/ndb_1_cluster.log (cluster-wide events) |
| Data node | /usr/local/mysql/data/ndb_<id>_out.log and ndb_<id>_error.log |
| SQL node | /var/log/mysql/error.log (path may vary by package) |
Common issues
EXPKEYSIG B7B3B788A8D3785C / "repository is not signed" on apt update
MySQL's GPG key expires periodically. Do not use sed to patch the .list file — if it runs more than once it corrupts the entries. Instead: fetch the new key (via keyserver.ubuntu.com or directly from https://repo.mysql.com/RPM-GPG-KEY-mysql-2023), export it to /usr/share/keyrings/mysql.gpg, then overwrite /etc/apt/sources.list.d/mysql.list in full using tee. See the collapsible fix block in section 2 above.
Malformed entry in /etc/apt/sources.list.d/mysql.list / "unparsable [option]"
The .list file was corrupted (usually by a sed command running multiple times). The symptom looks like lines truncated mid-way: deb [signed-by=/usr/share/keyrings/mysql.gpg with no closing ]. Fix: overwrite the entire file with sudo tee /etc/apt/sources.list.d/mysql.list <<'EOF' ... EOF — never try to patch it with sed again.
Data node won't reach "started" / stuck in a startup phase
Usually a connectivity or NodeId/HostName mismatch in config.ini. Double-check /etc/hosts resolves correctly on all 3 boxes and that the firewall allows ports 1186/2202 between cluster IPs.
Could not connect to socket / "Unable to connect with connect string"
The ndb-connectstring in my.cnf doesn't match the management node's IP/port, or ndb_mgmd isn't running. Confirm with sudo systemctl status ndb_mgmd and ss -ltnp | grep 1186 on Server 1.
Data node crashes with "out of DataMemory" / Error 773 / 827
Your dataset exceeded DataMemory. Check usage with ndb_mgm -e "all report memoryusage". With 4GB boxes you have limited headroom — either trim the dataset, add RAM, or as a stopgap raise DataMemory slightly (watch for OOM from the OS side if you do).
Server killed by OOM killer (dmesg shows Out of memory: Killed process ...ndbd)
DataMemory + IndexMemory + OS + co-located mysqld exceeded 4GB. Lower DataMemory/IndexMemory in config.ini, restart with --initial (data node only, after backing up if it's not a fresh test), or reduce innodb_buffer_pool_size on the co-located SQL node.
Table writes succeed on one SQL node but SELECT fails on another with "Table doesn't exist"
The table was likely created as InnoDB instead of NDBCLUSTER. Confirm engine: SHOW CREATE TABLE clustertest.items;. Recreate with ENGINE=NDBCLUSTER.
NoOfReplicas confusion / cluster won't form
NoOfReplicas=2 with exactly 2 data nodes means 1 replica copy per node (i.e., each node holds the full dataset, single redundancy). Don't set NoOfReplicas=3 with only 2 data nodes — the math won't work and the cluster will refuse to start.
Clock drift causing odd timeouts
Re-check chronyc tracking on all nodes — NDB heartbeats are timing-sensitive.
After a reboot, things don't come back
Confirm ndb_mgmd, ndbd, and mysql services are all enabled (systemd), and that they start in the right order: management node → data nodes → SQL nodes. If a data node starts before ndb_mgmd is reachable, it will retry/wait, which is normal — give it a minute.
Useful one-liners
# Cluster-wide table list as NDB sees them:
ndb_show_tables -c 10.0.0.11
# Force a data node restart (not initial — preserves data):
sudo systemctl restart ndbd
# Check which SQL node version each mysqld reports:
mysql -h <ip> -u root -p -e "SHOW VARIABLES LIKE 'version%';"10. A few honest caveats given your hardware
- 2 data nodes is the minimum, not the ideal. It gives you redundancy (survive 1 node failure) but no horizontal scaling — you can't add data nodes later without re-partitioning (
NoOfReplicasand node groups). If this cluster needs to grow, plan for 4 data nodes (2 node groups) down the line. - 4GB RAM is genuinely small for NDB. This setup is solid for development, staging, or light production with a modest dataset (low GB range). If your real dataset is large, budget more RAM before going to production.
- Co-locating
ndb_mgmdwith a data node (as done here on Server 1, alongside amysqld) is fine — the management process is lightweight and mostly idle after the cluster is formed.