This guide provides a Ansible playbook for automated deployment of Vigil on Debian 10+, Ubuntu LTS, and RHEL 9+ compatible hosts. The playbook includes Docker Compose deployment, security hardening, TLS certificate automation, and Nginx reverse proxy configuration.
Latest Stable Version: v1.28.6 (November 28, 2025)
Ansible Version: 2.14+
Supported OS: Debian 10-12, Ubuntu 20.04-24.04, RHEL 9+, AlmaLinux 9+, Rocky Linux 9+
# Install Ansible
pip3 install ansible
# Or via package manager
sudo apt-get install -y ansible # Debian/Ubuntu
sudo dnf install -y ansible # RHEL/Fedora
# Verify installation
ansible --version
# Generate SSH key (if not exists)
ssh-keygen -t ed25519 -C "ansible-vigil"
# Copy to target host
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@vigil-server
ansible-vigil/
βββ ansible.cfg
βββ inventory/
β βββ hosts.yml
β βββ group_vars/
β βββ vigil_servers.yml
βββ playbooks/
β βββ deploy.yml
β βββ security.yml
β βββ maintenance.yml
βββ roles/
β βββ docker/
β β βββ tasks/
β β βββ main.yml
β βββ vigil/
β β βββ tasks/
β β β βββ main.yml
β β βββ templates/
β β β βββ config.cfg.j2
β β β βββ docker-compose.yml.j2
β β βββ handlers/
β β β βββ main.yml
β β βββ vars/
β β βββ main.yml
β βββ nginx/
β β βββ tasks/
β β β βββ main.yml
β β βββ templates/
β β βββ nginx.conf.j2
β βββ ssl/
β β βββ tasks/
β β βββ main.yml
β βββ hardening/
β βββ tasks/
β βββ main.yml
βββ scripts/
βββ backup.sh
all:
children:
vigil_servers:
hosts:
vigil-prod-01:
ansible_host: 192.168.1.100
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
vigil_domain: status.example.com
vigil_environment: production
vigil-prod-02:
ansible_host: 192.168.1.101
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
vigil_domain: status2.example.com
vigil_environment: production
staging:
hosts:
vigil-staging:
ansible_host: 192.168.1.200
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
vigil_domain: status-staging.example.com
vigil_environment: staging
---
# Vigil Application Variables
vigil_version: "v1.28.6"
vigil_app_root: /opt/vigil
vigil_config_dir: "{{ vigil_app_root }}/config"
vigil_data_dir: "{{ vigil_app_root }}/data"
vigil_logs_dir: "{{ vigil_app_root }}/logs"
# Network Configuration
vigil_port: 8080
vigil_bind_address: "127.0.0.1"
# Domain and TLS
vigil_domain: "status.example.com"
vigil_admin_email: "admin@example.com"
vigil_tls_enabled: true
vigil_tls_provider: "letsencrypt" # letsencrypt, selfsigned, custom
# Security Tokens (use Ansible Vault for production)
vigil_manager_token: "{{ lookup('password', '/dev/null length=32 chars=hexdigits') }}"
vigil_reporter_token: "{{ lookup('password', '/dev/null length=32 chars=hexdigits') }}"
# Docker Configuration
docker_install_compose_plugin: true
docker_users:
- "{{ ansible_user }}"
# Nginx Configuration
nginx_install: true
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_rate_limit: "10r/s"
nginx_rate_limit_burst: 20
# Security Hardening
vigil_security_hardening: true
vigil_enable_firewall: true
vigil_firewall_allowed_tcp_ports:
- 22
- 80
- 443
vigil_enable_fail2ban: true
vigil_enable_automatic_security_updates: true
# Backup Configuration
vigil_backup_enabled: true
vigil_backup_dir: /backup/vigil
vigil_backup_retention_days: 30
vigil_backup_schedule: "0 2 * * *" # Daily at 2 AM
# Monitoring
vigil_healthcheck_enabled: true
vigil_healthcheck_interval: 30s
vigil_external_monitoring_url: "" # Optional: external Uptime Kuma, etc.
# Create vault for sensitive data
ansible-vault create inventory/group_vars/vigil_vault.yml
# Add secrets
vigil_manager_token: "your-secure-manager-token"
vigil_reporter_token: "your-secure-reporter-token"
smtp_password: "your-smtp-password"
database_password: "your-database-password"
| Variable | Description | Default | Required |
|---|---|---|---|
vigil_version |
Vigil Docker image tag | v1.28.6 |
Yes |
vigil_app_root |
Installation directory | /opt/vigil |
Yes |
vigil_domain |
Domain for status page | - | Yes |
vigil_manager_token |
Manager API token | (auto-generated) | Yes |
vigil_reporter_token |
Reporter API token | (auto-generated) | Yes |
vigil_tls_enabled |
Enable TLS/HTTPS | true |
No |
vigil_tls_provider |
Certificate provider | letsencrypt |
No |
vigil_enable_firewall |
Enable UFW/firewalld | true |
No |
vigil_backup_enabled |
Enable automated backups | true |
No |
---
- name: Deploy Vigil Status Page
hosts: vigil_servers
become: true
gather_facts: true
vars_files:
- ../inventory/group_vars/vigil_servers.yml
- ../inventory/group_vars/vigil_vault.yml
pre_tasks:
- name: Validate Ansible version
assert:
that: ansible_version.full is version('2.14', '>=')
fail_msg: "Ansible 2.14 or higher is required"
- name: Gather OS-specific variables
include_vars: "{{ item }}"
with_first_found:
- files:
- "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- default.yml
paths:
- ../vars/
skip: true
roles:
- role: docker
tags: ['docker', 'base']
- role: vigil
tags: ['vigil', 'app']
- role: nginx
tags: ['nginx', 'proxy']
when: nginx_install | bool
- role: ssl
tags: ['ssl', 'tls']
when: vigil_tls_enabled | bool
- role: hardening
tags: ['hardening', 'security']
when: vigil_security_hardening | bool
post_tasks:
- name: Verify Vigil is running
uri:
url: "http://{{ vigil_bind_address }}:{{ vigil_port }}/status/report"
method: GET
status_code: 200
register: vigil_health
retries: 5
delay: 10
until: vigil_health.status == 200
- name: Display deployment summary
debug:
msg: |
Vigil deployment completed!
Domain: https://{{ vigil_domain }}
Health: {{ 'β
Healthy' if vigil_health.status == 200 else 'β Unhealthy' }}
Version: {{ vigil_version }}
Next steps:
1. Configure DNS: {{ vigil_domain }} -> {{ ansible_host }}
2. Review configuration: {{ vigil_config_dir }}/config.cfg
3. Set up monitoring probes
4. Configure notification channels
---
- name: Install Docker prerequisites
package:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
when: ansible_os_family == "Debian"
- name: Add Docker GPG key (Debian/Ubuntu)
apt_key:
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
state: present
when: ansible_os_family == "Debian"
- name: Add Docker repository (Debian/Ubuntu)
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
state: present
filename: docker
when: ansible_os_family == "Debian"
- name: Install Docker packages
package:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
notify: restart docker
- name: Enable and start Docker
service:
name: docker
state: started
enabled: true
- name: Create Docker configuration directory
file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon
copy:
dest: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"userns-remap": "default",
"live-restore": true
}
mode: '0644'
notify: restart docker
- name: Add user to Docker group
user:
name: "{{ ansible_user }}"
groups: docker
append: true
---
- name: Create Vigil directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
loop:
- "{{ vigil_app_root }}"
- "{{ vigil_config_dir }}"
- "{{ vigil_data_dir }}"
- "{{ vigil_logs_dir }}"
- name: Generate Vigil configuration
template:
src: config.cfg.j2
dest: "{{ vigil_config_dir }}/config.cfg"
mode: '0640'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
backup: true
notify: restart vigil
- name: Create .env file for Docker Compose
copy:
content: |
VIGIL_MANAGER_TOKEN={{ vigil_manager_token }}
VIGIL_REPORTER_TOKEN={{ vigil_reporter_token }}
TZ=UTC
dest: "{{ vigil_app_root }}/.env"
mode: '0600'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
- name: Create Docker Compose file
template:
src: docker-compose.yml.j2
dest: "{{ vigil_app_root }}/docker-compose.yml"
mode: '0644'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
- name: Pull Vigil Docker image
command: docker pull valeriansaliou/vigil:{{ vigil_version }}
args:
creates: "{{ vigil_app_root }}/.image_pulled"
- name: Start Vigil containers
command: docker compose up -d
args:
chdir: "{{ vigil_app_root }}"
register: docker_compose_up
changed_when: "'Creating' in docker_compose_up.stdout or 'Starting' in docker_compose_up.stdout"
notify: wait for vigil health
- name: Create backup script
template:
src: backup.sh.j2
dest: "{{ vigil_app_root }}/backup.sh"
mode: '0755'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: vigil_backup_enabled | bool
- name: Schedule backup cron job
cron:
name: "Vigil backup"
minute: "0"
hour: "2"
job: "{{ vigil_app_root }}/backup.sh"
user: "{{ ansible_user }}"
when: vigil_backup_enabled | bool
# Vigil Configuration - Managed by Ansible
# Domain: {{ vigil_domain }}
# Environment: {{ vigil_environment | default('production') }}
[server]
log_level = "info"
inet = "[::]:{{ vigil_port }}"
workers = 4
mcp_server = false
manager_token = "${VIGIL_MANAGER_TOKEN}"
reporter_token = "${VIGIL_REPORTER_TOKEN}"
[assets]
path = "./res/assets/"
[branding]
page_title = "{{ vigil_domain | replace('status.', '') | replace('.com', '') | title }} Status"
page_url = "https://{{ vigil_domain }}"
company_name = "{{ vigil_company_name | default('Your Company') }}"
icon_color = "#3B82F6"
icon_url = "https://{{ vigil_domain }}/icon.png"
logo_color = "#1E40AF"
logo_url = "https://{{ vigil_domain }}/logo.svg"
website_url = "https://{{ vigil_domain | replace('status.', '') }}"
support_url = "mailto:{{ vigil_support_email | default('support@' + vigil_domain.split('.')[-2] + '.' + vigil_domain.split('.')[-1]) }}"
custom_html = ""
[metrics]
poll_interval = 120
poll_retry = 2
poll_retry_wait = 500
poll_http_status_healthy_above = 200
poll_http_status_healthy_below = 400
poll_delay_dead = 10
poll_delay_sick = 5
poll_parallelism = 4
push_delay_dead = 20
push_system_cpu_sick_above = 0.90
push_system_ram_sick_above = 0.90
script_interval = 300
script_parallelism = 2
local_delay_dead = 40
[notify]
startup_notification = true
reminder_interval = 300
reminder_backoff_function = "linear"
reminder_backoff_limit = 3
{% if vigil_smtp_enabled | default(false) %}
[notify.email]
from = "{{ vigil_smtp_from }}"
to = "{{ vigil_smtp_to }}"
smtp_host = "{{ vigil_smtp_host }}"
smtp_port = {{ vigil_smtp_port | default(587) }}
smtp_username = "{{ vigil_smtp_username }}"
smtp_password = "${SMTP_PASSWORD}"
smtp_encrypt = {{ vigil_smtp_encrypt | default(true) | lower }}
{% endif %}
{% if vigil_slack_webhook | default(false) %}
[notify.slack]
hook_url = "{{ vigil_slack_webhook }}"
mention_channel = true
{% endif %}
[probe]
# Configure your service probes below
# Example:
# [[probe.service]]
# id = "api"
# label = "API Service"
#
# [[probe.service.node]]
# id = "api-health"
# label = "API Health Check"
# mode = "poll"
# replicas = ["https://api.{{ vigil_domain | replace('status.', '') }}/health"]
# http_method = "GET"
services:
vigil:
image: valeriansaliou/vigil:{{ vigil_version }}
container_name: vigil
restart: unless-stopped
expose:
- "{{ vigil_port }}"
volumes:
- {{ vigil_config_dir }}/config.cfg:/etc/vigil.cfg:ro
- {{ vigil_data_dir }}:/var/lib/vigil:rw
env_file:
- .env
environment:
- TZ=UTC
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- NET_RAW
networks:
- vigil-internal
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:{{ vigil_port }}/status/report"]
interval: {{ vigil_healthcheck_interval }}
timeout: 10s
retries: 3
start_period: 10s
{% if nginx_install | default(true) %}
nginx:
image: nginx:alpine
container_name: vigil-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- {{ vigil_app_root }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- {{ vigil_app_root }}/nginx/ssl:/etc/nginx/ssl:ro
- {{ vigil_app_root }}/nginx/html:/usr/share/nginx/html:ro
- {{ vigil_logs_dir }}/nginx:/var/log/nginx:rw
depends_on:
vigil:
condition: service_healthy
read_only: true
tmpfs:
- /var/cache/nginx:rw,noexec,nosuid,size=64m,mode=1777
- /var/run:rw,noexec,nosuid,size=32m,mode=1777
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- CHOWN
- SETGID
- SETUID
networks:
- vigil-internal
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
compress: "true"
{% endif %}
networks:
vigil-internal:
driver: bridge
---
- name: Create Nginx directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
loop:
- "{{ vigil_app_root }}/nginx"
- "{{ vigil_app_root }}/nginx/ssl"
- "{{ vigil_app_root }}/nginx/html"
- name: Generate Nginx configuration
template:
src: nginx.conf.j2
dest: "{{ vigil_app_root }}/nginx/nginx.conf"
mode: '0644'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
notify: reload nginx
- name: Create default index page
copy:
content: |
<!DOCTYPE html>
<html>
<head>
<title>Vigil Status Page</title>
<meta http-equiv="refresh" content="0; url=https://{{ vigil_domain }}/">
</head>
<body>
<p>Redirecting to <a href="https://{{ vigil_domain }}/">status page</a>...</p>
</body>
</html>
dest: "{{ vigil_app_root }}/nginx/html/index.html"
mode: '0644'
---
- name: Install Certbot
package:
name: certbot
state: present
- name: Check if certificate exists
stat:
path: /etc/letsencrypt/live/{{ vigil_domain }}/fullchain.pem
register: cert_exists
- name: Obtain Let's Encrypt certificate
command: >
certbot certonly --standalone
-d {{ vigil_domain }}
--email {{ vigil_admin_email }}
--agree-tos
--non-interactive
--force-renewal
when: not cert_exists.stat.exists
register: certbot_result
- name: Copy certificates to Vigil directory
copy:
src: "/etc/letsencrypt/live/{{ vigil_domain }}/{{ item.src }}"
dest: "{{ vigil_app_root }}/nginx/ssl/{{ item.dest }}"
mode: "{{ item.mode }}"
remote_src: true
loop:
- { src: 'fullchain.pem', dest: 'fullchain.pem', mode: '0644' }
- { src: 'privkey.pem', dest: 'privkey.pem', mode: '0600' }
notify: reload nginx
- name: Schedule certificate renewal
cron:
name: "Let's Encrypt renewal"
minute: "0"
hour: "3"
day: "{{ (range(1, 29) | random) }}"
month: "*"
job: "certbot renew --quiet && docker compose -f {{ vigil_app_root }}/docker-compose.yml restart nginx"
user: root
---
- name: Enable UFW firewall
ufw:
direction: incoming
policy: deny
when: ansible_os_family == "Debian" and vigil_enable_firewall | bool
- name: Allow SSH through firewall
ufw:
rule: allow
port: "22"
proto: tcp
when: ansible_os_family == "Debian" and vigil_enable_firewall | bool
- name: Allow HTTP/HTTPS through firewall
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
when: ansible_os_family == "Debian" and vigil_enable_firewall | bool
- name: Enable UFW
ufw:
state: enabled
when: ansible_os_family == "Debian" and vigil_enable_firewall | bool
- name: Install fail2ban
package:
name: fail2ban
state: present
when: vigil_enable_fail2ban | bool
- name: Enable and start fail2ban
service:
name: fail2ban
state: started
enabled: true
when: vigil_enable_fail2ban | bool
- name: Configure sysctl security parameters
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: 'net.ipv4.tcp_syncookies', value: '1' }
- { name: 'net.ipv4.conf.all.accept_redirects', value: '0' }
- { name: 'net.ipv4.conf.all.send_redirects', value: '0' }
- { name: 'net.ipv4.conf.all.accept_source_route', value: '0' }
when: vigil_security_hardening | bool
- name: Enable automatic security updates
package:
name:
- unattended-upgrades
- apt-listchanges
state: present
when: ansible_os_family == "Debian" and vigil_enable_automatic_security_updates | bool
cd ansible-vigil
# Syntax check
ansible-playbook playbooks/deploy.yml --syntax-check
# Dry run (check mode)
ansible-playbook playbooks/deploy.yml --check
# List tasks
ansible-playbook playbooks/deploy.yml --list-tasks
# Full deployment
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml
# With vault password
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --ask-vault-pass
# Limit to specific host
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --limit vigil-prod-01
# With verbose output
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml -vvv
# Check service status
ansible vigil_servers -m shell -a "docker compose -C /opt/vigil ps"
# Test health endpoint
ansible vigil_servers -m uri -a "url=http://localhost:8080/status/report method=GET"
# View logs
ansible vigil_servers -m shell -a "docker compose -C /opt/vigil logs --tail=50"
ansible-playbook -i inventory/hosts.yml playbooks/security.yml
The playbook includes automatic certificate renewal via cron. Test renewal:
ansible vigil_servers -m shell -a "certbot renew --dry-run"
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags ssl
---
- name: Vigil Maintenance Tasks
hosts: vigil_servers
become: true
tasks:
- name: Check Vigil health
uri:
url: "http://localhost:{{ vigil_port }}/status/report"
method: GET
register: health_check
- name: Display health status
debug:
msg: "Vigil health: {{ 'β
Healthy' if health_check.status == 200 else 'β Unhealthy' }}"
- name: Check disk space
shell: df -h {{ vigil_app_root }}
register: disk_space
- name: Display disk usage
debug:
var: disk_space.stdout_lines
- name: Check Docker container status
shell: docker compose -C {{ vigil_app_root }} ps
register: container_status
- name: Display container status
debug:
var: container_status.stdout_lines
ansible-playbook -i inventory/hosts.yml playbooks/maintenance.yml
ansible vigil_servers -m shell -a "ls -lh /backup/vigil/"
# Update version in group_vars
# vigil_version: "v1.29.0"
# Run deployment with upgrade
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags vigil
Docker not starting:
ansible vigil_servers -m shell -a "journalctl -u docker -n 50 --no-pager"
Vigil container unhealthy:
ansible vigil_servers -m shell -a "docker compose -C /opt/vigil logs vigil"
Certificate renewal failed:
ansible vigil_servers -m shell -a "certbot renew --force-renewal --dry-run"
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml -vvv --step
Any questions?
Feel free to contact us. Find all contact information on our contact page.