libSQL Self-hosting
You might have heard of Turso, a hosted SQLite solution. Their fork of SQLite, libSQL, is open source, so anyone can play with it for free.
Trouble is, their guide on self-hosting is somewhat lacking, so I figured I’d write down how I got everything up and running for some proper testing.
NOTE: This post is written for a Debian-based Linux system, but it’s probably not that far off for a Mac user.
Get the turso
and sqld
binaries
If you’re interested in self-hosting libSQL, then you probably already have the Turso CLI installed, in which case you can skip over to the next section.
To install the Turso CLI, follow the first step of their guide:
curl -sSfL https://get.tur.so/install.sh | bash
That’ll create a ~/.turso
directory containing the turso
and sqld
binaries. It also modifies ~/.bashrc
to add that directory to the PATH
, so remember to
source ~/.bashrc
to get the PATH
updated.
Create the sqld
user and directory
Create sqld
user
sudo useradd --system --home /opt/sqld --shell /bin/false sqld
Add yourself to the group
sudo usermod --append --groups sqld $(whoami)
Create a directory for the data and scripts. I like using /opt
sudo mkdir /opt/sqld
sudo chown sqld:sqld /opt/sqld
sudo chmod 775 /opt/sqld
Go into that directory
cd /opt/sqld
Generate certificates
Get the script to generate certificates
wget https://raw.githubusercontent.com/tursodatabase/libsql/refs/heads/main/libsql-server/scripts/gen_certs.py
That script will, by default, generate certs that last 3 days, which is quite inconvenient, so you can edit that to something a bit longer, like 100 years:
-not_after = not_before + datetime.timedelta(days=3)
+not_after = not_before + datetime.timedelta(days=365*100)
Run the script
python3 gen_certs.py
You might have an error like this
Traceback (most recent call last):
File "/opt/sqld/gen_certs.py", line 10, in <module>
from cryptography import x509
ModuleNotFoundError: No module named 'cryptography'
In which case, install the missing package
sudo apt install python3-cryptography
This time, it should work
stored cert 'ca' into 'ca_cert.pem'
stored private key 'ca' into 'ca_key.pem'
stored cert 'server' into 'server_cert.pem'
stored private key 'server' into 'server_key.pem'
stored cert 'client' into 'client_cert.pem'
stored private key 'client' into 'client_key.pem'
these are development certs, they will expire at 2025-02-06 08:36:02.521579+00:00
Generate keypair for authentication tokens
To generate authentication tokens (JWTs), you need to generate another keypair, and as I never remember how to use OpenSSL, I just use the smallstep CLI, which you can install with
wget https://dl.smallstep.com/cli/docs-cli-install/latest/step-cli_amd64.deb
sudo dpkg -i step-cli_amd64.deb
Create the keypair
step crypto keypair jwt.pub jwt.key \
--kty OKP \
--curve Ed25519 \
--no-password \
--insecure
Run the primary server
To run the primary, create run-primary.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
mkdir -p primary
cd primary
exec sqld \
--enable-namespaces \
--admin-listen-addr 127.0.0.1:5000 \
--auth-jwt-key-file ../jwt.pub \
--http-listen-addr 127.0.0.1:3000 \
--grpc-listen-addr 127.0.0.1:4000 \
--grpc-tls \
--grpc-ca-cert-file ../ca_cert.pem \
--grpc-cert-file ../server_cert.pem \
--grpc-key-file ../server_key.pem
Make that executable
chmod +x run-primary.sh
And see if it runs!
_ _
___ __ _| | __| |
/ __|/ _` | |/ _` |
\__ \ (_| | | (_| |
|___/\__, |_|\__,_|
|_|
Welcome to sqld!
version: 0.24.31
commit SHA: e88c6b513da5e87e2051c8dd58797ef367392440
build date: 2025-01-06
This software is in BETA version.
If you encounter any bug, please open an issue at https://github.com/tursodatabase/libsql/issues
config:
- mode: primary (127.0.0.1:4000)
- database path: data.sqld
- extensions path: <disabled>
- listening for HTTP requests on: 127.0.0.1:3000
- grpc_tls: yes
2025-02-03T08:43:12.667913Z INFO sqld: listening for incoming user HTTP connection on 127.0.0.1:3000
2025-02-03T08:43:12.668370Z INFO sqld: Using JWT-based authentication
2025-02-03T08:43:12.668523Z INFO sqld: listening for incoming adming HTTP connection on 127.0.0.1:5000
2025-02-03T08:43:12.669558Z INFO sqld: listening for incoming gRPC connection on 127.0.0.1:4000
2025-02-03T08:43:12.674563Z INFO restore: libsql_server::namespace::meta_store: restoring meta store
2025-02-03T08:43:12.674746Z INFO restore: libsql_server::namespace::meta_store: meta store restore completed
2025-02-03T08:43:12.675639Z INFO libsql_server: Server sending heartbeat to URL <not supplied> every 30s
2025-02-03T08:43:12.676206Z INFO libsql_server::rpc: serving internal rpc server with tls
2025-02-03T08:43:12.677746Z INFO libsql_server::http::admin: initializing prometheus metrics
Run the replica
A similar script can be created for a replica
run-replica-sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
mkdir -p replica
cd replica
exec sqld \
--enable-namespaces \
--auth-jwt-key-file ../jwt.pub \
--http-listen-addr 127.0.0.1:3001 \
--primary-grpc-url https://127.0.0.1:4000 \
--primary-grpc-tls \
--primary-grpc-ca-cert-file ../ca_cert.pem \
--primary-grpc-cert-file ../client_cert.pem \
--primary-grpc-key-file ../client_key.pem
You can run this along with the primary:
_ _
___ __ _| | __| |
/ __|/ _` | |/ _` |
\__ \ (_| | | (_| |
|___/\__, |_|\__,_|
|_|
Welcome to sqld!
version: 0.24.31
commit SHA: e88c6b513da5e87e2051c8dd58797ef367392440
build date: 2025-01-06
This software is in BETA version.
If you encounter any bug, please open an issue at https://github.com/tursodatabase/libsql/issues
config:
- mode: replica (primary at https://127.0.0.1:4000)
- database path: data.sqld
- extensions path: <disabled>
- listening for HTTP requests on: 127.0.0.1:3001
- grpc_tls: no
2025-02-03T08:47:13.254061Z INFO sqld: listening for incoming user HTTP connection on 127.0.0.1:3001
2025-02-03T08:47:13.254436Z INFO sqld: Using JWT-based authentication
2025-02-03T08:47:13.262912Z INFO restore: libsql_server::namespace::meta_store: restoring meta store
2025-02-03T08:47:13.263089Z INFO restore: libsql_server::namespace::meta_store: meta store restore completed
2025-02-03T08:47:13.263977Z INFO libsql_server: Server sending heartbeat to URL <not supplied> every 30s
Create a JWT token
To actually connect to the server(s), you’ll need to generate an authentication token. You can use jwt-cli for that:
wget https://github.com/mike-engel/jwt-cli/releases/download/6.2.0/jwt-linux-musl.tar.gz
tar xf jwt-linux-musl.tar.gz
rm jwt-linux-musl.tar.gz
sudo mv jwt /usr/bin
Create another script, gen-auth-token.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
step crypto key format --pkcs8 jwt.key > jwt.key.der
args=(
--alg EDDSA
--out jwt.token
--secret @jwt.key.der
'{"a": "rw"}'
)
jwt encode "${args[@]}"
jwt decode - < jwt.token
Make it executable, and run it. It will create a jwt.token
file, which you can use for authentication.
But before using that token, you should to create a database to connect to.
Database management
You’ll want to create a database using the Admin API, so create a script for that
db-create.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
db_name=$1
if [[ -z "${db_name}" ]]; then
echo "usage: $0 <db-name>"
exit 1
fi
curl "http://127.0.0.1:5000/v1/namespaces/${db_name}/create" \
-X POST \
-H "content-type: application/json" \
-d '{ "dump_url": null }'
You might also want to delete or fork a database, so here are scripts for those actions as well:
db-delete.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
db_name=$1
if [[ -z "${db_name}" ]]; then
echo "usage: $0 <db-name>"
exit 1
fi
curl "http://127.0.0.1:5000/v1/namespaces/${db_name}" \
-X DELETE \
-H "content-type: application/json"
db-fork.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
db_from=$1
db_to=$2
if [[ -z "${db_from}" ]] || [[ -z "${db_to}" ]]; then
echo "usage: $0 <from> <to>"
exit 1
fi
curl "http://127.0.0.1:5000/v1/namespaces/${db_from}/fork/${db_to}" \
-X POST \
-H "content-type: application/json" \
-d '{ "dump_url": null }'
Create a database
Make sure the primary is running, and create the first database
./db-create.sh test
Use the interactive shell
For an interactive shell, create another script (which uses the JWT we created earlier)
shell-primary.sh
#!/bin/bash
set -eo pipefail
script_dir=$(dirname "$(realpath "$0")")
cd "${script_dir}"
db_name=$1
if [[ -z "${db_name}" ]]; then
echo "usage: $0 <db-name>"
exit 1
fi
exec turso db shell "http://${db_name}.localhost:3000?authToken=$(cat jwt.token)"
Try it out
./shell-primary.sh test
Connected to http://test.localhost:3000?authToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJhIjoicnciLCJpYXQiOjE3Mzg1NzI3MzJ9.lc1D2JR-3TPoGrCITABUVvqrRZJkhz_cDxVDIKxRg_6NagMqGWb9maas4Gea3zc0rILbnAS4BLY97hcHd0KaBw
Welcome to Turso SQL shell!
Type ".quit" to exit the shell and ".help" to list all available commands.
→ create table users (id integer primary key autoincrement, name text) strict;
→ insert into users (name) values ('foo'), ('bar');
→ select * from users;
ID NAME
1 foo
2 bar
Notice that if you try connecting to a non-existent database, you’ll get an error
./shell-primary.sh test2
Error: failed to connect to database. err: failed to execute SQL:
error code 404: Namespace `test2` doesn't exist
Using this setup with the official SDK is also very simple
// typescript
import { createClient } from "@libsql/client";
const client = createClient({
url: "libsql://test.localhost:3000",
authToken: "< contents of jwt.token >",
});
Important note on DNS
Finally, please note that because selecting the database relies on the domain (eg. <db>.your.host.com
), you need DNS to support it. Which is why I used <db>.localhost
in this guide, as localhost
has built-in wildcard DNS support.