rqloud

package module
v0.0.0-...-6272725 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 2, 2026 License: MIT Imports: 23 Imported by: 0

README

rqloud

rqloud combines Tailscale (tsnet) networking with rqlite (distributed SQLite) into a self-contained replicated application platform.

Your application gets a database/sql or native gorqlite interface backed by a Raft-replicated SQLite database, with all inter-node communication happening over your Tailscale network.

For a real-world example, see JustinAzoff/golink, a fork of tailscale/golink using rqloud instead of a local SQLite database.

Features

  • Embedded rqlite — no separate database process to manage
  • Tailscale networking — all traffic (Raft consensus, cluster coordination, HTTP API) runs over tsnet, with caller identity via WhoIs()
  • Automatic clustering — nodes discover each other by hostname prefix and auto-join
  • Two database interfaces — database/sql for standard Go code, or native gorqlite for rqlite-specific features

Standalone Binary

cmd/rqloud runs a bare rqlite cluster over Tailscale with no application code on top. Use it to deploy a replicated SQLite database accessible via the standard rqlite CLI or HTTP API.

CGO_ENABLED=1 go build -o rqloud ./cmd/rqloud/

Start a single node:

./rqloud -instance mydb /tmp/rqloud-test/mydb

Start a 3-node cluster:

./rqloud -instance mydb-1 -bootstrap-expect 3 /tmp/rqloud-test/mydb-1
./rqloud -instance mydb-2 -bootstrap-expect 3 /tmp/rqloud-test/mydb-2
./rqloud -instance mydb-3 -bootstrap-expect 3 /tmp/rqloud-test/mydb-3

The three nodes can all run on the same machine or on three different machines — since all communication happens over the tailnet, it doesn't matter where they are.

Connect via the rqlite CLI over the tailnet:

rqlite -H mydb-1 -p 4001

Or open the rqlite web console at http://mydb-1:4001/.

To access rqlite from outside of the tailnet, use -local-rqlite-bind:

./rqloud -instance mydb-1 -local-rqlite-bind 127.0.0.1:4001 /tmp/rqloud-test/mydb-1
rqlite  # connects to localhost:4001

This is useful for local tooling, monitoring, or applications that don't run on the tailnet.

Quick Start (Library)

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"

    "github.com/JustinAzoff/rqloud"
)

func main() {
    flag.Usage = func() {
        fmt.Fprintf(flag.CommandLine.Output(), "Usage: hitcount [hostname]\n\n")
        fmt.Fprintf(flag.CommandLine.Output(), "Use \"hitcount\" for a single node, or \"hitcount-1\", \"hitcount-2\", etc. for a cluster.\n\n")
        flag.PrintDefaults()
    }
    flag.Parse()

    hostname := "hitcount"
    if flag.NArg() > 0 {
        hostname = flag.Arg(0)
    }

    srv := &rqloud.Server{
        Hostname: hostname,
    }
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
    defer srv.Close()

    db, _ := srv.DB()
    db.Exec(`CREATE TABLE IF NOT EXISTS hits (count INTEGER)`)
    db.Exec(`INSERT INTO hits (count) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM hits)`)

    ln, _ := srv.Listen("tcp", ":80")
    log.Printf("listening on http://%s/", hostname)
    http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        db.Exec(`UPDATE hits SET count = count + 1`)
        var count int
        db.QueryRow(`SELECT count FROM hits`).Scan(&count)
        fmt.Fprintf(w, "hits: %d\n", count)
    }))
}

Clustering

Nodes discover peers automatically by hostname prefix. Name your instances with a shared prefix and a unique suffix separated by a hyphen:

myapp-1
myapp-2
myapp-3

The first node to start bootstraps a new single-node cluster. Subsequent nodes discover existing peers on the tailnet and join automatically.

A standalone instance (hostname with no hyphen, e.g. myapp) runs as a single non-clustered node.

API

Constructors
// Create a server that manages its own tsnet node.
srv := rqloud.New()
srv.Hostname = "myapp-1"

// Or use an existing tsnet.Server.
srv := rqloud.NewWithTSNet(existingTS)
rqloud.Server
srv := &rqloud.Server{
    Hostname:      "myapp-1",         // tsnet hostname (required)
    Dir:           "/data/myapp-1",   // rqlite data directory (default: ~/.config/rqloud/<hostname>)
    TSDir:         "",                // tsnet config directory (default: ~/.config/tsnet-<hostname>)
    AuthKey:       "tskey-...",       // Tailscale auth key (default: interactive login)
    AdvertiseTags: []string{"tag:myapp"},
    Verbose:       false,
}
Method Returns Description
Start() error Initialize tsnet, wait for tailnet, start rqlite store and HTTP API
Close() error Graceful shutdown
Up(ctx) error Wait for the tsnet node to connect to the tailnet
Listen(net, addr) net.Listener, error Listen on the tsnet interface (for your app's traffic)
ListenService(name, mode) *tsnet.ServiceListener, error Register a Tailscale Service listener
DB() *sql.DB, error Standard database/sql handle
Gorqlite() *gorqlite.Connection, error Native gorqlite connection (uses tsnet HTTP client)
LocalListen(net, addr) net.Listener, error Listen on a normal network interface
WhoIs(r) *apitype.WhoIsResponse, error Identify the Tailscale caller from an HTTP request
TS() *tsnet.Server Access the underlying tsnet server

Example: Todo App

See examples/todo/ for a complete per-user todo list application.

CGO_ENABLED=1 go build -o rqloud-todo ./examples/todo/
./rqloud-todo -instance todo-1

Start a second instance to form a cluster:

./rqloud-todo -instance todo-2

They'll discover each other on the tailnet and replicate automatically.

Building

CGO is required (rqlite uses a fork of go-sqlite3):

CGO_ENABLED=1 go build ./...

rqlite Fork

rqloud depends on a fork of rqlite with two small patches:

  • http/service.goListener field so Start() accepts an external net.Listener (for tsnet)
  • store/store.goResolveAddress hook to override DNS resolution (tsnet handles its own name resolution)

These are tracked as separate branches (custom-http-listener, custom-dns) merged into the justin-integration branch, which go.mod references via a replace directive.

Documentation

Overview

Package rqloud provides a self-contained replicated application platform combining Tailscale (tsnet) networking with rqlite distributed SQLite.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewAddr

func NewAddr(host string, port int) net.Addr

Types

type Server

type Server struct {
	// Hostname is the tsnet hostname for this node.
	Hostname string

	// Dir is the rqlite data directory (Raft logs, snapshots, SQLite DB).
	// Defaults to a directory based on Hostname in os.UserConfigDir().
	Dir string

	// TSDir is the tsnet configuration directory. If empty, defaults to
	// ~/.config/tsnet-<Hostname>/.
	// Only used when rqloud creates its own tsnet (i.e. not NewWithTSNet).
	TSDir string

	// AuthKey is the Tailscale auth key. If empty, interactive login is used.
	// Only used when rqloud creates its own tsnet (i.e. not NewWithTSNet).
	AuthKey string

	// AdvertiseTags is a list of ACL tags to advertise (e.g. "tag:todo").
	// Only used when rqloud creates its own tsnet (i.e. not NewWithTSNet).
	AdvertiseTags []string

	// BootstrapExpect is the number of nodes expected to form the initial
	// cluster. When set, nodes use the notify/bootstrap protocol to
	// coordinate simultaneous startup. When 0 (default), the first node
	// bootstraps solo and others join it.
	BootstrapExpect int

	// RaftHeartbeat controls the Raft heartbeat timeout. All other Raft
	// timeouts (election, lease, commit) are scaled proportionally from
	// Raft's default ratios. Higher values reduce idle traffic but
	// increase failover time. Defaults to 3s (Raft default is 1s).
	RaftHeartbeat time.Duration

	// Verbose enables verbose tsnet logging.
	Verbose bool
	// contains filtered or unexported fields
}

Server is the main rqloud server. It manages a tsnet node, an embedded rqlite store, and provides database access over the tailnet.

func New

func New() *Server

New creates a new rqloud Server that will create and manage its own tsnet node. Call Start() to begin.

func NewWithTSNet

func NewWithTSNet(ts *tsnet.Server) *Server

NewWithTSNet creates a new rqloud Server using an existing tsnet.Server. The caller is responsible for the tsnet lifecycle (starting it before calling Start, closing it after calling Close). Hostname is derived from the tsnet server.

func (*Server) Close

func (s *Server) Close() error

Close shuts down the server.

func (*Server) DB

func (s *Server) DB() (*sql.DB, error)

DB returns a database/sql handle connected to the local rqlite node. Uses a custom driver that routes all HTTP traffic through tsnet.

func (*Server) Gorqlite

func (s *Server) Gorqlite() (*gorqlite.Connection, error)

Gorqlite returns a native gorqlite connection to the local rqlite node. Uses tsnet's HTTP client so all traffic stays on the tailnet.

func (*Server) Listen

func (s *Server) Listen(network, addr string) (net.Listener, error)

Listen returns a net.Listener on the tsnet interface.

func (*Server) ListenService

func (s *Server) ListenService(name string, mode tsnet.ServiceMode) (*tsnet.ServiceListener, error)

ListenService creates a Tailscale Service listener, advertising this node as hosting the named service. See tsnet.Server.ListenService for details.

func (*Server) LocalListen

func (s *Server) LocalListen(network, addr string) (net.Listener, error)

LocalListen returns a net.Listener on a normal network interface.

func (*Server) Start

func (s *Server) Start() error

Start initializes and starts the tsnet node, rqlite store, and HTTP API.

func (*Server) TS

func (s *Server) TS() *tsnet.Server

TS returns the internal tsnet.Server

func (*Server) Up

func (s *Server) Up(ctx context.Context) error

Up waits for the tsnet node to connect to the tailnet.

func (*Server) WhoIs

func (s *Server) WhoIs(r *http.Request) (*apitype.WhoIsResponse, error)

WhoIs returns the Tailscale identity of the caller for the given HTTP request.

Directories

Path Synopsis
cmd
rqloud command
Command rqloud runs a standalone rqlite cluster over Tailscale.
Command rqloud runs a standalone rqlite cluster over Tailscale.
examples
counter command
Command counter is a minimal rqloud example: a replicated counter with increment/decrement buttons, useful for integration testing.
Command counter is a minimal rqloud example: a replicated counter with increment/decrement buttons, useful for integration testing.
hitcount command
todo command
Command todo is a demo rqloud application: a per-user todo list served over tsnet.
Command todo is a demo rqloud application: a per-user todo list served over tsnet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL