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.
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.
| Part | Where | What it holds |
|---|---|---|
| Database | keycloak on PostgreSQL, MySQL, MariaDB, SQL Server or Oracle | Realms, 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/conf | keycloak.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 |
| Environment | The systemd unit and its EnvironmentFile, or the compose file and .env | KC_* variables override keycloak.conf, so the database password may only live here |
providers/, themes/ | /opt/keycloak | Custom provider JARs and themes. The upgrade guide copies both to every new installation. |
| TLS files | Where https-certificate-file and https-certificate-key-file point | Not needed when a reverse proxy terminates TLS |
| Version | bin/kc.sh --version | Prints 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
install -d -m 700 /var/backups/keycloakrunuser -u postgres -- pg_dump -Fc keycloak > /var/backups/keycloak/keycloak-db.dumprunuser -u postgresruns pg_dump as thepostgressystem user, which logs in over the local socket without a password; root's shell writes the file.-Fcwrites PostgreSQL's compressed custom format, whichpg_restorecan 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-hostanddb-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.
tar -czf /var/backups/keycloak/keycloak-files.tar.gz -C /opt/keycloak conf providers themesAutomate it
One script does both, nightly from cron:
#!/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 -delete15 3 * * * root /usr/local/bin/keycloak-backup.sh >> /var/log/keycloak-backup.log 2>&1The 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:
systemctl stop keycloaksudo -u keycloak /opt/keycloak/bin/kc.sh export --dir /var/backups/keycloak/export --users realm_file --optimizedsystemctl start keycloak--dircreates the folder and writes<realm>-realm.jsonper realm.--fileputs everything in one file, which the guide says to avoid above 50,000 users.--realm <name>exports one realm; the default is every realm,masterand its admin accounts included.--users:different_files(the default, 50 users per file, changed with--users-per-file),same_file,realm_fileorskip.--optimizedskips the build check. Use it when the server starts withstart --optimized; otherwise the export may rebuild Keycloak and change its next start. Without a priorkc.sh build, leave it out.- The export reads
keycloak.conflike 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:
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:
install -d -o 1000 -m 700 /var/backups/keycloak/exportdocker compose stop keycloakdocker compose run --rm -v /var/backups/keycloak/export:/opt/keycloak/data/export keycloak export --dir /opt/keycloak/data/export --users realm_filedocker compose start keycloakThe 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:
sudo -u postgres createuser --pwprompt keycloaksudo -u postgres createdb --owner=keycloak keycloakrunuser -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:
tar -xzf keycloak-26.8.0.tar.gz -C /opt && mv /opt/keycloak-26.8.0 /opt/keycloaktar -xzf keycloak-files.tar.gz -C /opt/keycloakchown -R keycloak:keycloak /opt/keycloakRestore 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:
sudo -u keycloak /opt/keycloak/bin/kc.sh buildsystemctl start keycloakcurl -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.
sudo -u keycloak /opt/keycloak/bin/kc.sh import --dir /var/backups/keycloak/exportCommon errors
| Error | Fix |
|---|---|
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 HTTPS | The 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 false | hostname 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 use | An 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 table | Restored 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:
- Keycloak: Importing and exporting realms
- Keycloak: Configuring the database
- Keycloak: All configuration
- Keycloak: Configuring Keycloak (sources, show-config, optimized builds)
- Keycloak: Running Keycloak in a container
- Keycloak: Configuring the hostname (v2)
- Keycloak: Configuring TLS
- Keycloak: Health checks
- Keycloak: OpenID Connect endpoints
- Keycloak: Getting started with OpenJDK
- Keycloak Upgrading Guide (preparing, migrating the database)
- Keycloak 26.8.0 source: Database.java (dev-file H2 path)
- Keycloak 26.8.0 source: ExportUtils.java (what an export writes)
- Keycloak 26.8.0 source: DirImportProvider.java (file names, import errors)
- Keycloak 26.8.0 source: DefaultMigrationManager.java (version warnings)
- Keycloak 26.8.0 source: Messages.java and Picocli.java (startup errors)
- Keycloak 26.8.0 source: HostnameV2ProviderFactory.java
- Keycloak 26.8.0 source: container Dockerfile (USER 1000)
- H2 2.4.240: error messages (90020)
- PostgreSQL documentation: pg_dump
- PostgreSQL documentation: pg_restore
- Docker documentation: docker compose run