VPS Snaps

How to back up and restore Keycloak

Keycloak keeps realms, users and their password hashes, client secrets, signing keys and sessions in its database, so a database dump such as pg_dump -Fc keycloak is the backup, and it runs while Keycloak keeps serving logins. Add conf/keycloak.conf or the KC_* environment, providers/, themes/ and your TLS files, and restore onto the same Keycloak version before upgrading. kc.sh export writes realms as JSON, but only with Keycloak stopped and without sessions or events, so it is a migration tool rather than your nightly backup.

9 min readUpdated Checked against official documentation

What to back up

This guide assumes Keycloak 26 (26.8.0 is current) unpacked in /opt/keycloak, the path the official container image uses, run by a keycloak user, with PostgreSQL on the same machine.

PartWhereWhat it holds
Databasekeycloak on PostgreSQL, MySQL, MariaDB, SQL Server or OracleRealms, users and password hashes, clients and secrets, realm keys, roles, groups, user sessions (kept in the database by default since 26.0), saved events
conf//opt/keycloak/confkeycloak.conf with db, db-url-host, db-username, db-password, hostname and the https-* options; cache-ispn.xml if you changed it; server.keystore if you use one
EnvironmentThe systemd unit and its EnvironmentFile, or the compose file and .envKC_* variables override keycloak.conf, so the database password may only live here
providers/, themes//opt/keycloakCustom provider JARs and themes. The upgrade guide copies both to every new installation.
TLS filesWhere https-certificate-file and https-certificate-key-file pointNot needed when a reverse proxy terminates TLS
Versionbin/kc.sh --versionPrints Keycloak 26.8.0 first; restore onto the same version.

Keycloak's database guide says the database holds hashed passwords, client credentials and the realm keys that sign tokens, and that backups need encryption too (how). Because the keys come back with the database, applications keep accepting tokens from a restored server, provided hostname is unchanged: every token's issuer is built from it.

If db is unset or dev-file, Keycloak runs on its built-in H2 database in data/h2. The database guide says dev-file is for development only and must be replaced before production, and the upgrade guide says it cannot be migrated to a new version. Stop Keycloak, copy data/h2, then move to PostgreSQL with export and import, below. kc.sh show-config prints the settings Keycloak would start with, secrets masked.

Dump the database and copy the files

Terminal
install -d -m 700 /var/backups/keycloak
Terminal
runuser -u postgres -- pg_dump -Fc keycloak > /var/backups/keycloak/keycloak-db.dump
  • runuser -u postgres runs pg_dump as the postgres system user, which logs in over the local socket without a password; root's shell writes the file.
  • -Fc writes PostgreSQL's compressed custom format, which pg_restore can load under a new owner.
  • pg_dump reads one consistent snapshot, so Keycloak keeps running. It must be the server's major version or newer; Keycloak 26.8 supports PostgreSQL 14 to 18.
  • For a database on another host, use db-url-host and db-username: pg_dump -h <db-host> -U keycloak -Fc keycloak, with the password in ~/.pgpass.

On MySQL or MariaDB, mysqldump --single-transaction keycloak reads a consistent snapshot of InnoDB tables without locking them. More in pg_dump and mysqldump.

Then copy the files. keycloak.conf may hold db-password in plain text, so keep this archive as private as the dump; a config keystore (--config-keystore) is a file plus a password, and you need both.

Terminal
tar -czf /var/backups/keycloak/keycloak-files.tar.gz -C /opt/keycloak conf providers themes

Automate it

One script does both, nightly from cron:

/usr/local/bin/keycloak-backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /

OUT=/var/backups/keycloak
STAMP=$(date +%F-%H%M)
mkdir -p "$OUT"
trap 'rm -f "$OUT"/*.partial' EXIT

runuser -u keycloak -- /opt/keycloak/bin/kc.sh --version > "$OUT/keycloak-version-$STAMP.txt"
runuser -u postgres -- pg_dump -Fc keycloak > "$OUT/keycloak-db-$STAMP.dump.partial"
mv "$OUT/keycloak-db-$STAMP.dump.partial" "$OUT/keycloak-db-$STAMP.dump"
tar -czf "$OUT/keycloak-files-$STAMP.tar.gz" -C /opt/keycloak conf providers themes

find "$OUT" -maxdepth 1 -name 'keycloak-*' -type f -mtime +7 -delete
/etc/cron.d/keycloak-backup
15 3 * * * root /usr/local/bin/keycloak-backup.sh >> /var/log/keycloak-backup.log 2>&1

The dump is written as .partial and renamed when complete; the trap deletes a half-written one. Files older than 7 days go only after a successful run. Run chmod 700 on the script, and copy the folder off the machine, for example with rclone.

Realm export with kc.sh export

Keycloak's guide says the export has limitations as a backup: it is only consistent with every node stopped, and the server must not be running. The command is itself a server launch that writes JSON and exits before the full server comes up:

Terminal
systemctl stop keycloak
Terminal
sudo -u keycloak /opt/keycloak/bin/kc.sh export --dir /var/backups/keycloak/export --users realm_file --optimized
Terminal
systemctl start keycloak
  • --dir creates the folder and writes <realm>-realm.json per realm. --file puts everything in one file, which the guide says to avoid above 50,000 users.
  • --realm <name> exports one realm; the default is every realm, master and its admin accounts included.
  • --users: different_files (the default, 50 users per file, changed with --users-per-file), same_file, realm_file or skip.
  • --optimized skips the build check. Use it when the server starts with start --optimized; otherwise the export may rebuild Keycloak and change its next start. Without a prior kc.sh build, leave it out.
  • The export reads keycloak.conf like the server. KC_* variables set only in the systemd unit must be exported in your shell too.

A CLI export holds realm settings, clients with their secrets, users with password hashes, and each component's full configuration, which includes the realm private keys, the SMTP password and LDAP bind passwords: keep it as secret as the database. It leaves out user and admin events, persisted sessions, workflow state and revoked tokens, so everyone signs in again after an import. The admin console's Partial export has no users, masks secrets with *, and the guide calls it unsuitable for backups.

Docker installs

These commands assume compose services named keycloak and postgres and a database and user called keycloak; use your names. Dump inside the database container, so pg_dump matches the server:

Terminal
docker compose exec -T postgres pg_dump -U keycloak -Fc keycloak > /var/backups/keycloak/keycloak-db.dump

-T turns off the pseudo-terminal, which a redirected dump needs. Keep the compose file, .env and any mounted certificates or provider JARs too. A start-dev container without a volume keeps H2 inside the container, so removing the container deletes every realm. To export, run a one-off container from the same service while the real one is stopped:

Terminal
install -d -o 1000 -m 700 /var/backups/keycloak/export
Terminal
docker compose stop keycloak
Terminal
docker compose run --rm -v /var/backups/keycloak/export:/opt/keycloak/data/export keycloak export --dir /opt/keycloak/data/export --users realm_file
Terminal
docker compose start keycloak

The image's entrypoint is kc.sh and it runs as UID 1000, hence the folder's owner. docker compose run replaces the service's command and publishes no ports. Add --optimized if your image runs kc.sh build when it is built.

Restore onto a new server

Restore onto the same Keycloak version, check it, then upgrade. Do not start Keycloak against the empty database first, or it creates its own tables. Create the role and database, then restore as that role:

Terminal
sudo -u postgres createuser --pwprompt keycloak
Terminal
sudo -u postgres createdb --owner=keycloak keycloak
Terminal
runuser -u postgres -- pg_restore -d keycloak --no-owner --role=keycloak < keycloak-db.dump

--no-owner skips the old ownership commands and --role=keycloak creates every table as keycloak, so later upgrades can alter them. Install Java (OpenJDK 25 for 26.8, per the getting-started guide), unpack the same release from GitHub and put your files back:

Terminal
tar -xzf keycloak-26.8.0.tar.gz -C /opt && mv /opt/keycloak-26.8.0 /opt/keycloak
Terminal
tar -xzf keycloak-files.tar.gz -C /opt/keycloak
Terminal
chown -R keycloak:keycloak /opt/keycloak

Restore the TLS files, unit and environment, and change db-url-host or the password if they differ. Keep hostname identical: it is in every token's issuer. If you start with --optimized, build once as the service user, then start:

Terminal
sudo -u keycloak /opt/keycloak/bin/kc.sh build
Terminal
systemctl start keycloak
Terminal
curl -s https://auth.example.com/realms/myrealm/protocol/openid-connect/certs | jq -r '.keys[].kid'

The key IDs must match the old server's; save that output with your backups. Sign in as a normal user, then upgrade: a newer Keycloak migrates the database on its first start, and the upgrade guide's only way back is the old installation plus the dump. Testing restores covers making this a routine.

From an export instead, which is also how you leave dev-file or change database vendor: point the new Keycloak at an empty database and import before its first start. --override is true by default and replaces realms with the same name; --override false skips them.

Terminal
sudo -u keycloak /opt/keycloak/bin/kc.sh import --dir /var/backups/keycloak/export

Common errors

ErrorFix
The '--optimized' flag was used for first ever server start.A fresh unpack has no build yet. Run kc.sh build once, then start with --optimized.
The following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run:Your restored config sets a build option, such as db, that differs from the last build. Run kc.sh build again.
Key material not provided to setup HTTPSThe certificate files or keystore were not restored, or their paths changed. Put them back or fix the https-* options.
hostname is not configured; either configure hostname, or set hostname-strict to falsehostname was set in a file or variable you did not restore. Set it to the old value.
Possibly incorrect state of migration. You are trying to run server version '<old>' against database, which was already migrated to newer version '<new>'.A warning, not a stop: the dump comes from a newer Keycloak. Install that version instead of running an older one on its schema.
File name / realm name mismatch.An export file was renamed. Each must be named <realm>-realm.json after the realm inside it.
Realm '<name>' already exists. Import skipped--import-realm at startup never overwrites a realm. Run kc.sh import before starting instead.
Database may be already in useAn export or import on the dev-file (H2) database while Keycloak runs. Stop the server first.
pg_restore: error: input file appears to be a text format dump. Please use psql.The dump is plain SQL. Load it with psql -d keycloak -f <file>, or gunzip -c <file>.sql.gz | psql -d keycloak.
must be owner of tableRestored tables belong to another role, so an upgrade cannot alter them. Restore again with --no-owner --role=keycloak.

Frequently asked questions

Does a Keycloak realm export include users and passwords?
kc.sh export includes password hashes, client secrets and realm keys; --users skip leaves users out. The admin console's partial export has no users and masks secrets.
Can I export Keycloak while it is running?
No. Keycloak's guide says to stop every node first. For a live backup, dump the database with pg_dump.
Where does Keycloak store its data?
In the database set by the db option. The default dev-file database is an H2 file under data/h2, meant for development only.
Can I restore a Keycloak backup on a newer version?
Restore onto the same version first, then upgrade: a newer Keycloak migrates the database on its first start, and an older one should not run on a migrated database.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: