# Migrating Rundeck from H2 to PostgreSQL: A Complete Guide

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.db` file
    
*   **No 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

```bash
# 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):

```bash
# 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

```bash
# 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:

```bash
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):

```plaintext
# 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:

```bash
systemctl reload postgresql
```

Verify connectivity:

```bash
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:

```properties
# === 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>.jar` and place it in `/var/lib/rundeck/lib/`.

* * *

### Step 5 — Start Rundeck and Let It Initialize

```bash
systemctl start rundeckd

# Watch the logs for successful schema creation
tail -f /var/log/rundeck/service.log
```

Look for:

```plaintext
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

```bash
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:

```bash
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:

```bash
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

```bash
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

```bash
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:

```bash
# 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:

```properties
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`:

```plaintext
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.
