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 sudo privileges on every server, and SSH access from node1 to the other two.
  • A private network between the servers. This guide uses these addresses; replace them with yours:
HostnamePrivate IP
node110.0.0.11
node210.0.0.12
node310.0.0.13

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-addr is the address other nodes use to reach this one. Change it to 10.0.0.12 on node2 and 10.0.0.13 on node3.
  • --join lists the nodes to contact at startup and is the same on all of them.
  • --cache and --max-sql-memory each reserve 25% of RAM, the recommended values for a dedicated server.
  • Type=notify lets 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 with timedatectl as 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 with cockroach cert create-node listing that address, copy it to the node and restart the service.
  • open /var/lib/cockroach/certs/node.key: permission denied or a warning about key permissions: the files must belong to cockroach and node.key must have mode 600.
  • Nodes start but never join: check that port 26257 is open between all nodes and that --advertise-addr on each node is its own private IP. sudo journalctl -u cockroach -n 50 shows 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.