Quick Start (Docker Compose)¶
This guide brings up a small VaireDB cluster — one coordinator and five core
nodes — using Docker Compose, then walks you through your first table and
queries. It mirrors the cluster used by the project's end-to-end tests
(tests/e2e/docker-compose.yml).
Prerequisites
You need Docker and Docker Compose, plus a PostgreSQL client such as
psql. First build the images with make docker (see
Installation).
1. Build the images¶
The Compose file references the locally built vairedb-coordinator and
vairedb-core images, so build them first:
2. Create the Compose file¶
Create a working directory and add the following docker-compose.yml. It
defines one coordinator and five core nodes on a shared bridge network. The
coordinator exposes the PostgreSQL port (5432), the coordinator gRPC port
(50040), and the Ballista scheduler port (50050). Core nodes only talk to
the coordinator over the internal network.
services:
coordinator:
image: vairedb-coordinator
container_name: vairedb-coordinator
ports:
- "5432:5432" # PostgreSQL wire protocol (clients connect here)
- "50040:50040" # coordinator gRPC (node registration, heartbeats)
- "50050:50050" # Ballista scheduler (distributed query execution)
volumes:
- ./config/coordinator.yml:/app/config/coordinator/config.yml:ro
healthcheck:
test:
["CMD-SHELL", "timeout 2 bash -c '</dev/tcp/127.0.0.1/5432' || exit 1"]
interval: 2s
timeout: 3s
retries: 15
start_period: 5s
networks:
- vairedb
core-1:
image: vairedb-core
container_name: vairedb-core-1
depends_on:
coordinator:
condition: service_healthy
volumes:
- ./config/core-1.yml:/app/config/core/config.yml:ro
# - ./data/core/1/:/app/data/core # uncomment to persist this shard's data
networks:
- vairedb
core-2:
image: vairedb-core
container_name: vairedb-core-2
depends_on:
coordinator:
condition: service_healthy
volumes:
- ./config/core-2.yml:/app/config/core/config.yml:ro
networks:
- vairedb
core-3:
image: vairedb-core
container_name: vairedb-core-3
depends_on:
coordinator:
condition: service_healthy
volumes:
- ./config/core-3.yml:/app/config/core/config.yml:ro
networks:
- vairedb
core-4:
image: vairedb-core
container_name: vairedb-core-4
depends_on:
coordinator:
condition: service_healthy
volumes:
- ./config/core-4.yml:/app/config/core/config.yml:ro
networks:
- vairedb
core-5:
image: vairedb-core
container_name: vairedb-core-5
depends_on:
coordinator:
condition: service_healthy
volumes:
- ./config/core-5.yml:/app/config/core/config.yml:ro
networks:
- vairedb
networks:
vairedb:
driver: bridge
Why core nodes wait for the coordinator
Core nodes register themselves with the coordinator on startup. The
depends_on … condition: service_healthy clause delays each core node until
the coordinator's PostgreSQL port answers, so registration does not race
against a starting coordinator.
3. Add the config files¶
Create a config/ directory next to the Compose file with one coordinator
config and one config per core node.
log_level: info
node_id: "core-1"
data_dir: data/core
grpc_listen_addr: "0.0.0.0:50041"
advertised_address: "core-1:50041"
coordinator_addr: "http://coordinator:50040"
heartbeat_interval_secs: 2
write_queue_capacity: 1024
ballista_scheduler_addr: "http://coordinator:50050"
ballista_concurrent_tasks: 4
Each remaining core node is identical except for node_id and
advertised_address, which must be unique. For core-2:
log_level: info
node_id: "core-2"
data_dir: data/core
grpc_listen_addr: "0.0.0.0:50041"
advertised_address: "core-2:50041"
coordinator_addr: "http://coordinator:50040"
heartbeat_interval_secs: 2
write_queue_capacity: 1024
ballista_scheduler_addr: "http://coordinator:50050"
ballista_concurrent_tasks: 4
Repeat for core-3, core-4, and core-5, changing node_id and the
hostname in advertised_address to match the service name.
Networking inside Compose
Each core node's advertised_address and coordinator_addr use the Compose
service names (core-1, coordinator, …) as hostnames. Docker's
embedded DNS resolves them on the shared vairedb network. The
default_replication_factor: 3 means each shard is kept on three nodes — so
a five-node cluster comfortably tolerates a quorum write.
See the Configuration Reference for every field.
4. Start the cluster¶
--wait blocks until the coordinator is healthy and the core nodes have
started. Check the status and logs with:
You should see the core nodes registering with the coordinator in the logs.
5. Connect and run your first queries¶
Connect with any PostgreSQL client:
Create a sharded table, insert rows, and query it:
CREATE TABLE foo_table (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
created_at TIMESTAMP
) WITH (
shards = 3,
replication_factor = 3,
shard_by = 'HASH(id)'
);
INSERT INTO foo_table (id, name, email, created_at)
VALUES (1, 'Alice', 'alice@example.com', '2026-01-15 10:30:00');
INSERT INTO foo_table (id, name, email, created_at)
VALUES (2, 'Bob', 'bob@example.com', '2026-02-20 14:00:00');
SELECT * FROM foo_table;
SELECT id, name FROM foo_table WHERE id = 1;
Inspect the cluster catalog to see how the table was distributed:
For the full SQL surface — DDL, DML, ALTER TABLE, and catalog introspection —
see the SQL Guide.
6. Tear it down¶
The -v flag removes the named volumes; drop it if you mounted host
directories and want to keep shard data between runs.
What just happened¶
- The coordinator accepted your PostgreSQL connection, parsed each statement, and consulted its metadata catalog.
CREATE TABLE … WITH (shards = 3, replication_factor = 3, …)registered the table and assigned its three shards (plus replicas) across the five core nodes.- Each
INSERTwas routed to the owning shard's primary and replicas, and acknowledged once a quorum confirmed it. - The
SELECTwas planned by the embedded Ballista scheduler, which dispatched shard-local scans to the core nodes' DuckDB instances and merged the results.
Read Query Processing to follow these paths end to end.