← Deploying Rundeck the Right Way

Chapter 4

Rundeck Installation & Configuration

In this chapter
<nav id="TableOfContents" aria-label="Chapter sections"> <ul> <li><a href="#installing-java">Installing Java</a></li> <li><a href="#adding-the-rundeck-repository">Adding the Rundeck Repository</a></li> <li><a href="#installing-the-mariadb-jdbc-driver">Installing the MariaDB JDBC Driver</a></li> <li><a href="#configuring-rundeck-configproperties">Configuring rundeck-config.properties</a> <ul> <li><a href="#database-connection">Database Connection</a></li> <li><a href="#the-grailsserverurl-setting">The grails.serverURL Setting</a></li> <li><a href="#forward-headers-for-reverse-proxy">Forward Headers for Reverse Proxy</a></li> <li><a href="#key-storage-encryption">Key Storage Encryption</a></li> </ul> </li> <li><a href="#configuring-frameworkproperties">Configuring framework.properties</a></li> <li><a href="#jvm-heap-tuning">JVM Heap Tuning</a></li> <li><a href="#selinux-configuration">SELinux Configuration</a></li> <li><a href="#firewall-configuration">Firewall Configuration</a></li> <li><a href="#apache-reverse-proxy-setup">Apache Reverse Proxy Setup</a> <ul> <li><a href="#install-apache-and-mod_ssl">Install Apache and mod_ssl</a></li> <li><a href="#deploy-ssl-certificates">Deploy SSL Certificates</a></li> <li><a href="#configure-the-proxy-virtual-host">Configure the Proxy Virtual Host</a></li> <li><a href="#start-apache">Start Apache</a></li> </ul> </li> <li><a href="#first-boot">First Boot</a></li> <li><a href="#verification">Verification</a> <ul> <li><a href="#check-the-service">Check the Service</a></li> <li><a href="#check-the-port">Check the Port</a></li> <li><a href="#access-the-web-ui">Access the Web UI</a></li> <li><a href="#confirm-mariadb-backend">Confirm MariaDB Backend</a></li> </ul> </li> <li><a href="#what-can-go-wrong">What Can Go Wrong</a> <ul> <li><a href="#rundeck-wont-start--address-already-in-use">Rundeck Won&rsquo;t Start &mdash; &ldquo;Address already in use&rdquo;</a></li> <li><a href="#apache-returns-503-through-the-proxy">Apache Returns 503 Through the Proxy</a></li> <li><a href="#login-redirects-to-httplocalhost4440">Login Redirects to http://localhost:4440</a></li> <li><a href="#rundeck-starts-but-the-ui-is-blank-or-throws-errors">Rundeck Starts But the UI Is Blank or Throws Errors</a></li> <li><a href="#database-connection-failures-on-startup">Database Connection Failures on Startup</a></li> </ul> </li> </ul> </nav>

What you’ll accomplish: Install Rundeck from the official RPM repository, connect it to your MariaDB backend, configure SSL via Apache reverse proxy, tune the JVM so it doesn’t fall over under load, and verify the web UI loads correctly.

This is the heaviest chapter in the guide. There are a lot of moving parts — Java, Rundeck itself, a JDBC driver, config files, a reverse proxy, SELinux, firewall rules, and JVM tuning — but every step builds on the last. By the end, you’ll have Rundeck running behind HTTPS with a MariaDB backend, which is a fundamentally different (and better) deployment than what the official quickstart gives you.


Installing Java

Rundeck is a Java application running on an embedded Jetty server. It needs a JRE to run. Java 11 is the safe, tested choice. Java 17 also works, but 11 has the most deployment hours behind it in the Rundeck community.

sudo dnf install -y java-11-openjdk

Verify the installation:

java -version

Expected output (version numbers may vary slightly):

openjdk version "11.0.x" 2024-xx-xx LTS
OpenJDK Runtime Environment (Red_Hat-11.0.x.x.x-x.el9) (build 11.0.x+x-LTS)
OpenJDK 64-Bit Server VM (Red_Hat-11.0.x.x.x-x.el9) (build 11.0.x+x-LTS, mixed mode, sharing)

If you see command not found, the package didn’t install. Check dnf list installed | grep openjdk.


Adding the Rundeck Repository

PagerDuty maintains an RPM repository for Rundeck. This is the cleanest install method on Rocky Linux — you get a proper systemd service unit, correct file ownership, and straightforward upgrades via dnf update.

sudo tee /etc/yum.repos.d/rundeck.repo > /dev/null << 'EOF'
[rundeck]
name=Rundeck - Release
baseurl=https://packages.rundeck.com/pagerduty/rundeck/rpm_any/rpm_any/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packages.rundeck.com/pagerduty/rundeck/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF

Warning: The Rundeck community repository does not GPG-sign its RPM packages (gpgcheck=0). The repo metadata itself is signed (repo_gpgcheck=1), which verifies you’re getting packages from the right source, but individual package integrity is not cryptographically verified. For a home lab, this is an acceptable trade-off. For anything with compliance requirements, you’d want to download the RPM manually and verify its checksum.

Now install Rundeck:

sudo dnf install -y rundeck

Use state: present (not latest) if you’re automating this with Ansible. Using latest means every playbook run potentially upgrades Rundeck, which is not what you want in a deployment that should be predictable.


Installing the MariaDB JDBC Driver

Rundeck doesn’t ship with a MariaDB driver. You need to download the MariaDB Connector/J JAR and place it where Rundeck can find it.

# Create the lib directory if it doesn't exist
sudo mkdir -p /var/lib/rundeck/lib

# Download MariaDB Connector/J 3.3.2
sudo curl -L -o /var/lib/rundeck/lib/mariadb-java-client-3.3.2.jar \
  https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/3.3.2/mariadb-java-client-3.3.2.jar

# Set ownership
sudo chown rundeck:rundeck /var/lib/rundeck/lib/mariadb-java-client-3.3.2.jar

MariaDB Connector/J is purpose-built for MariaDB and handles its protocol natively. This is the driver the bundled Ansible playbooks use, so your manual setup will match exactly what the automation deploys.


Configuring rundeck-config.properties

This is where the important decisions live. Open the main configuration file:

sudo vi /etc/rundeck/rundeck-config.properties

Here are the key settings. I’ll explain what matters and why.

Database Connection

Find these two H2 datasource lines (they’ll be near the top of the file):

dataSource.dbCreate = none
dataSource.url = jdbc:h2:file:/var/lib/rundeck/data/rundeckdb;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=MONTH,HOUR,MINUTE,YEAR,SECONDS

Delete them and add the following MariaDB configuration in their place:

# --- Database Configuration (MariaDB) ---
dataSource.driverClassName = org.mariadb.jdbc.Driver
dataSource.url = jdbc:mariadb://localhost/rundeck_db?autoReconnect=true&useSSL=false
dataSource.username = rundeck
dataSource.password = your-password-here
dataSource.dialect = org.hibernate.dialect.MariaDBDialect
dataSource.properties.maxActive = 50
dataSource.properties.maxIdle = 25
dataSource.properties.minIdle = 5
dataSource.properties.initialSize = 5
dataSource.properties.maxWait = 10000

Leave everything else in the file untouched — the loglevel, rdeck.base, rss.enabled, encryption settings, and grails.plugin.databasemigration lines should all stay as they are.

Replace your-password-here with the same password you set for the rundeck database user in Chapter 3. If you’re managing this with Ansible, the password comes from vault: {{ vault_rundeck_db_password }}.

Pro tip: For very small home labs (fewer than 5 concurrent jobs), maxActive=20 is plenty. The connection pool settings above are generous but harmless — unused connections don’t consume meaningful resources.

The grails.serverURL Setting

This is the single most misconfigured setting in Rundeck. Get it wrong and you’ll get redirect loops, broken API calls, and CSRF token errors that make you question your career choices.

Find the existing grails.serverURL line (it will say http://localhost:4440) and change it to your actual URL:

grails.serverURL = https://rundeck.example.com

The rules:

  • It must match the URL in the browser exactly. If users access Rundeck at https://rundeck.example.com, that’s what goes here.
  • Include the protocol (https://).
  • Do NOT include a trailing slash.
  • Do NOT include a port number unless you’re using a non-standard port (not 443).
  • If you’re using the reverse proxy (and you should be), this is the proxy URL, not http://localhost:4440.

Forward Headers for Reverse Proxy

When Apache sits in front of Rundeck, Rundeck needs to trust the forwarded headers so it knows the original request came over HTTPS. This line doesn’t exist in the default config — add it anywhere in the file:

server.useForwardHeaders = true

Without this, Rundeck thinks all requests are HTTP and generates HTTP URLs in its responses, which breaks everything when the browser expects HTTPS.

Key Storage Encryption

Rundeck ships with Jasypt encryption already configured in rundeck-config.properties. You’ll see two encryption blocks near the bottom of the file — one for key storage and one for project config storage:

# Encryption for key storage
rundeck.storage.provider.1.type=db
rundeck.storage.provider.1.path=keys
rundeck.storage.converter.1.type=jasypt-encryption
rundeck.storage.converter.1.config.password=<auto-generated>
...

# Encryption for project config storage
rundeck.projectsStorageType=db
rundeck.config.storage.converter.1.type=jasypt-encryption
rundeck.config.storage.converter.1.config.password=<auto-generated>
...

Leave these blocks as-is. Rundeck generates a random encryption password on first install. If you change it later, any keys or project config already encrypted with the original password become unreadable.

Note: If you’re managing this with Ansible, the playbook sets the encryption password from vault ({{ vault_rundeck_storage_encryption_password }}), which is fine because the password is set before any keys are stored. For a manual install, just use the auto-generated password.


Configuring framework.properties

The framework config mirrors some of the settings from rundeck-config.properties but from Rundeck’s internal framework perspective:

sudo vi /etc/rundeck/framework.properties

The key lines to set:

# /etc/rundeck/framework.properties

framework.server.url = https://rundeck.example.com
framework.server.port = 443

# SSH defaults for node execution
framework.ssh.keypath = /var/lib/rundeck/.ssh/id_ed25519
framework.ssh.user = rundeck

# ... (rest of file unchanged)

The framework.server.url must match grails.serverURL. Yes, you configure the URL in two places. No, it’s not ideal, but that’s how Rundeck works.


JVM Heap Tuning

This is the setting everyone gets wrong, and it’s the reason half the “Rundeck is slow” posts exist on forums and GitHub issues.

The default JVM heap allocation is too small for real use. Rundeck loads all job definitions and recent execution metadata into memory. Even a modest home lab with a few dozen jobs and a week of execution history can push past the default heap limit.

Create the sysconfig override file (it doesn’t exist by default):

sudo vi /etc/sysconfig/rundeckd

Add or modify the JVM options:

# /etc/sysconfig/rundeckd
#
# JVM heap settings:
#   -Xmx = maximum heap size (ceiling)
#   -Xms = initial heap size (floor)
#
# Minimum for real use: -Xmx2048m
# Recommended for home lab: -Xmx2048m to -Xmx4096m
# If your VM has 8 GB RAM, use -Xmx4096m
# If your VM has 4 GB RAM, use -Xmx2048m (don't go lower)

RDECK_JVM_OPTS="$RDECK_JVM_OPTS -Xmx2048m -Xms1024m"

What happens if you skip this:

  1. Rundeck starts fine and works for a few days.
  2. Execution history accumulates. Job definitions pile up.
  3. The JVM runs out of heap space.
  4. Rundeck becomes unresponsive — the UI loads slowly or not at all.
  5. Eventually the Linux OOM killer terminates the process.
  6. You find OutOfMemoryError buried in /var/log/rundeck/service.log and wonder why Rundeck didn’t just say something obvious.

Set the heap now and save yourself the debugging session later.


SELinux Configuration

If SELinux is enforcing (and on Rocky Linux 9 it should be), you need to tell it that Apache is allowed to make network connections. Without this, Apache cannot proxy requests to Rundeck’s backend port, and you’ll get 503 errors with no useful information in the default logs.

# Check current state
getsebool httpd_can_network_connect

If it returns httpd_can_network_connect --> off, fix it:

# The -P flag makes this persistent across reboots
sudo setsebool -P httpd_can_network_connect on

This is the single most common deployment blocker on Rocky Linux. I’ve seen experienced sysadmins spend hours troubleshooting proxy failures before checking SELinux. The symptom is maddening: Rundeck works fine on http://localhost:4440, Apache is running, the proxy config looks correct, but HTTPS returns 503. SELinux is silently blocking the connection.

If you want to see what SELinux is blocking (useful for debugging other issues too):

sudo ausearch -m avc -ts recent

Firewall Configuration

The firewall strategy is simple: expose only port 443 (HTTPS through Apache). Everything else stays on loopback.

First, check if firewalld is installed and running:

sudo systemctl status firewalld

If firewalld is active, open HTTPS and verify:

# Open HTTPS
sudo firewall-cmd --permanent --add-service=https

# Reload to apply
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-all

You should see https in the services list. Ports 4440 (Rundeck HTTP) and 3306 (MariaDB) should NOT be listed — they’re only accessed via localhost.

Note: Some minimal installs (especially cloud images and AMIs) don’t include firewalld. If systemctl status firewalld says Unit firewalld.service could not be found, install it with sudo dnf install -y firewalld && sudo systemctl enable --now firewalld, then run the commands above. If your host uses a different firewall (e.g., cloud security groups or iptables directly), ensure port 443 is open and ports 4440 and 3306 are not exposed.


Apache Reverse Proxy Setup

Apache httpd sits in front of Rundeck, terminating SSL and forwarding requests to Rundeck’s internal HTTP port. This is the recommended architecture because it separates SSL certificate management from Java keystore complexity, and it lets you use standard tools (certbot, mod_security) at the proxy layer.

Install Apache and mod_ssl

sudo dnf install -y httpd mod_ssl

Deploy SSL Certificates

If you already have a certificate (from Let’s Encrypt, your CA, etc.), copy it into place:

sudo cp your-certificate.crt /etc/pki/tls/certs/rundeck.example.com.crt
sudo cp your-private-key.key /etc/pki/tls/private/rundeck.example.com.key
sudo chmod 600 /etc/pki/tls/private/rundeck.example.com.key

If you need a self-signed certificate (perfectly fine for a home lab), generate one now:

sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /etc/pki/tls/private/rundeck.example.com.key \
  -out /etc/pki/tls/certs/rundeck.example.com.crt \
  -subj "/CN=rundeck.example.com"

Replace rundeck.example.com with your actual hostname or IP address (e.g., /CN=192.168.1.50). The -nodes flag means the key won’t be password-protected, which is what you want for a service that starts automatically.

Lock down the key file:

sudo chmod 600 /etc/pki/tls/private/rundeck.example.com.key

Note: Your browser will show a certificate warning with a self-signed cert. That’s expected — the connection is still encrypted. If you have a domain and want a trusted cert later, Let’s Encrypt with certbot is free and straightforward.

Configure the Proxy Virtual Host

Create a dedicated config file (don’t patch the default ssl.conf — it gets overwritten on updates):

sudo vi /etc/httpd/conf.d/rundeck-proxy.conf
# /etc/httpd/conf.d/rundeck-proxy.conf
<VirtualHost *:443>
    ServerName rundeck.example.com

    SSLEngine on
    SSLCertificateFile /etc/pki/tls/certs/rundeck.example.com.crt
    SSLCertificateKeyFile /etc/pki/tls/private/rundeck.example.com.key

    # Tell Rundeck the original request was HTTPS
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"

    ProxyPreserveHost On
    ProxyPass / http://localhost:4440/
    ProxyPassReverse / http://localhost:4440/
</VirtualHost>

Warning: If you’re using the default ssl.conf that ships with mod_ssl, it also listens on port 443 and will conflict. Either remove the default <VirtualHost> block from /etc/httpd/conf.d/ssl.conf or rename the file to ssl.conf.bak.

Start Apache

sudo systemctl enable --now httpd

First Boot

Everything is configured. Time to start Rundeck and see if it all comes together.

sudo systemctl enable --now rundeckd

The first boot takes longer than subsequent starts — 60 to 90 seconds is normal. Rundeck is initializing the database schema, creating default projects, and generating encryption keys.

Note: The log file /var/log/rundeck/service.log won’t exist immediately. Rundeck’s Java process takes 10–20 seconds to start producing output. If tail says “No such file or directory”, wait a moment and try again.

Watch for the log file to appear, then follow it:

# Wait for the log file to be created, then tail it
while [ ! -f /var/log/rundeck/service.log ]; do sleep 2; done
sudo tail -f /var/log/rundeck/service.log

You’re looking for a line like:

Grails application running at http://localhost:4440 in environment: production

Once you see that, Rundeck is ready. Press Ctrl+C to stop tailing.


Verification

Check the Service

sudo systemctl status rundeckd

Should show active (running).

Check the Port

ss -tlnp | grep 4440

Should show Rundeck listening on port 4440. By default it binds to all interfaces (*:4440). This is fine — Apache handles external access on port 443, and your firewall should not expose port 4440 to the network.

Access the Web UI

Open a browser and navigate to https://rundeck.example.com. You should see the Rundeck login page.

Default credentials:

  • Username: admin
  • Password: admin

Change the admin password immediately after first login. You can do this in System Menu > User Profiles or by editing /etc/rundeck/realm.properties:

# /etc/rundeck/realm.properties
# Format: username:password,role
admin:your-new-password-here,user,admin

Restart Rundeck after editing realm.properties:

sudo systemctl restart rundeckd

Confirm MariaDB Backend

After logging in, go to the gear icon (System Menu) and look at System Information. Under the database section, you should see MariaDB listed as the database vendor, not H2. If you see H2, your rundeck-config.properties datasource settings aren’t being picked up — double-check the file and restart Rundeck.


What Can Go Wrong

Rundeck Won’t Start — “Address already in use”

Symptom: service.log shows java.net.BindException: Address already in use on port 4440.

Cause: Another process is using port 4440, or a previous Rundeck instance didn’t shut down cleanly.

Fix:

# Find what's using the port
sudo ss -tlnp | grep 4440
# Kill the process if it's a stale Rundeck instance
sudo kill <pid>
# Then start Rundeck
sudo systemctl start rundeckd

Apache Returns 503 Through the Proxy

Symptom: https://rundeck.example.com returns a 503 error. Rundeck itself works at http://localhost:4440.

Cause: Almost certainly SELinux. Check:

getsebool httpd_can_network_connect

If it’s off, fix it:

sudo setsebool -P httpd_can_network_connect on

Login Redirects to http://localhost:4440

Symptom: You access https://rundeck.example.com, the login page loads, but after logging in you get redirected to http://localhost:4440.

Cause: grails.serverURL in rundeck-config.properties is wrong. It’s either still set to the default or doesn’t match your actual URL.

Fix: Set grails.serverURL = https://rundeck.example.com (matching your browser URL exactly), ensure server.useForwardHeaders = true, and restart Rundeck.

Rundeck Starts But the UI Is Blank or Throws Errors

Symptom: The login page loads but after logging in, the UI is broken — missing elements, JavaScript errors.

Cause: Usually a grails.serverURL protocol mismatch. Rundeck is generating HTTP URLs for assets but the browser expects HTTPS, triggering mixed-content blocking.

Fix: Same as above — verify grails.serverURL uses https:// and server.useForwardHeaders = true is set.

Database Connection Failures on Startup

Symptom: service.log shows JDBC connection errors or Access denied for user 'rundeck'.

Cause: Either MariaDB isn’t running, the credentials are wrong, or the database doesn’t exist.

Fix:

# Is MariaDB running?
sudo systemctl status mariadb

# Can you connect manually?
mysql -u rundeck -p -e "SELECT 1;" rundeck_db

If the manual connection fails, revisit Chapter 3’s MariaDB setup. If it succeeds but Rundeck can’t connect, check that the password in rundeck-config.properties matches exactly (watch for trailing whitespace).

Want the automation code? Get the production-ready Ansible playbooks that deploy everything in this guide in ~10 minutes.

Get Playbooks — $14