librtmp2-server keeps its persistent state in one SQLite database. The path
must be supplied through LRTMP2_DB or LRTMP2_DB_PATH; the Docker image uses
/data/server.db.
This guide uses offline backups and restores: stop the server before copying
the database and keep it stopped until the copy or restore is complete. The
server enables SQLite WAL mode, so a running database may also have
server.db-wal and server.db-shm files. Copying only server.db while the
server is running is not a consistent backup.
The database contains:
- the Bearer API token in the
settingstable; - stream definitions, including publish, play, and stats keys;
- per-viewer play keys;
- publisher/player session rows and collected stats samples.
Listener settings, TLS certificates and keys, logs, and the server .env file
are not stored in SQLite. Back them up separately when they are part of the
deployment.
| Operation | Status |
|---|---|
| Stop the server, copy the complete database directory, then restart | Supported |
| Restore a stopped server from a complete offline backup | Supported |
| Move a stopped Docker volume or bind-mounted data directory | Supported |
Use LRTMP2_API_TOKEN to seed an empty database on first startup |
Supported |
Change LRTMP2_API_TOKEN after a token is already stored |
Not a rotation mechanism; the stored database token wins |
| Replace the token directly in SQLite while the server is stopped | Manual; no supported rotation CLI or API exists yet |
Copy only server.db while the server is running |
Unsupported |
First identify the exact database path used by the running service. In the
examples below, replace /var/lib/librtmp2-server/server.db with that path.
-
Stop the server with the same supervisor used to start it:
sudo systemctl stop <service-name>
-
Create a private backup directory and copy the database plus any WAL sidecars:
DB=/var/lib/librtmp2-server/server.db BACKUP_DIR="$HOME/librtmp2-backup-$(date -u +%Y%m%dT%H%M%SZ)" umask 077 mkdir -p "$BACKUP_DIR" sudo cp -a -- "$DB" "$BACKUP_DIR/server.db" for suffix in -wal -shm; do if [ -e "${DB}${suffix}" ]; then sudo cp -a -- "${DB}${suffix}" "$BACKUP_DIR/server.db${suffix}" fi done
-
Start the server and verify it:
sudo systemctl start <service-name> curl --fail --silent --show-error http://127.0.0.1:8080/api/v1/health
Store the backup as sensitive data: it contains the API token and every stream key.
Restore into a new path first. This keeps the current database available for a quick rollback.
-
Stop the server and make a fresh pre-restore backup using the procedure above.
-
Copy the selected backup into a new private directory:
DB=/var/lib/librtmp2-server/server.db BACKUP_DIR=/path/to/selected/backup RESTORE_DIR=/var/lib/librtmp2-server-restored umask 077 sudo install -d -m 700 "$RESTORE_DIR" sudo find "$BACKUP_DIR" -maxdepth 1 -name 'server.db*' \ -exec cp -a -t "$RESTORE_DIR" -- {} + sudo chown --reference="$(dirname "$DB")" "$RESTORE_DIR" sudo find "$RESTORE_DIR" -maxdepth 1 -name 'server.db*' \ -exec chown --reference="$DB" -- {} + \ -exec chmod 600 -- {} + sudo test -f "$RESTORE_DIR/server.db" || { echo "Restore failed: $RESTORE_DIR/server.db not found" >&2 exit 1 }
BACKUP_DIRmust point at the backup you selected to restore, not necessarily the fresh pre-restore backup made in the previous step. Thetest -fcheck guards against pointing the server at an emptyRESTORE_DIR, which would otherwise make it silently bootstrap a brand new, empty database and token. -
Stop the server again (the backup procedure's step 3 started it). Point
LRTMP2_DBat/var/lib/librtmp2-server-restored/server.db. The server readsLRTMP2_DBbeforeLRTMP2_DB_PATH, so if the service environment already setsLRTMP2_DB_PATH, either updateLRTMP2_DBto override it or unset/alignLRTMP2_DB_PATHtoo — otherwise the service keeps using the old database. Then start the server. -
Run the verification checks below. Keep the old directory unchanged until verification succeeds.
To roll back, stop the server, restore the previous database path in the service environment, and start it again.
The quickstart container is named librtmp2-server and mounts a named volume
at /data. Confirm the actual volume name before continuing:
docker inspect librtmp2-server \
--format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}'Set DATA_VOLUME to that output and IMAGE to the exact image tag or digest
used by the container:
DATA_VOLUME=librtmp2-server-data
IMAGE=<same-image-tag-or-digest-as-the-server>
BACKUP_DIR="$PWD/backups"
ARCHIVE="librtmp2-data-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
umask 077
mkdir -p "$BACKUP_DIR"
docker stop librtmp2-server
docker run --rm --user 0 --entrypoint sh \
-v "$DATA_VOLUME":/data:ro \
-v "$BACKUP_DIR":/backup \
"$IMAGE" \
-c "set -eu; tar -czf /backup/$ARCHIVE -C /data ."
test -s "$BACKUP_DIR/$ARCHIVE" || {
echo "Backup failed: $BACKUP_DIR/$ARCHIVE is missing or empty" >&2
exit 1
}
docker start librtmp2-serverEach run writes a distinct, timestamped archive so re-running the backup never overwrites a prior recovery point. This archives the whole volume, including any WAL sidecars.
Restore into a new named volume instead of overwriting the existing one. Set
ARCHIVE to the filename of the backup you selected to restore, not
necessarily the most recent one:
RESTORE_VOLUME=librtmp2-server-data-restored
BACKUP_DIR="$PWD/backups"
ARCHIVE=librtmp2-data-<selected-backup-timestamp>.tar.gz
IMAGE=<same-image-tag-or-digest-as-the-server>
docker volume create "$RESTORE_VOLUME"
docker run --rm --user 0 --entrypoint sh \
-v "$RESTORE_VOLUME":/data \
-v "$BACKUP_DIR":/backup:ro \
"$IMAGE" \
-c "set -eu
tar -xzf /backup/$ARCHIVE -C /data
chown -R openrtmp:openrtmp /data
chmod 700 /data
for f in /data/server.db*; do
[ -e \"\$f\" ] && chmod 600 \"\$f\"
done
test -f /data/server.db"Stop the old container, but keep it (do not remove it) so it stays available
for rollback. Create the replacement under a temporary name — for example
librtmp2-server-restored — with the same ports and environment but mounting
"$RESTORE_VOLUME":/data. For Compose, change the volume mapping to the
restored external volume and set container_name to the temporary name before
running docker compose up -d.
For migration to another host, copy librtmp2-data.tar.gz over a secure
channel, create a new volume there, and run the same restore command. Do not
start both copies against a shared database or expose both with the same stream
keys.
Keep the old stopped container and volume until the restored deployment passes
the verification checks below. Only then remove the old container and rename
the replacement to librtmp2-server (or update the Compose container_name
and re-apply). Rollback before that point is starting the old container again;
rollback after that point is switching the replacement container back to the
old volume.
For a bind mount such as -v /srv/librtmp2:/data, stop the container and
archive the complete host directory:
DATA_DIR=/srv/librtmp2
BACKUP_DIR="$PWD/backups"
ARCHIVE="librtmp2-data-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
umask 077
mkdir -p "$BACKUP_DIR"
docker stop librtmp2-server
sudo tar -czf "$BACKUP_DIR/$ARCHIVE" -C "$DATA_DIR" .
test -s "$BACKUP_DIR/$ARCHIVE" || {
echo "Backup failed: $BACKUP_DIR/$ARCHIVE is missing or empty" >&2
exit 1
}
docker start librtmp2-serverEach run writes a distinct, timestamped archive so re-running the backup never overwrites a prior recovery point.
Restore into a new host directory, preserve restrictive permissions, and mount
that directory at /data in the replacement container. Set ARCHIVE to the
filename of the backup you selected to restore, not necessarily the most
recent one:
RESTORE_DIR=/srv/librtmp2-restored
BACKUP_DIR="$PWD/backups"
ARCHIVE=librtmp2-data-<selected-backup-timestamp>.tar.gz
IMAGE=<same-image-tag-or-digest-as-the-server>
sudo install -d -m 700 "$RESTORE_DIR"
sudo tar -xzf "$BACKUP_DIR/$ARCHIVE" -C "$RESTORE_DIR"
sudo test -f "$RESTORE_DIR/server.db" || {
echo "Restore failed: $RESTORE_DIR/server.db not found" >&2
exit 1
}
docker run --rm --user 0 --entrypoint sh \
-v "$RESTORE_DIR":/data \
"$IMAGE" \
-c 'chown -R openrtmp:openrtmp /data
chmod 700 /data
for f in /data/server.db*; do
[ -e "$f" ] && chmod 600 "$f"
done'On startup the server reads the api_token row from SQLite first:
- If a non-empty token is stored, that token is used. A different
LRTMP2_API_TOKENvalue only produces a warning. - If the database has no token, a valid
LRTMP2_API_TOKENprocess environment variable is stored and used. - If neither exists, the server generates a 64-character hexadecimal token, stores it, and prints it once to stderr.
The .env config loader intentionally ignores LRTMP2_API_TOKEN; it must be a
real process environment variable during the first startup of an empty
database. Restoring a database also restores its token.
There is currently no supported API or CLI for rotating the token while
preserving the rest of the database. If an in-place rotation is required, the
following is a manual operation grounded in the current settings schema.
Schedule downtime and take a backup first.
-
Stop
librtmp2-serverand thelibrtmp2-server-panel. -
Run this on the stopped database. It generates a token without putting it in shell history or process arguments, writes it to a mode-
0600file, and updates exactly the existingapi_tokenrow:DB=/var/lib/librtmp2-server/server.db TOKEN_FILE="$HOME/librtmp2-new-api-token" umask 077 python3 - "$DB" "$TOKEN_FILE" <<'PY' import os import secrets import sqlite3 import sys db_path, token_path = sys.argv[1:] conn = sqlite3.connect(db_path) row = conn.execute( "SELECT val FROM settings WHERE key = 'api_token'" ).fetchone() if row is None or not row[0]: raise SystemExit("database does not contain a non-empty api_token row") new_token = secrets.token_hex(32) fd = os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) try: with os.fdopen(fd, "w", encoding="ascii") as token_file: token_file.write(new_token + "\n") token_file.flush() os.fsync(token_file.fileno()) with conn: changed = conn.execute( "UPDATE settings SET val = ? WHERE key = 'api_token'", (new_token,), ).rowcount if changed != 1: raise RuntimeError(f"expected one updated row, got {changed}") except BaseException: try: os.unlink(token_path) except FileNotFoundError: pass raise finally: conn.close() PY
-
Set
LRTMP2_API_TOKENto the value inTOKEN_FILEas a real process environment variable for the panel (for example, a systemdEnvironment=orEnvironmentFile=unit drop-in outside the panel's own.env, or a Docker/Composeenvironment:/--env-fileentry), not by adding it to the panel's.envfile. Keep the token out of command-line arguments and shell history. -
Start the server, then start the panel, and run both verification requests below.
-
After every client has been updated, securely remove
TOKEN_FILE.
If verification fails, stop the server and panel, restore the pre-rotation database backup, restore the old panel token, and start the server followed by the panel.
The public health check proves that the HTTP listener is available:
curl --fail --silent --show-error \
http://127.0.0.1:8080/api/v1/healthThen read the token without placing it in curl's process arguments and verify an authenticated request:
read -r -s -p 'API token: ' API_TOKEN
printf '\n'
curl --fail --silent --show-error --config - \
http://127.0.0.1:8080/api/v1/streams <<EOF
header = "Authorization: Bearer ${API_TOKEN}"
EOF
unset API_TOKENFor a restore or migration, compare the returned stream IDs with the source deployment. A public health response alone does not prove that the intended database or API token was restored.