MrSloth Labs
Database 13 min readJuly 30, 2026

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.

MySQLNDB ClusterUbuntuDebianDatabase

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

ServerRole(s)Hostname (example)IP (example)
Server 1Management node (ndb_mgmd) + SQL node (mysqld)ndb110.0.0.11
Server 2Data node (ndbd) + SQL node (mysqld)ndb210.0.0.12
Server 3Data node (ndbd) + SQL node (mysqld)ndb310.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
EOF

1.2 Update OS and install basics

sudo apt update && sudo apt upgrade -y
sudo apt install -y wget gnupg lsb-release chrony curl

1.3 Time sync (important — clusters are sensitive to clock drift)

sudo systemctl enable --now chrony
chronyc tracking   # confirm it's syncing

1.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.conf

If 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/fstab

1.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 reload

Adjust 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.deb

When 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 .deb link from https://dev.mysql.com/downloads/repo/apt/.

sudo apt update

⚠️ GPG key error after apt update? MySQL's APT repo key expires periodically. If you see: EXPKEYSIG B7B3B788A8D3785C MySQL Release Engineering follow the fix below before continuing.

<details> <summary><strong>Fix: Expired/invalid MySQL GPG key</strong></summary>

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.gpg

Step 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
EOF

If 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.list

Step 3 — Re-run update:

sudo apt update
</details>

2.1 On Server 1 (management node)

sudo apt install -y mysql-cluster-community-management-server

2.2 On Server 2 and Server 3 (data nodes)

sudo apt install -y mysql-cluster-community-data-node

2.3 On ALL three servers (SQL node)

sudo apt install -y mysql-cluster-community-server mysql-cluster-community-client

This 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.13

Notes on the values:

  • DataMemory=1536M + IndexMemory=256M leaves roughly ~2GB of headroom on each 4GB data-node box for the OS and the co-located mysqld process. 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/data

4. Start the management node (Server 1 only)

sudo ndb_mgmd -f /var/lib/mysql-cluster/config.ini --configdir=/var/lib/mysql-cluster --initial

Check it's listening:

sudo ss -ltnp | grep 1186

To 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.11

Start 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 --initial

Watch 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 ndbd

With 2 logical cores, ndbd (single-threaded) is actually fine — ndbmtd (multi-threaded) only pays off with more cores. Stick with ndbd here.


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.11

innodb_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 mysql

Secure the installation (run once, e.g. on Server 1):

sudo mysql_secure_installation

7. Verify the cluster is up

From the management node:

sudo ndb_mgm -e show

You 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 (or ENGINE=NDB) to be distributed across the cluster. A plain InnoDB table 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 ndbd

Your SELECT should keep working without interruption (served from the surviving data node), confirming redundancy. Check status:

sudo ndb_mgm -e show

You 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 run

Given 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

NodeLocation
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 (NoOfReplicas and 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_mgmd with a data node (as done here on Server 1, alongside a mysqld) is fine — the management process is lightweight and mostly idle after the cluster is formed.