Migrating Rundeck from H2 to PostgreSQL: A Complete Guide
Step-by-step walkthrough — from zero-downtime backup to production-ready database switch
If you've been running Rundeck with its default H2 embedded database, you've probably already hit the wall: slow queries as job history grows, no proper connection pooling, and the constant anxiety of a file-based database in production. PostgreSQL fixes all of that.
This guide walks you through two migration paths — pick the one that fits your situation.
Why Migrate Away from H2?
Rundeck ships with H2 for a reason: zero-config, works out of the box. But H2 has real limitations at scale:
No concurrent writes — H2 struggles under parallel job execution
File corruption risk — a hard shutdown can corrupt the
.mv.dbfileNo external tooling — can't use pg_dump, replication, or your existing DB monitoring
Memory pressure — H2 loads data into the JVM heap
PostgreSQL gives you connection pooling, proper ACID guarantees, pg_dump for backups, and the ability to run Standby replicas. For any team running more than a handful of jobs, it's the right call.
Before You Start: Understand What Lives in the Database
Rundeck stores the following in its database:
| Data Type | Migration Notes |
|---|---|
| Project definitions | Exportable via API archive |
| Job definitions | Included in project export |
| Execution history & logs | Log files stay on disk; metadata in DB |
| Users & ACL tokens | Recreate manually or via config |
| Scheduled jobs | Preserved in job definitions |
Execution log content lives in /var/lib/rundeck/logs/, not the database — so you don't need to migrate that.
Two Migration Strategies
Strategy A: Clean Start + Project Import (Recommended)
Best for: Most teams. You keep all your job definitions and project configs. You lose execution history records in the DB (but not the actual log files on disk).
Effort: Low | Risk: Low | Downtime: ~30 minutes
Strategy B: Full H2 → PostgreSQL Data Migration
Best for: Teams that need to preserve execution history records in the DB for audit/compliance.
Effort: High | Risk: Medium | Downtime: 1–3 hours
Strategy A: Clean Start + Project Import
Step 1 — Back Up Everything
# Stop Rundeck first
systemctl stop rundeckd
# Back up config files
cp /etc/rundeck/rundeck-config.properties /backup/
cp /etc/rundeck/framework.properties /backup/
# Back up the H2 database files (just in case)
cp -r /var/lib/rundeck/data /backup/rundeck-data-h2/
# Back up project files on disk
cp -r /var/lib/rundeck/projects /backup/rundeck-projects/
cp -r /var/lib/rundeck/logs /backup/rundeck-logs/
Export each project via the Rundeck API (Rundeck must be running for this):
# Start Rundeck temporarily to export
systemctl start rundeckd
# Export all projects via API
TOKEN="your-api-token"
RUNDECK_URL="http://localhost:4440"
for project in \((curl -s -H "X-Rundeck-Auth-Token: \)TOKEN" \
"$RUNDECK_URL/api/41/projects" | jq -r '.[].name'); do
echo "Exporting: $project"
curl -s -H "X-Rundeck-Auth-Token: $TOKEN" \
"\(RUNDECK_URL/api/41/project/\)project/export" \
-o "/backup/project-${project}.zip"
done
# Stop again before DB switch
systemctl stop rundeckd
Step 2 — Install and Configure PostgreSQL
# Ubuntu/Debian
apt-get install -y postgresql postgresql-contrib
# CentOS/RHEL 8+
dnf install -y postgresql-server postgresql-contrib
postgresql-setup --initdb
systemctl enable --now postgresql
Create the Rundeck database and user:
su - postgres
psql << 'EOF'
CREATE DATABASE rundeck ENCODING 'UTF8';
CREATE USER rundeckuser WITH PASSWORD 'your_strong_password_here';
GRANT ALL PRIVILEGES ON DATABASE rundeck TO rundeckuser;
\c rundeck
GRANT ALL PRIVILEGES ON SCHEMA public TO rundeckuser;
EOF
Step 3 — Configure pg_hba.conf
Edit /etc/postgresql/<version>/main/pg_hba.conf (Debian/Ubuntu) or /var/lib/pgsql/data/pg_hba.conf (RHEL):
# Allow rundeckuser from localhost
host rundeck rundeckuser 127.0.0.1/32 scram-sha-256
host rundeck rundeckuser ::1/128 scram-sha-256
Reload PostgreSQL:
systemctl reload postgresql
Verify connectivity:
psql -U rundeckuser -h 127.0.0.1 -d rundeck -c "SELECT version();"
Step 4 — Update rundeck-config.properties
Open /etc/rundeck/rundeck-config.properties and replace the H2 datasource block:
# === REMOVE or comment out the H2 config ===
# dataSource.url = jdbc:h2:file:/var/lib/rundeck/data/rundeckdb;...
# === ADD PostgreSQL config ===
dataSource.driverClassName = org.postgresql.Driver
dataSource.url = jdbc:postgresql://127.0.0.1:5432/rundeck
dataSource.username = rundeckuser
dataSource.password = your_strong_password_here
dataSource.dialect = org.hibernate.dialect.PostgreSQLDialect
# Connection pool tuning (recommended)
dataSource.properties.maxActive = 50
dataSource.properties.maxIdle = 25
dataSource.properties.minIdle = 5
dataSource.properties.initialSize = 5
dataSource.properties.validationQuery = SELECT 1
dataSource.properties.testOnBorrow = true
Note: Rundeck 4.x+ bundles the PostgreSQL JDBC driver — no manual download needed. For older versions, download
postgresql-<version>.jarand place it in/var/lib/rundeck/lib/.
Step 5 — Start Rundeck and Let It Initialize
systemctl start rundeckd
# Watch the logs for successful schema creation
tail -f /var/log/rundeck/service.log
Look for:
Hibernate: create table rduser ...
Grails application running at http://localhost:4440/
If you see Connection refused or FATAL: password authentication failed, double-check pg_hba.conf and your credentials.
Step 6 — Import Projects Back
TOKEN="your-new-api-token"
RUNDECK_URL="http://localhost:4440"
for zipfile in /backup/project-*.zip; do
project=\((basename "\)zipfile" .zip | sed 's/project-//')
echo "Importing: $project"
# Create project first if it doesn't exist
curl -s -X POST \
-H "X-Rundeck-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$project\"}" \
"$RUNDECK_URL/api/41/projects"
# Import archive
curl -s -X POST \
-H "X-Rundeck-Auth-Token: $TOKEN" \
-H "Content-Type: application/zip" \
--data-binary "@$zipfile" \
"\(RUNDECK_URL/api/41/project/\)project/import?importExecutions=true"
done
Strategy B: Full Data Migration from H2
Use this only if you need historical execution records preserved in the database.
The Core Challenge
H2 and PostgreSQL have different SQL dialects. H2's SCRIPT TO command exports valid H2 SQL, but that SQL won't run cleanly in PostgreSQL without transformation.
Step 1 — Export H2 with the H2 Console
Identify the H2 version Rundeck is using:
find /var/lib/rundeck -name "h2-*.jar" 2>/dev/null
# or
ls /var/lib/rundeck/bootstrap/
Run the H2 Script tool with the matching version:
systemctl stop rundeckd
java -cp /path/to/h2-<version>.jar org.h2.tools.Script \
-url "jdbc:h2:/var/lib/rundeck/data/rundeckdb" \
-user sa \
-password "" \
-script /tmp/rundeck_h2_export.sql
Step 2 — Transform the SQL
cp /tmp/rundeck_h2_export.sql /tmp/rundeck_pg_import.sql
# Remove H2-specific statements
sed -i '/^SET /d' /tmp/rundeck_pg_import.sql
sed -i '/^CREATE USER IF NOT EXISTS/d' /tmp/rundeck_pg_import.sql
sed -i '/^ALTER TABLE.*ADD CONSTRAINT.*CHECK/d' /tmp/rundeck_pg_import.sql
# Fix identity columns
sed -i 's/BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH [0-9]* INCREMENT BY 1)/BIGSERIAL/g' \
/tmp/rundeck_pg_import.sql
# Fix boolean literals
sed -i "s/\bTRUE\b/true/g; s/\bFALSE\b/false/g" /tmp/rundeck_pg_import.sql
Tip: H2 exports may include vendor-specific syntax that varies by version. Always test imports in a staging environment first and fix errors iteratively.
Step 3 — Import to PostgreSQL
psql -U rundeckuser -d rundeck -f /tmp/rundeck_pg_import.sql 2>&1 | tee /tmp/import_log.txt
# Check for errors
grep -i "error\|fatal" /tmp/import_log.txt
Fix any errors, then update rundeck-config.properties (same as Strategy A, Step 4) and restart.
Post-Migration Verification
Run this checklist after switching to PostgreSQL:
# 1. Service status
systemctl status rundeckd
# 2. Database tables created
psql -U rundeckuser -d rundeck -c "\dt" | wc -l
# Should show 30+ tables
# 3. Check for connection errors in logs
grep -i "HikariPool\|Cannot acquire connection\|ORA-\|PSQLException" \
/var/log/rundeck/service.log | tail -20
# 4. Test a job execution end-to-end
# 5. Verify ACL tokens still work
# 6. Confirm scheduled jobs are firing
Performance Tuning Tips
Once you're on PostgreSQL, a few quick wins:
Connection pooling — Rundeck uses HikariCP internally. The defaults are conservative; bump them for busy instances:
dataSource.properties.maximumPoolSize = 50
dataSource.properties.minimumIdle = 10
dataSource.properties.connectionTimeout = 30000
dataSource.properties.idleTimeout = 600000
PostgreSQL autovacuum — Execution history tables grow fast. Make sure autovacuum is running and work_mem is set appropriately in postgresql.conf:
work_mem = 64MB
maintenance_work_mem = 256MB
autovacuum = on
Indexes — If execution history queries are slow, check that the execution table indexes on project, date_started, and status exist.
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
FATAL: password authentication failed |
Wrong password or pg_hba auth method | Check credentials + pg_hba.conf |
Connection refused (127.0.0.1:5432) |
PostgreSQL not running | systemctl start postgresql |
Permission denied for schema public |
PostgreSQL 15+ changed defaults | Run GRANT ALL ON SCHEMA public TO rundeckuser |
No suitable driver found |
Missing JDBC jar (old Rundeck) | Add postgresql.jar to /var/lib/rundeck/lib/ |
Table already exists |
Partial init ran before | Drop and recreate the database, restart clean |
UnsupportedOperationException: dialect |
Missing dialect config | Add dataSource.dialect = org.hibernate.dialect.PostgreSQLDialect |
Wrapping Up
Migrating Rundeck to PostgreSQL is a one-time investment that pays off immediately in stability, performance, and operational visibility. Strategy A (clean start + project import) is the pragmatic path for most teams and can be done in under an hour.
If you're running Rundeck on Kubernetes (KubernetesExecutor or similar), the same rundeck-config.properties approach applies — just mount the config as a Secret and point the JDBC URL at your PostgreSQL service.

