CockroachDB is a distributed SQL database that speaks the PostgreSQL wire protocol. It splits data into ranges, keeps three replicas of each range on different nodes by default and uses the Raft consensus protocol to keep them consistent, so the cluster keeps serving reads and writes when a node fails. In this tutorial you will deploy a secure three-node CockroachDB cluster on Ubuntu 24.04 with TLS certificates and systemd, run SQL against it, watch it survive a node failure and take a backup.
Prerequisites
To follow this tutorial, you will need:
- Three servers running Ubuntu 24.04 LTS on x86_64, for example three CubePath VPS, each with at least 2 vCPUs and 4 GB of RAM. Production clusters should have at least 4 vCPUs and 8 GB of RAM per node.
- A non-root user with
sudoprivileges on every server, and SSH access fromnode1to the other two. - A private network between the servers. This guide uses these addresses; replace them with yours:
| Hostname | Private IP |
|---|---|
node1 | 10.0.0.11 |
node2 | 10.0.0.12 |
node3 | 10.0.0.13 |
ImportantCockroachDB is distributed under the CockroachDB Software License. Depending on your organization and how you use it, a cluster may require a license key (there is a free tier for smaller companies). Check the current terms on the Cockroach Labs website before running it in production.
Unless a step says otherwise, run each command on all three servers.
Step 1 - Preparing the servers
CockroachDB rejects operations when node clocks drift too far apart (500 ms by default), and a node whose clock is badly off shuts itself down. Ubuntu keeps time with systemd-timesyncd. Check that synchronization is active:
timedatectl
Local time: Fri 2026-09-25 10:15:02 UTC
Universal time: Fri 2026-09-25 10:15:02 UTC
RTC time: Fri 2026-09-25 10:15:02
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
If System clock synchronized shows no, enable it with sudo timedatectl set-ntp true and check again after a minute.
Nodes communicate on port 26257 (SQL and inter-node traffic) and serve the DB Console on port 8080. Allow both from the private subnet:
sudo ufw allow OpenSSH
sudo ufw allow from 10.0.0.0/24 proto tcp to any port 26257,8080
sudo ufw enable
Create a system user and the data directory:
sudo useradd --system --home-dir /var/lib/cockroach --shell /usr/sbin/nologin cockroach
sudo mkdir -p /var/lib/cockroach/certs
sudo chown -R cockroach:cockroach /var/lib/cockroach
sudo chmod 700 /var/lib/cockroach/certs
Step 2 - Installing the CockroachDB binary
Cockroach Labs distributes CockroachDB as a single binary plus optional libraries for spatial features. Find the current version number on the releases page and set it in a variable. This guide uses v25.2.0 as an example:
COCKROACH_VERSION=v25.2.0
curl -fsSLO "https://binaries.cockroachdb.com/cockroach-${COCKROACH_VERSION}.linux-amd64.tgz"
tar -xzf "cockroach-${COCKROACH_VERSION}.linux-amd64.tgz"
Install the binary and the GEOS libraries in the locations CockroachDB searches by default:
sudo install -m 0755 "cockroach-${COCKROACH_VERSION}.linux-amd64/cockroach" /usr/local/bin/
sudo mkdir -p /usr/local/lib/cockroach
sudo cp "cockroach-${COCKROACH_VERSION}.linux-amd64/lib/"* /usr/local/lib/cockroach/
Confirm the version:
cockroach version
Build Tag: v25.2.0
Build Time: ...
Distribution: CCL
Platform: linux amd64 (x86_64-pc-linux-gnu)
Step 3 - Creating the TLS certificates
A secure cluster uses TLS for all traffic between nodes and clients, and authenticates nodes and the root user with certificates signed by your own certificate authority (CA). You will create all certificates on node1 and copy them to the other nodes. Run the commands in this step on node1 only.
Create a directory for the certificates and a separate one for the CA private key. Keep the CA key out of the certs directory: anyone who has it can create valid certificates for your cluster.
mkdir -p "$HOME/certs" "$HOME/cockroach-ca"
chmod 700 "$HOME/cockroach-ca"
cockroach cert create-ca --certs-dir="$HOME/certs" --ca-key="$HOME/cockroach-ca/ca.key"
Create a client certificate for the root SQL user. You will use it to run administrative commands from node1:
cockroach cert create-client root --certs-dir="$HOME/certs" --ca-key="$HOME/cockroach-ca/ca.key"
Each node needs its own certificate, valid for the addresses clients use to reach it. Create one per node in a separate directory, starting from a copy of the CA certificate:
for n in 1 2 3; do
mkdir -p "$HOME/node${n}-certs"
cp "$HOME/certs/ca.crt" "$HOME/node${n}-certs/"
cockroach cert create-node "10.0.0.1${n}" "node${n}" localhost 127.0.0.1 \
--certs-dir="$HOME/node${n}-certs" --ca-key="$HOME/cockroach-ca/ca.key"
done
Each directory now contains ca.crt, node.crt and node.key:
ls "$HOME/node2-certs"
ca.crt node.crt node.key
Install the certificates for node1 locally:
sudo cp "$HOME/node1-certs/"* /var/lib/cockroach/certs/
Copy the other nodes' certificates to their home directories, replacing your_user with your user name:
scp -r "$HOME/node2-certs" [email protected]:
scp -r "$HOME/node3-certs" [email protected]:
On node2, move them into place (on node3, use node3-certs), then remove the copy:
sudo cp "$HOME/node2-certs/"* /var/lib/cockroach/certs/
rm -rf "$HOME/node2-certs"
Finally, on all three nodes, give the files to the cockroach user. CockroachDB refuses to start if a private key is readable by other users:
sudo chown -R cockroach:cockroach /var/lib/cockroach/certs
sudo chmod 600 /var/lib/cockroach/certs/node.key
Step 4 - Running CockroachDB as a systemd service
Create a unit file on each node:
sudo nano /etc/systemd/system/cockroach.service
[Unit]
Description=CockroachDB node
Wants=network-online.target
After=network-online.target
[Service]
Type=notify
User=cockroach
WorkingDirectory=/var/lib/cockroach
ExecStart=/usr/local/bin/cockroach start \
--certs-dir=/var/lib/cockroach/certs \
--store=/var/lib/cockroach/data \
--advertise-addr=10.0.0.11 \
--join=10.0.0.11,10.0.0.12,10.0.0.13 \
--cache=.25 \
--max-sql-memory=.25
TimeoutStopSec=300
Restart=always
RestartSec=10
LimitNOFILE=35000
[Install]
WantedBy=multi-user.target
The flags that matter:
--advertise-addris the address other nodes use to reach this one. Change it to10.0.0.12onnode2and10.0.0.13onnode3.--joinlists the nodes to contact at startup and is the same on all of them.--cacheand--max-sql-memoryeach reserve 25% of RAM, the recommended values for a dedicated server.Type=notifylets systemd know when the node is ready to serve.
Load the unit and start the service on all three nodes:
sudo systemctl daemon-reload
sudo systemctl enable --now cockroach
The nodes start, but they wait for the cluster to be initialized. The log shows a message like this until you do so:
sudo journalctl -u cockroach -n 5 --no-pager
... initial startup completed.
... Node will now attempt to join a running cluster, or wait for `cockroach init`.
Step 5 - Initializing the cluster
Initialization happens once, from any machine that has the root client certificate. On node1:
cockroach init --certs-dir="$HOME/certs" --host=10.0.0.11
Cluster successfully initialized
Check that all three nodes joined and are live:
cockroach node status --certs-dir="$HOME/certs" --host=10.0.0.11
id | address | sql_address | build | started_at | updated_at | locality | is_available | is_live
-----+-----------------+-----------------+---------+----------------------------+----------------------------+----------+--------------+----------
1 | 10.0.0.11:26257 | 10.0.0.11:26257 | v25.2.0 | 2026-09-25 10:20:11.52 UTC | 2026-09-25 10:21:02.08 UTC | | true | true
2 | 10.0.0.12:26257 | 10.0.0.12:26257 | v25.2.0 | 2026-09-25 10:20:12.03 UTC | 2026-09-25 10:21:02.61 UTC | | true | true
3 | 10.0.0.13:26257 | 10.0.0.13:26257 | v25.2.0 | 2026-09-25 10:20:12.40 UTC | 2026-09-25 10:21:02.97 UTC | | true | true
(3 rows)
Step 6 - Creating a database and a SQL user
Open a SQL shell as root:
cockroach sql --certs-dir="$HOME/certs" --host=10.0.0.11
Create a database, a table and some rows. The syntax is PostgreSQL-compatible:
CREATE DATABASE bank;
CREATE TABLE bank.accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner STRING NOT NULL,
balance DECIMAL(12,2) NOT NULL DEFAULT 0
);
INSERT INTO bank.accounts (owner, balance) VALUES ('alice', 1000), ('bob', 250);
Use UUIDs rather than sequential integers as primary keys. Sequential keys send all new rows to the same range, and therefore to the same node, while random UUIDs spread writes across the cluster.
Create a user for applications, with a password so it can connect without a client certificate. Replace your_strong_password:
CREATE USER app_user WITH PASSWORD 'your_strong_password';
GRANT ALL ON DATABASE bank TO app_user;
GRANT ALL ON TABLE bank.accounts TO app_user;
Create an administrator with a password to log in to the DB Console:
CREATE USER dbadmin WITH PASSWORD 'your_admin_password';
GRANT admin TO dbadmin;
Type \q to leave the shell. Applications connect with any PostgreSQL driver using a connection string like:
postgresql://app_user:[email protected]:26257/bank?sslmode=verify-full&sslrootcert=/path/to/ca.crt
Copy ca.crt (not the CA key) to the application servers so they can verify the nodes' certificates.
Step 7 - Verifying replication and surviving a node failure
Each range of data has three replicas, one on each node. Check the replication factor that applies to the new database:
cockroach sql --certs-dir="$HOME/certs" --host=10.0.0.11 -e "SHOW ZONE CONFIGURATION FROM DATABASE bank;"
target | raw_config_sql
----------------+-------------------------------------------
RANGE default | ALTER RANGE default CONFIGURE ZONE USING
| range_min_bytes = 134217728,
| range_max_bytes = 536870912,
| gc.ttlseconds = 14400,
| num_replicas = 3,
| constraints = '[]',
| lease_preferences = '[]'
(1 row)
With three replicas, a majority (two) must be available for a range to accept reads and writes, so the cluster tolerates the loss of one node. Test it by stopping node3:
sudo systemctl stop cockroach
On node1, the data is still readable and writable:
cockroach sql --certs-dir="$HOME/certs" --host=10.0.0.11 -e "UPDATE bank.accounts SET balance = balance + 50 WHERE owner = 'bob'; SELECT owner, balance FROM bank.accounts ORDER BY owner;"
owner | balance
--------+----------
alice | 1000.00
bob | 300.00
(2 rows)
cockroach node status now shows node3 with is_live set to false. Start it again on node3:
sudo systemctl start cockroach
The node rejoins and catches up on the changes it missed. If a node stays down for more than 5 minutes (the default server.time_until_store_dead), the cluster considers it dead and rebuilds its replicas on the remaining nodes, which requires at least three live nodes, so add a fourth node before you remove one for good.
Step 8 - Using the DB Console
Every node serves the DB Console on port 8080 over HTTPS. Because the port is only open to the private network, reach it through an SSH tunnel from your computer:
ssh -L 8080:10.0.0.11:8080 your_user@your_server_ip
Open https://localhost:8080 in your browser, accept the warning about the certificate signed by your own CA and log in as dbadmin. The console shows node health, SQL throughput, slow statements, range replication status and storage use per node.
Step 9 - Backing up and restoring a database
The BACKUP statement writes a consistent copy of a database to a storage location. For a quick test you can use nodelocal, which writes into the extern directory of a node's store. In production, point it at object storage such as an S3-compatible bucket, because a backup kept on a cluster node is lost with that node.
Back up the bank database to node 1:
cockroach sql --certs-dir="$HOME/certs" --host=10.0.0.11 -e "BACKUP DATABASE bank INTO 'nodelocal://1/backups';"
job_id | status | fraction_completed | rows | index_entries | bytes
---------------------+-----------+--------------------+------+---------------+--------
1012345678901234567 | succeeded | 1 | 2 | 0 | 104
(1 row)
To restore it, drop the database (only in a test) and restore the latest backup from that location:
cockroach sql --certs-dir="$HOME/certs" --host=10.0.0.11 -e "DROP DATABASE bank CASCADE; RESTORE DATABASE bank FROM LATEST IN 'nodelocal://1/backups';"
Check that the rows are back with SELECT * FROM bank.accounts;. For recurring backups, use CREATE SCHEDULE FOR BACKUP to let the cluster run them on a schedule.
Troubleshooting
- The node exits with
clock synchronization error: this node is more than 500ms away from at least half of the known nodes: fix time synchronization withtimedatectlas in Step 1, then start the service again. x509: certificate is valid for ..., not 10.0.0.12: the node certificate does not include the address used to connect. Recreate it withcockroach cert create-nodelisting that address, copy it to the node and restart the service.open /var/lib/cockroach/certs/node.key: permission deniedor a warning about key permissions: the files must belong tocockroachandnode.keymust have mode600.- Nodes start but never join: check that port 26257 is open between all nodes and that
--advertise-addron each node is its own private IP.sudo journalctl -u cockroach -n 50shows the connection errors.
Conclusion
You deployed a secure three-node CockroachDB cluster with TLS certificates and systemd, created a database and users, confirmed that it keeps working when a node is down and made a backup. As next steps, put a TCP load balancer such as HAProxy in front of the nodes (cockroach gen haproxy generates a configuration for it), schedule backups to object storage, and grow the cluster by starting more nodes with the same --join list.
