Rundeck is an open-source runbook automation server: it stores operational procedures as jobs, runs them over SSH on groups of servers, keeps a full log of who ran what, and lets you hand specific jobs to other people without giving them shell access. In this tutorial you will install Rundeck on Ubuntu 24.04 from the official repository, publish it over HTTPS with Nginx, register remote nodes, load two incident response jobs from YAML, restrict them to an on-call group with an ACL policy and trigger one from a webhook.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS with at least 2 CPUs and 4 GB of RAM for a small team, for example a CubePath VPS. Upstream recommends 8 GB for production use.
- A non-root user with
sudoprivileges and UFW enabled with OpenSSH allowed. - A domain name with an
Arecord for a subdomain such asrundeck.your_domainpointing to the server. - One or more Linux servers to manage (the "nodes"), reachable from the Rundeck server over SSH. This guide calls them
web01andweb02.
Replace rundeck.your_domain and the example IP addresses with your own values.
Step 1 - Installing Java
Rundeck 6 runs on Java 17, 21 or 25. Install the Java 21 runtime from the Ubuntu repositories:
sudo apt update
sudo apt install openjdk-21-jre-headless
Check the version:
java -version
openjdk version "21.0.8" 2025-07-15
OpenJDK Runtime Environment (build 21.0.8+9-Ubuntu-0ubuntu124.04.1)
OpenJDK 64-Bit Server VM (build 21.0.8+9-Ubuntu-0ubuntu124.04.1, mixed mode, sharing)
Step 2 - Installing Rundeck from the official repository
Download the repository signing key into /etc/apt/keyrings:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://packages.rundeck.com/pagerduty/rundeck/gpgkey | sudo gpg --dearmor -o /etc/apt/keyrings/rundeck.gpg
Add the repository, restricted to that key:
echo "deb [signed-by=/etc/apt/keyrings/rundeck.gpg] https://packages.rundeck.com/pagerduty/rundeck/any/ any main" | sudo tee /etc/apt/sources.list.d/rundeck.list
Install the server and the rd command-line client:
sudo apt update
sudo apt install rundeck rundeck-cli
Confirm what was installed:
apt policy rundeck | head -n 3
rundeck:
Installed: 6.2.1.20260909-1
Candidate: 6.2.1.20260909-1
The package creates a rundeck system user with its home in /var/lib/rundeck, puts configuration in /etc/rundeck and logs in /var/log/rundeck. The service is called rundeckd.
Step 3 - Configuring Rundeck before the first start
Rundeck ships with two demo accounts, admin:admin and user:user. Replace them before the service ever starts. Open the user file:
sudo nano /etc/rundeck/realm.properties
Find the account lines at the end of the file and change them so only the admin remains, with a strong password:
admin:your_strong_password,user,admin
This is a temporary clear-text password; in Step 5 you will replace it with a BCRYPT hash. Restrict the file so only root and the rundeck group can read it:
sudo chown root:rundeck /etc/rundeck/realm.properties
sudo chmod 640 /etc/rundeck/realm.properties
Next, tell Rundeck its public URL. It uses this value in links, emails, API responses and webhook URLs:
sudo sed -i 's|^grails.serverURL=.*|grails.serverURL=https://rundeck.your_domain|' /etc/rundeck/rundeck-config.properties
sudo sed -Ei 's|^(framework.server.url[[:space:]]*=).*|\1 https://rundeck.your_domain|' /etc/rundeck/framework.properties
grep -h 'serverURL\|framework.server.url' /etc/rundeck/rundeck-config.properties /etc/rundeck/framework.properties
grails.serverURL=https://rundeck.your_domain
framework.server.url = https://rundeck.your_domain
Finally, make Rundeck listen on localhost only and honor the X-Forwarded-* headers Nginx will send. On Debian and Ubuntu, JVM options go in /etc/default/rundeckd (do not edit /etc/rundeck/profile, which upgrades overwrite):
sudo nano /etc/default/rundeckd
RDECK_JVM_OPTS="-Dserver.address=127.0.0.1 -Drundeck.jetty.connector.forwarded=true"
RDECK_JVM_SETTINGS="-Xmx2g -Xms512m -server"
RDECK_JVM_SETTINGS sets the Java heap. Around half of the server's RAM is a reasonable starting point.
Enable and start the service. The first start takes one to two minutes while Rundeck creates its database:
sudo systemctl enable --now rundeckd
sudo tail -f /var/log/rundeck/service.log
Wait until the log shows a line similar to Grails application running at http://127.0.0.1:4440 in environment: production, then press CTRL+C. Check that the login page answers locally:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4440/user/login
200
Step 4 - Publishing Rundeck with Nginx and HTTPS
Install Nginx and Certbot:
sudo apt install nginx certbot python3-certbot-nginx
Create a server block:
sudo nano /etc/nginx/sites-available/rundeck
server {
listen 80;
listen [::]:80;
server_name rundeck.your_domain;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:4440;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 300s;
}
}
Enable it, test and reload:
sudo ln -s /etc/nginx/sites-available/rundeck /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Allow web traffic and request a certificate:
sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d rundeck.your_domain
Port 4440 stays closed because Rundeck only listens on 127.0.0.1. Open https://rundeck.your_domain and log in as admin with the password from Step 3.
Step 5 - Hashing the admin password and getting an API token
Rundeck recommends BCRYPT hashes in realm.properties. In the web interface, click the gear icon in the top right and choose Password Utility. Select BCRYPT, type the admin password and copy the result, which starts with BCRYPT:.
Edit the user file again:
sudo nano /etc/rundeck/realm.properties
Replace the clear-text password with the hash:
admin:BCRYPT:$2a$10$your_generated_hash,user,admin
The default login module reads this file at startup, so restart Rundeck and log in again to confirm the password still works:
sudo systemctl restart rundeckd
Now create an API token for the rd client. Click the user icon, open Profile and, under User API Tokens, generate a new token for the admin role. Copy it, then export it together with the server URL in your shell on the Rundeck server:
export RD_URL=https://rundeck.your_domain
export RD_TOKEN=your_api_token
Test the connection:
rd projects list
The command should return an empty project list without errors. A 401 response means the token is wrong or was generated for a different server URL.
Step 6 - Preparing SSH access to the nodes
Rundeck runs commands on nodes over SSH as a regular user. Create a key pair owned by the rundeck user on the Rundeck server:
sudo install -d -o rundeck -g rundeck -m 700 /var/lib/rundeck/.ssh
sudo -u rundeck ssh-keygen -t ed25519 -N '' -C 'rundeck' -f /var/lib/rundeck/.ssh/id_ed25519
sudo cat /var/lib/rundeck/.ssh/id_ed25519.pub
On each node, create a dedicated account for Rundeck, add it to the adm group so it can read the system journal, and install the public key you just printed:
sudo adduser --disabled-password --gecos '' rundeck-exec
sudo usermod -aG adm rundeck-exec
sudo install -d -o rundeck-exec -g rundeck-exec -m 700 /home/rundeck-exec/.ssh
echo 'ssh-ed25519 AAAA...your_public_key... rundeck' | sudo tee /home/rundeck-exec/.ssh/authorized_keys
sudo chown rundeck-exec:rundeck-exec /home/rundeck-exec/.ssh/authorized_keys
sudo chmod 600 /home/rundeck-exec/.ssh/authorized_keys
The restart runbook needs to restart Nginx and nothing else. Grant exactly that command through a sudoers drop-in, and validate the file with visudo:
echo 'rundeck-exec ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx' | sudo tee /etc/sudoers.d/rundeck-exec
sudo chmod 440 /etc/sudoers.d/rundeck-exec
sudo visudo -cf /etc/sudoers.d/rundeck-exec
/etc/sudoers.d/rundeck-exec: parsed OK
Back on the Rundeck server, connect once to each node as the rundeck user. This confirms the key works and records the node's host key in /var/lib/rundeck/.ssh/known_hosts:
sudo -u rundeck ssh -i /var/lib/rundeck/.ssh/id_ed25519 [email protected] hostname
Answer yes to the host key prompt after checking the fingerprint. The command should print the node's hostname.
Step 7 - Creating a project and registering nodes
A project groups nodes, jobs and their history. Define the nodes in a YAML file that Rundeck reads as the project's node source:
sudo install -d -o rundeck -g rundeck -m 750 /var/lib/rundeck/nodes
sudo nano /var/lib/rundeck/nodes/ops.yaml
web01:
nodename: web01
hostname: 203.0.113.21
username: rundeck-exec
osFamily: unix
description: Web server 1
tags: web,production
web02:
nodename: web02
hostname: 203.0.113.22
username: rundeck-exec
osFamily: unix
description: Web server 2
tags: web,production
sudo chown rundeck:rundeck /var/lib/rundeck/nodes/ops.yaml
Create the project with rd. The properties after -- set the SSH key for the whole project and register the YAML file as its node source:
rd projects create -p ops -- \
--project.label="Operations" \
--project.ssh-keypath=/var/lib/rundeck/.ssh/id_ed25519 \
--resources.source.1.type=file \
--resources.source.1.config.file=/var/lib/rundeck/nodes/ops.yaml \
--resources.source.1.config.format=resourceyaml \
--resources.source.1.config.requireFileExists=true \
--resources.source.1.config.includeServerNode=true
List the nodes Rundeck now knows about:
rd nodes list -p ops
rundeck-server
web01
web02
Run an ad hoc command on every node tagged web to confirm SSH execution works end to end:
rd adhoc -p ops -F 'tags: web' -f -- uptime
10:41:07 up 12 days, 3:02, 0 users, load average: 0.08, 0.05, 0.01
10:41:07 up 30 days, 1:17, 0 users, load average: 0.21, 0.12, 0.09
Step 8 - Loading incident response jobs
A runbook is only useful if it is safe to run at 3 a.m. by someone who did not write it. The two jobs below follow that rule: the first only reads state, and the second performs one well-defined action, one node at a time, and checks the result.
Create the job file:
nano ~/incident-jobs.yaml
- name: Collect diagnostics
group: incident
description: Read-only snapshot of the web nodes during an incident.
loglevel: INFO
nodefilters:
filter: 'tags: web'
dispatch:
threadcount: 4
keepgoing: true
options:
- name: service
description: systemd unit to inspect
value: nginx
required: true
enforced: true
values:
- nginx
sequence:
keepgoing: true
strategy: node-first
commands:
- description: Load, memory and disk usage
exec: uptime && free -m && df -h -x tmpfs -x devtmpfs
- description: Top processes by CPU
exec: ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 15
- description: Service state and recent warnings
script: |-
#!/usr/bin/env bash
systemctl is-active '@option.service@' || true
journalctl -u '@option.service@' --since '-30 min' -p warning --no-pager | tail -n 50
- name: Restart service
group: incident
description: Restart a web service on the selected nodes, one node at a time.
loglevel: INFO
nodefilters:
filter: 'tags: web'
dispatch:
threadcount: 1
keepgoing: false
options:
- name: service
description: systemd unit to restart
value: nginx
required: true
enforced: true
values:
- nginx
sequence:
keepgoing: false
strategy: node-first
commands:
- description: Restart the service
exec: sudo /usr/bin/systemctl restart ${option.service}
- description: Verify it is running again
exec: sleep 5 && systemctl is-active ${option.service}
Some notes on the format:
enforced: truewith avalueslist means the option can only take the values you listed, so nobody can restart an arbitrary unit through this job.- Option values are referenced as
${option.name}inexecsteps and as@option.name@inside inline scripts. threadcount: 1andkeepgoing: falsemake the restart job stop at the first node that fails instead of taking down the whole pool.
Load the file into the project:
rd jobs load -p ops -f ~/incident-jobs.yaml -F yaml
The command reports both jobs as created. Loading the same file again updates them in place, because Rundeck matches jobs by group and name.
Run the diagnostics job and follow its output:
rd run -p ops -j 'incident/Collect diagnostics' -f -- -service nginx
The same jobs appear in the web interface under Jobs, where you can run them with a form, see every past execution and read the output per node and per step.
Step 9 - Giving the on-call team access with an ACL policy
Rundeck grants permissions through .aclpolicy files. Any file in /etc/rundeck ending in .aclpolicy is loaded automatically, without a restart. This policy lets members of the oncall group see the ops project and its nodes, and run or stop only the jobs in the incident group:
sudo nano /etc/rundeck/oncall.aclpolicy
description: On-call engineers can run incident jobs in the ops project.
context:
project: ops
for:
resource:
- equals:
kind: node
allow: [read]
- equals:
kind: event
allow: [read]
job:
- equals:
group: incident
allow: [read, run, kill]
node:
- allow: [read, run]
by:
group: oncall
---
description: On-call engineers can see the ops project.
context:
application: rundeck
for:
project:
- equals:
name: ops
allow: [read]
by:
group: oncall
Validate the policy:
sudo rd acl validate -f /etc/rundeck/oncall.aclpolicy
The validation passed
Create an on-call user in realm.properties. Generate the hash with Password Utility as before and give the account the user and oncall roles:
alice:BCRYPT:$2a$10$another_generated_hash,user,oncall
Restart Rundeck to load the new account, then log in as that user in a private window. alice sees the two incident jobs and can run them, but has no ad hoc command box and no access to other projects or to the system settings.
sudo systemctl restart rundeckd
Step 10 - Triggering a runbook from a webhook
Webhooks let your monitoring system start a job when an alert fires, so the diagnostics are already in Rundeck when the on-call engineer opens the alert. Webhooks run as an existing user, and that user must have logged in at least once.
In the ops project, open Webhooks in the left menu and create a new webhook:
- Name:
alert-diagnostics. - User and Roles:
adminandadmin(or a dedicated service user with its own ACL). - Tick Use Authorization Header so the URL alone is not enough to trigger it.
- Webhook plugin: Run Job, then select incident/Collect diagnostics and set Options to
-service nginx. - Save. Copy the Post URL and the authorization string, which is shown only once.
Test it with curl:
curl -s -X POST 'https://rundeck.your_domain/api/50/webhook/your_webhook_token#alert-diagnostics' \
-H 'Authorization: your_authorization_string' \
-H 'Content-Type: application/json' \
-d '{"alert": "HighLatency", "severity": "warning"}'
{"executionId":"12","jobId":"9bb310cf-fa0a-4a66-89a0-1892d73021e2"}
Use the exact Post URL that Rundeck displays, since the API version number in it depends on your Rundeck version. Fields from the JSON payload are available to the Run Job plugin as ${data.field}, for example -alert ${data.alert} if you add an alert option to the job.
Troubleshooting
rundeckd starts but the web interface never comes up. Read /var/log/rundeck/service.log. OutOfMemoryError means the heap in RDECK_JVM_SETTINGS is too small for the server's workload, and Address already in use means something else is on port 4440 (sudo ss -tlnp | grep 4440).
Links and redirects point to http://127.0.0.1:4440. grails.serverURL in /etc/rundeck/rundeck-config.properties is not set to your public URL, or the forwarded headers option is missing from /etc/default/rundeckd. Fix the value and restart rundeckd.
Jobs fail with Authentication failure on a node. Repeat the manual test from Step 6 with sudo -u rundeck ssh -i /var/lib/rundeck/.ssh/id_ed25519 rundeck-exec@node_ip hostname. If it works by hand, check that project.ssh-keypath points to the same key (rd projects configure get -p ops) and that the node's username in ops.yaml is rundeck-exec.
The restart step fails with sudo: a password is required. The command in the job must match the sudoers rule exactly, including the full path /usr/bin/systemctl.
A user sees an empty project list. Their roles in realm.properties do not match any by: group: in an ACL policy, or the application context section is missing. Validate every policy with sudo rd acl validate.
Conclusion
You installed Rundeck on Ubuntu 24.04 behind Nginx, locked down its accounts, connected it to your servers over SSH with a restricted user, and turned two incident procedures into audited, access-controlled jobs that can also be started by your alerting system. Next, store your job definitions in Git and load them with rd jobs load from CI, replace realm.properties with LDAP or SSO once more people need access, and move from the embedded database to MySQL or PostgreSQL before relying on Rundeck in production.
