Vitess is a clustering system for MySQL, originally built at YouTube, that splits a database into shards across many MySQL servers while applications keep talking to what looks like a single MySQL database. It handles query routing, connection pooling, failover and live resharding. In this tutorial you will run the official Vitess local example on a single Ubuntu 24.04 server, learn what each component does, move tables into their own keyspace and then split that keyspace into two shards while it stays online.
This is a learning and evaluation setup: every component runs on one machine. Production Vitess clusters normally run on Kubernetes with the Vitess Operator, which uses the same concepts and commands shown here.
Prerequisites
To follow this guide you need:
- A dedicated test server running Ubuntu 24.04 LTS with at least 4 GB of RAM and 2 vCPUs, for example a CubePath VPS. The example starts around a dozen MySQL instances.
- A non-root user with
sudoprivileges. - Basic knowledge of MySQL and SQL.
WarningStep 2 stops the system MySQL service and removes the AppArmor profile for
mysqld, as the Vitess documentation requires for the local example. Do not run this guide on a server that hosts a real MySQL database.
Vitess architecture in brief
Before running anything, it helps to know the pieces the example starts:
| Component | Role |
|---|---|
| Keyspace | A logical database. It can be unsharded (one shard) or split into several shards. |
| Shard | A subset of a keyspace's rows, stored in one MySQL primary and its replicas. Shards are named by key range, such as -80 and 80-. |
| VTTablet | A process in front of every MySQL instance that manages replication, pools connections and enforces query rules. A tablet is a VTTablet plus its mysqld. |
| VTGate | The stateless proxy applications connect to using the MySQL protocol. It parses each query and routes it to the right shards. |
| Topology service | A key-value store (etcd in this example) holding which keyspaces, shards and tablets exist. |
| vtctld / vtctldclient | The control server and its CLI, used to create keyspaces, apply schemas and run workflows. |
| VTOrc | Detects a failed primary and promotes a replica automatically. |
| VSchema | Per-keyspace JSON that tells VTGate how rows are distributed, through a vindex (for example a hash of customer_id). |
Step 1 - Installing dependencies
The example needs MySQL server binaries, etcd and a MySQL client. Install them from the Ubuntu repositories:
sudo apt update
sudo apt install -y mysql-server etcd-server etcd-client curl git
The example also starts VTAdmin, the Vitess web UI, which needs a current Node.js. Ubuntu 24.04 ships Node.js 18, so add the NodeSource repository for Node.js 22:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y nodejs
Verify the versions:
mysqld --version
etcd --version | head -n 1
node --version
/usr/sbin/mysqld Ver 8.0.43-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))
etcd Version: 3.4.30
v22.20.0
Step 2 - Preparing the system services
Vitess starts its own mysqld and etcd processes with custom data directories under your home directory, so the packaged services must not run. Stop and disable them:
sudo systemctl disable --now mysql etcd
The Ubuntu AppArmor profile for mysqld only allows the standard data paths and blocks the MySQL instances Vitess launches. Disable that profile:
sudo ln -s /etc/apparmor.d/usr.sbin.mysqld /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.mysqld
Confirm that nothing is listening on the MySQL and etcd ports:
sudo ss -tlnp | grep -E ':(3306|2379)\b' || echo "ports free"
ports free
Step 3 - Installing Vitess
Vitess publishes prebuilt Linux binaries on GitHub. The asset names include a commit hash, so list the files of the latest release first:
curl -s https://api.github.com/repos/vitessio/vitess/releases/latest | grep -E '"(tag_name|browser_download_url)"'
Note the tag_name (for example v22.0.1) and copy the URL of the Linux .tar.gz for your architecture. Download and extract it into ~/vitess, replacing the URL with the one you copied:
cd ~
curl -fLO https://github.com/vitessio/vitess/releases/download/v22.0.1/vitess-22.0.1-xxxxxxx-linux-amd64.tar.gz
mkdir -p ~/vitess
tar -xzf vitess-*.tar.gz -C ~/vitess --strip-components=1
Add the binaries to your PATH permanently:
echo 'export PATH="$HOME/vitess/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Verify the installation:
vtgate --version
vtgate version Version: 22.0.1 (Git revision ... branch 'HEAD') built on ... by runner@... using go1.24.x linux/amd64
The example scripts live in the Vitess source repository. Clone the tag that matches your binaries so the scripts and binaries agree, replacing v22.0.1 with your tag_name:
git clone --depth 1 --branch v22.0.1 https://github.com/vitessio/vitess.git ~/vitess-src
cd ~/vitess-src/examples/local
ls
The directory contains numbered scripts (101_initial_cluster.sh, 201_customer_tablets.sh and so on) that you will run in order, plus the SQL and VSchema files they use.
Step 4 - Starting an unsharded cluster
The first script starts etcd, vtctld, three tablets for a keyspace named commerce (one primary and two replicas), VTOrc, VTGate and VTAdmin, and creates the tables product, customer and corder:
./101_initial_cluster.sh
It takes a minute or two. Load the shell helpers, which set aliases so mysql connects to VTGate on port 15306 and vtctldclient talks to vtctld on port 15999:
source ../common/env.sh
List the tablets:
vtctldclient GetTablets
zone1-0000000100 commerce 0 primary localhost:15100 localhost:17100 [] 2026-09-25T10:14:02Z
zone1-0000000101 commerce 0 replica localhost:15101 localhost:17101 [] <null>
zone1-0000000102 commerce 0 rdonly localhost:15102 localhost:17102 [] <null>
Insert sample data through VTGate and read it back:
mysql < ../common/insert_commerce_data.sql
mysql --table < ../common/select_commerce_data.sql
Using commerce
Customer
+-------------+--------------------+
| customer_id | email |
+-------------+--------------------+
| 1 | [email protected] |
| 2 | [email protected] |
...
Your application would connect exactly like this: to VTGate, with a normal MySQL client or driver, on port 15306.
Step 5 - Moving tables to their own keyspace
A common first step in scaling is vertical splitting: moving busy tables to their own keyspace on separate MySQL servers. Here customer and corder move from commerce to a new customer keyspace.
Start the tablets for the new keyspace:
./201_customer_tablets.sh
Start a MoveTables workflow. It copies the existing rows and then keeps the target in sync by streaming the source binary logs (VReplication):
./202_move_tables.sh
Switch read traffic, then write traffic, to the new keyspace. Until writes are switched you can reverse the operation, and even after that Vitess keeps a reverse workflow so you can go back:
./203_switch_reads.sh
./204_switch_writes.sh
Check that the tables are now served from the customer keyspace:
mysql --table -e "use customer; select * from customer;"
Finally remove the old copies and routing rules from commerce:
./205_clean_commerce.sh
Each script is a few lines long. Read them with cat to see the underlying vtctldclient MoveTables commands you would use in your own cluster.
Step 6 - Defining a sharded VSchema
To shard customer, VTGate needs to know how to map each row to a shard. That is the job of the VSchema. Look at the one the example applies:
cat vschema_customer_sharded.json
Its core looks like this:
{
"sharded": true,
"vindexes": {
"hash": { "type": "hash" }
},
"tables": {
"customer": {
"column_vindexes": [{ "column": "customer_id", "name": "hash" }]
},
"corder": {
"column_vindexes": [{ "column": "customer_id", "name": "hash" }]
}
}
}
The hash vindex turns customer_id into a 64-bit keyspace ID, and each shard owns a range of those IDs: shard -80 holds IDs below 0x80... and 80- holds the rest. Because both tables use customer_id, a customer and all their orders always land on the same shard, so joins between them never cross shards.
Choosing the sharding key is the most important design decision in Vitess: pick a column present in most queries and with many distinct values. Auto-increment IDs no longer work across shards, so the example also creates sequence tables in the unsharded commerce keyspace to generate unique IDs.
Apply the VSchema and sequences:
./301_customer_sharded.sh
Step 7 - Resharding into two shards
Start the tablets for the two new shards, -80 and 80-:
./302_new_shards.sh
Start the Reshard workflow. Like MoveTables, it copies the data of shard 0 into the new shards, splitting rows by keyspace ID, and keeps them in sync:
./303_reshard.sh
Switch reads and then writes to the new shards:
./304_switch_reads.sh
./305_switch_writes.sh
Query each shard directly by adding the shard to the keyspace name. The rows are divided between them:
mysql --table -e "use customer:-80; select * from customer;"
mysql --table -e "use customer:80-; select * from customer;"
A query without a shard target still returns every row, because VTGate sends it to both shards and merges the results:
mysql --table -e "use customer; select * from customer;"
Remove the old unsharded shard once traffic runs on the new ones:
./306_down_shard_0.sh
Throughout the whole process the customer keyspace stayed available; the only interruption is a brief pause for in-flight writes during the write switch.
Step 8 - Exploring and cleaning up
VTAdmin shows keyspaces, shards, tablets, schemas and workflows. It listens on port 14201 on localhost; from your workstation, open an SSH tunnel and browse to http://localhost:14201:
ssh -L 14201:localhost:14201 -L 14200:localhost:14200 your_user@your_server_ip
VTGate also answers Vitess-specific statements, for example:
mysql -e "show vitess_tablets;"
When you are done, stop every process and delete the data with the teardown script in the same directory (its number differs between versions, so look it up with ls *teardown*):
./$(ls *teardown*.sh)
Troubleshooting
101_initial_cluster.sh fails with mysqld permission errors: the AppArmor profile is still loaded. Check with sudo aa-status | grep mysqld and repeat Step 2.
address already in use on 2379 or 3306: the packaged etcd or mysql service is running. Run sudo systemctl disable --now mysql etcd and start again after the teardown script.
A script fails halfway: run the teardown script, check the logs under ~/vitess-src/examples/local/vtdataroot/tmp/ (for example vttablet.out and vtgate.out), fix the cause and start from 101_initial_cluster.sh again.
command not found: vtctldclient: the PATH change was not loaded in this shell. Run source ~/.bashrc and source ../common/env.sh.
Conclusion
You ran a complete Vitess cluster, routed MySQL traffic through VTGate, moved tables to a new keyspace with MoveTables and split that keyspace into two shards with Reshard while it stayed online. For a real deployment, the next steps are the Vitess Operator on Kubernetes, a three-node etcd cluster for the topology, scheduled backups of each shard, and testing your application's queries against a sharded VSchema before you migrate production data.
