This guide provides a Ansible playbook for deploying Uptime Kuma with Docker Compose on Debian, Ubuntu, and RHEL-compatible systems. Includes security hardening, backup automation, and multi-server support.
Ansible Version: 2.9+ required (2.12+ recommended)
Target Systems: Debian 10+, Ubuntu 20.04+, RHEL 9+, AlmaLinux, Rocky Linux
Latest Uptime Kuma: v2.1.1 (February 13, 2026)
Create inventory.ini:
[uptime-kuma]
monitor1.example.com ansible_user=deploy
monitor2.example.com ansible_user=deploy
[uptime-kuma:vars]
ansible_python_interpreter=/usr/bin/python3
uptime-kuma.yml---
- name: Deploy Uptime Kuma Monitoring
hosts: uptime-kuma
become: true
gather_facts: true
vars:
# Application settings
app_name: uptime-kuma
app_root: /opt/uptime-kuma
app_port: 3001
app_timezone: Europe/Berlin
# Docker image settings
docker_image: louislam/uptime-kuma
docker_tag: "2.1.1" # Pin to specific version
docker_image_full: "{{ docker_image }}:{{ docker_tag }}"
# Network settings
bind_address: "127.0.0.1" # Bind to localhost for reverse proxy
expose_port: true
# Volume settings
data_dir: /opt/uptime-kuma/data
# Security settings
enable_security_hardening: true
docker_user: "1000:1000"
# Backup settings
enable_backup: true
backup_dir: /backup/uptime-kuma
backup_retention_days: 30
# Reverse proxy settings
enable_reverse_proxy: false
reverse_proxy_type: nginx # nginx, traefik, caddy
domain_name: "uptime.example.com"
enable_ssl: true
tasks:
# ==========================================
# System Preparation
# ==========================================
- name: Update package cache (Debian/Ubuntu)
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Update package cache (RHEL)
dnf:
update_cache: true
when: ansible_os_family == "RedHat"
- name: Install required packages
package:
name:
- curl
- git
- gnupg
- ca-certificates
- apt-transport-https
state: present
when: ansible_os_family == "Debian"
- name: Install required packages (RHEL)
dnf:
name:
- curl
- git
- gnupg2
- ca-certificates
state: present
when: ansible_os_family == "RedHat"
# ==========================================
# Docker Installation
# ==========================================
- name: Install Docker (Debian/Ubuntu)
block:
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
filename: docker
- name: Install Docker packages
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Docker (RHEL)
block:
- name: Add Docker repository
yum_repository:
name: docker-ce
description: Docker CE Repository
baseurl: https://download.docker.com/linux/centos/$releasever/$basearch/stable
gpgkey: https://download.docker.com/linux/centos/gpg
gpgcheck: true
- name: Install Docker packages
dnf:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
when: ansible_os_family == "RedHat"
- name: Enable and start Docker service
service:
name: docker
state: started
enabled: true
- name: Add user to docker group
user:
name: "{{ ansible_user }}"
groups: docker
append: true
# ==========================================
# Application Directory Setup
# ==========================================
- name: Create application directory
file:
path: "{{ app_root }}"
state: directory
mode: "0755"
owner: root
group: root
- name: Create data directory
file:
path: "{{ data_dir }}"
state: directory
mode: "0750"
owner: "1000"
group: "1000"
- name: Create backup directory
file:
path: "{{ backup_dir }}"
state: directory
mode: "0750"
owner: root
group: root
when: enable_backup | bool
# ==========================================
# Docker Compose Configuration
# ==========================================
- name: Deploy Docker Compose file
copy:
dest: "{{ app_root }}/docker-compose.yml"
mode: "0644"
content: |
version: '3.8'
services:
uptime-kuma:
image: {{ docker_image_full }}
container_name: {{ app_name }}
restart: unless-stopped
ports:
- "{{ bind_address }}:{{ app_port }}:3001"
volumes:
- {{ data_dir }}:/app/data
- /etc/localtime:/etc/localtime:ro
environment:
- TZ={{ app_timezone }}
- NODE_ENV=production
networks:
- {{ app_name }}-network
{% if enable_security_hardening %}
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
user: "{{ docker_user }}"
{% endif %}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
labels:
- "com.uptime-kuma.app=monitoring"
- "com.uptime-kuma.environment=production"
networks:
{{ app_name }}-network:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
# ==========================================
# Security Hardening
# ==========================================
- name: Configure sysctl parameters
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: 'net.ipv4.ip_forward', value: '1' }
- { name: 'net.ipv4.conf.all.forwarding', value: '1' }
when: enable_security_hardening | bool
- name: Configure UFW firewall rules
ufw:
rule: allow
port: "{{ app_port }}"
proto: tcp
from_ip: "{{ item }}"
loop:
- "127.0.0.1"
when:
- enable_security_hardening | bool
- ansible_os_family == "Debian"
# ==========================================
# Deploy Application
# ==========================================
- name: Start Uptime Kuma
command: docker compose up -d
args:
chdir: "{{ app_root }}"
register: docker_compose_result
changed_when: "'Creating' in docker_compose_result.stdout or 'Starting' in docker_compose_result.stdout"
- name: Wait for application to be ready
wait_for:
port: "{{ app_port }}"
host: "{{ bind_address }}"
delay: 5
timeout: 60
retries: 5
delay: 10
# ==========================================
# Backup Script
# ==========================================
- name: Deploy backup script
copy:
dest: /usr/local/bin/backup-{{ app_name }}.sh
mode: "0755"
content: |
#!/bin/bash
set -e
BACKUP_DIR="{{ backup_dir }}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${{BACKUP_DIR}}/{{ app_name }}-${{DATE}}.tar.gz"
mkdir -p "${{BACKUP_DIR}}"
# Backup data directory
tar czf "${{BACKUP_FILE}}" -C {{ data_dir }} .
# Verify backup
tar tzf "${{BACKUP_FILE}}" > /dev/null && echo "Backup verified: ${{BACKUP_FILE}}"
# Cleanup old backups
find "${{BACKUP_DIR}}" -name "{{ app_name }}-*.tar.gz" -mtime +{{ backup_retention_days }} -delete
echo "Backup completed successfully"
when: enable_backup | bool
- name: Install backup cron job
cron:
name: "Backup {{ app_name }}"
minute: "0"
hour: "2"
job: "/usr/local/bin/backup-{{ app_name }}.sh >> /var/log/{{ app_name }}-backup.log 2>&1"
user: root
when: enable_backup | bool
# ==========================================
# Nginx Reverse Proxy (Optional)
# ==========================================
- name: Install Nginx
package:
name: nginx
state: present
when:
- enable_reverse_proxy | bool
- reverse_proxy_type == 'nginx'
- name: Deploy Nginx configuration
copy:
dest: /etc/nginx/sites-available/{{ domain_name }}
mode: "0644"
content: |
upstream {{ app_name }} {
server {{ bind_address }}:{{ app_port }};
}
server {
listen 80;
server_name {{ domain_name }};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name {{ domain_name }};
ssl_certificate /etc/letsencrypt/live/{{ domain_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ domain_name }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://{{ app_name }};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_read_timeout 86400;
}
}
when:
- enable_reverse_proxy | bool
- reverse_proxy_type == 'nginx'
notify: Reload Nginx
- name: Enable Nginx site
file:
src: /etc/nginx/sites-available/{{ domain_name }}
dest: /etc/nginx/sites-enabled/{{ domain_name }}
state: link
when:
- enable_reverse_proxy | bool
- reverse_proxy_type == 'nginx'
notify: Reload Nginx
# ==========================================
# Final Status
# ==========================================
- name: Display deployment summary
debug:
msg: |
==========================================
Uptime Kuma Deployment Complete!
==========================================
Application URL: http://{{ inventory_hostname }}:{{ app_port }}
{% if enable_reverse_proxy %}
Public URL: https://{{ domain_name }}
{% endif %}
Data Directory: {{ data_dir }}
Backup Directory: {{ backup_dir }}
Docker Image: {{ docker_image_full }}
==========================================
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
group_vars/uptime-kuma.yml---
# Global settings for all Uptime Kuma hosts
app_timezone: Europe/Berlin
docker_tag: "2.1.1"
enable_security_hardening: true
enable_backup: true
backup_retention_days: 30
# SSL/Let's Encrypt settings
enable_ssl: true
ssl_email: "admin@example.com"
# Monitoring settings
enable_monitoring: true
prometheus_export: true
# Deploy to all hosts
ansible-playbook -i inventory.ini uptime-kuma.yml
# Deploy with verbose output
ansible-playbook -i inventory.ini uptime-kuma.yml -vv
# Deploy to specific host
ansible-playbook -i inventory.ini uptime-kuma.yml --limit monitor1.example.com
# Dry run (check mode)
ansible-playbook -i inventory.ini uptime-kuma.yml --check
# Only install Docker
ansible-playbook -i inventory.ini uptime-kuma.yml --tags docker
# Only deploy application
ansible-playbook -i inventory.ini uptime-kuma.yml --tags deploy
# Only configure backup
ansible-playbook -i inventory.ini uptime-kuma.yml --tags backup
# Only setup reverse proxy
ansible-playbook -i inventory.ini uptime-kuma.yml --tags nginx
# Update the docker_tag variable in playbook or group_vars
# Then run:
ansible-playbook -i inventory.ini uptime-kuma.yml --tags deploy
# Run backup on all hosts
ansible uptime-kuma -m shell -a "/usr/local/bin/backup-uptime-kuma.sh" --become
# Check container status
ansible uptime-kuma -m shell -a "docker ps | grep uptime-kuma"
# Check application health
ansible uptime-kuma -m uri -a "url=http://localhost:3001 status_code=200"
# Fetch logs from all hosts
ansible uptime-kuma -m shell -a "docker logs uptime-kuma --tail 100"
Create uninstall-uptime-kuma.yml:
---
- name: Uninstall Uptime Kuma
hosts: uptime-kuma
become: true
vars:
app_name: uptime-kuma
app_root: /opt/uptime-kuma
tasks:
- name: Stop and remove container
command: docker compose down -v
args:
chdir: "{{ app_root }}"
ignore_errors: true
- name: Remove application directory
file:
path: "{{ app_root }}"
state: absent
- name: Remove backup script
file:
path: /usr/local/bin/backup-{{ app_name }}.sh
state: absent
- name: Remove backup cron job
cron:
name: "Backup {{ app_name }}"
state: absent
- name: Remove Nginx configuration
file:
path: "{{ item }}"
state: absent
loop:
- /etc/nginx/sites-available/{{ app_name }}
- /etc/nginx/sites-enabled/{{ app_name }}
ignore_errors: true
notify: Reload Nginx
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
Docker Installation Fails:
# Check existing Docker installation
ansible all -m shell -a "docker --version"
# Manual Docker installation verification
ansible all -m shell -a "systemctl status docker"
Port Already in Use:
# Change bind_address in vars:
bind_address: "127.0.0.1"
# Or change app_port:
app_port: 3002
Permission Denied on Data Directory:
# Fix permissions manually
ansible uptime-kuma -m file -a "path=/opt/uptime-kuma/data mode=0750 owner=1000 group=1000" --become
Application Not Starting:
# Check container logs
ansible uptime-kuma -m shell -a "docker logs uptime-kuma"
# Check if port is listening
ansible uptime-kuma -m shell -a "ss -tlnp | grep 3001"
# Use ansible-vault for sensitive data
[uptime-kuma:vars]
ansible_become_pass={{ vault_become_pass }}
Encrypt with:
ansible-vault encrypt group_vars/uptime-kuma.yml
# In ansible.cfg
[defaults]
host_key_checking = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=yes
:latest)Any questions?
Feel free to contact us. Find all contact information on our contact page.