Initial Ansible automation for hobbs.farm infrastructure

Complete IaC covering:
- 14 roles: common, docker, traefik, vaultwarden, monitoring, media, matrix,
  home_assistant, frigate, adguard, gitea, semaphore, openclaw, security, backup
- Inventory for all hosts (Peggy, Bart, Yoda, NAS, Firewalla)
- Stack playbooks: core-infrastructure, monitoring, media, communication,
  automation, smarthome, security-hardening
- Maintenance playbooks: update-containers, backup, health-check
- Production Docker Compose templates with Traefik labels
- Security hardening (sysctl, auditd, UFW, fail2ban)
- Automated backups with retention policy
This commit is contained in:
2026-02-01 23:14:46 -06:00
parent 02beae23a0
commit b9be45dc71
74 changed files with 2110 additions and 2 deletions
+12
View File
@@ -0,0 +1,12 @@
*.retry
.vault_pass
vault_password_file
*.pyc
__pycache__/
.fact_cache/
*.log
*.bak
*.swp
*~
.env
secrets/
+79 -2
View File
@@ -1,3 +1,80 @@
# ansible-homelab
# Ansible Homelab - hobbs.farm
Ansible automation for hobbs.farm infrastructure
Infrastructure-as-Code for the hobbs.farm homelab environment.
## Architecture
| Host | IP | Role | OS |
|------|-----|------|-----|
| Peggy | 192.168.86.45 | Primary Docker host (48+ containers) | Ubuntu 24.04 |
| Bart | 192.168.86.181 | Frigate NVR | Ubuntu |
| Yoda | 192.168.86.31 | BlueIris NVR | Windows |
| NAS | 192.168.86.30 | 16TB storage (SMB/NFS) | Synology |
| Firewalla | 192.168.86.1 | Gateway / Firewall | Firewalla |
## Quick Start
```bash
# Install Galaxy requirements
ansible-galaxy install -r requirements.yml
# Test connectivity
ansible all -m ping
# Full deployment
ansible-playbook playbooks/site.yml
# Deploy specific stack
ansible-playbook playbooks/core-infrastructure.yml
ansible-playbook playbooks/monitoring.yml
ansible-playbook playbooks/media.yml
# Maintenance
ansible-playbook playbooks/maintenance/health-check.yml
ansible-playbook playbooks/maintenance/update-containers.yml
ansible-playbook playbooks/maintenance/backup.yml
```
## Tags
All playbooks support selective execution via tags:
```bash
ansible-playbook playbooks/site.yml --tags traefik
ansible-playbook playbooks/site.yml --tags monitoring
ansible-playbook playbooks/site.yml --tags media
```
Available tags: `common`, `docker`, `traefik`, `vaultwarden`, `monitoring`, `grafana`, `prometheus`, `alertmanager`, `media`, `jellyfin`, `sonarr`, `radarr`, `matrix`, `homeassistant`, `esphome`, `frigate`, `adguard`, `gitea`, `semaphore`, `openclaw`, `security`, `backup`
## Vault
Sensitive variables are referenced via `vault_` prefix and should be stored in an encrypted vault file:
```bash
ansible-vault create inventory/group_vars/vault.yml
ansible-vault edit inventory/group_vars/vault.yml
```
Required vault variables:
- `vault_acme_email` - Let's Encrypt email
- `vault_cloudflare_api_token` - Cloudflare DNS API token
- `vault_grafana_admin_password` - Grafana admin password
- `vault_vaultwarden_admin_token` - Vaultwarden admin token
- `vault_matrix_shared_secret` - Matrix Synapse registration secret
- `vault_smtp_host` - SMTP relay host
- `vault_slack_webhook` - Slack/Discord webhook for alerts
- `vault_gitea_db_password` - Gitea PostgreSQL password
- `vault_semaphore_db_password` - Semaphore DB password
- `vault_nas_smb_password` - NAS SMB credentials
## Directory Structure
```
├── ansible.cfg # Ansible configuration
├── requirements.yml # Galaxy dependencies
├── inventory/ # Hosts and variables
├── playbooks/ # Orchestration playbooks
│ └── maintenance/ # Maintenance tasks
└── roles/ # Reusable roles
```
+25
View File
@@ -0,0 +1,25 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
remote_user = glyptic
private_key_file = ~/.ssh/id_docker_server
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callbacks_enabled = timer, profile_tasks
forks = 10
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = .fact_cache
fact_caching_timeout = 3600
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
+45
View File
@@ -0,0 +1,45 @@
---
# Global variables for hobbs.farm
domain: hobbs.farm
timezone: America/Chicago
dns_server: 192.168.86.45
gateway: 192.168.86.1
nas_ip: 192.168.86.30
nas_share: "//192.168.86.30/data"
# SSH
ansible_user: glyptic
ansible_ssh_private_key_file: ~/.ssh/id_docker_server
ansible_python_interpreter: /usr/bin/python3
# Docker network
docker_network_name: homelab_network
# Traefik
traefik_acme_email: "{{ vault_acme_email }}"
cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
# NAS mounts
nas_media_path: /mnt/nas/media
nas_backup_path: /mnt/nas/backups
nas_docker_path: /mnt/nas/docker
# Common packages
common_packages:
- htop
- iotop
- curl
- wget
- git
- jq
- unzip
- net-tools
- ncdu
- tmux
- rsync
- lsof
# NTP
ntp_servers:
- 0.us.pool.ntp.org
- 1.us.pool.ntp.org
+9
View File
@@ -0,0 +1,9 @@
---
docker_compose_base: /data/docker-compose
docker_data_dir: /data
docker_log_driver: json-file
docker_log_max_size: 10m
docker_log_max_file: "3"
docker_install_compose_plugin: true
docker_users:
- glyptic
+7
View File
@@ -0,0 +1,7 @@
---
prometheus_retention: 30d
prometheus_scrape_interval: 15s
grafana_admin_password: "{{ vault_grafana_admin_password }}"
alertmanager_smtp_host: "{{ vault_smtp_host }}"
alertmanager_smtp_from: "alerts@hobbs.farm"
alertmanager_slack_webhook: "{{ vault_slack_webhook | default('') }}"
+14
View File
@@ -0,0 +1,14 @@
---
ansible_host: 192.168.86.181
hostname: bart
hardware_description: "Intel i5-4570T, 16GB RAM"
os: "Ubuntu"
# Frigate-specific
frigate_config_dir: /opt/frigate/config
frigate_media_dir: /media/frigate
coral_device: /dev/bus/usb
backup_sources:
- /opt/frigate/config
backup_destination: /mnt/nas/backups/bart
+6
View File
@@ -0,0 +1,6 @@
---
ansible_host: 192.168.86.1
hostname: firewalla
device_type: firewall
ansible_connection: local
note: "Firewalla managed via its own interface. Inventory entry for documentation and monitoring targets."
+37
View File
@@ -0,0 +1,37 @@
---
ansible_host: 192.168.86.45
hostname: peggy
hardware_description: "Intel i7-6700T, 8GB RAM"
os: "Ubuntu 24.04"
docker_compose_stacks:
- core-infrastructure
- infrastructure
- monitoring
- media
- automation
- communication
- smarthome
# NAS mounts for Peggy
nfs_mounts:
- src: "192.168.86.30:/volume1/media"
dest: /mnt/nas/media
fstype: nfs
opts: "rw,hard,intr,rsize=8192,wsize=8192"
- src: "192.168.86.30:/volume1/backups"
dest: /mnt/nas/backups
fstype: nfs
opts: "rw,hard,intr"
# Backup paths
backup_sources:
- /data/docker-compose
- /data/traefik
- /data/vaultwarden
- /data/grafana
- /data/prometheus
- /data/homeassistant
- /data/gitea
backup_destination: /mnt/nas/backups/peggy
+26
View File
@@ -0,0 +1,26 @@
---
all:
children:
docker_hosts:
hosts:
peggy:
ansible_host: 192.168.86.45
bart:
ansible_host: 192.168.86.181
nvr:
hosts:
bart:
yoda:
ansible_host: 192.168.86.31
ansible_connection: local
ansible_shell_type: cmd
network:
hosts:
firewalla:
ansible_host: 192.168.86.1
ansible_connection: local
storage:
hosts:
nas:
ansible_host: 192.168.86.30
ansible_connection: local
+12
View File
@@ -0,0 +1,12 @@
---
- name: Deploy automation stack
hosts: peggy
roles:
- role: docker
tags: [docker]
- role: gitea
tags: [automation, gitea]
- role: semaphore
tags: [automation, semaphore]
- role: openclaw
tags: [automation, openclaw]
+8
View File
@@ -0,0 +1,8 @@
---
- name: Deploy communication stack
hosts: peggy
roles:
- role: docker
tags: [docker]
- role: matrix
tags: [communication, matrix]
+14
View File
@@ -0,0 +1,14 @@
---
- name: Deploy core infrastructure
hosts: peggy
roles:
- role: common
tags: [common]
- role: docker
tags: [docker]
- role: traefik
tags: [traefik]
- role: vaultwarden
tags: [vaultwarden]
- role: adguard
tags: [adguard]
+42
View File
@@ -0,0 +1,42 @@
---
- name: Run backups
hosts: docker_hosts
become: true
tasks:
- name: Ensure backup destination exists
ansible.builtin.file:
path: "{{ backup_destination }}"
state: directory
mode: "0750"
tags: [backup]
- name: Run backup script
ansible.builtin.command:
cmd: /usr/local/bin/homelab-backup.sh
register: backup_result
changed_when: backup_result.rc == 0
tags: [backup]
- name: Display backup result
ansible.builtin.debug:
msg: "{{ backup_result.stdout_lines }}"
tags: [backup]
- name: Clean up backups older than 30 days
ansible.builtin.find:
paths: "{{ backup_destination }}"
age: 30d
recurse: false
file_type: file
patterns: "*.tar.gz"
register: old_backups
tags: [backup, cleanup]
- name: Remove old backups
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
loop_control:
label: "{{ item.path | basename }}"
tags: [backup, cleanup]
+87
View File
@@ -0,0 +1,87 @@
---
- name: Health check all services
hosts: docker_hosts
become: true
tasks:
- name: Check Docker daemon is running
ansible.builtin.systemd:
name: docker
state: started
check_mode: true
register: docker_status
tags: [health]
- name: Get running containers
community.docker.docker_host_info:
containers: true
register: docker_info
tags: [health]
- name: Display container count
ansible.builtin.debug:
msg: "{{ inventory_hostname }}: {{ docker_info.containers | length }} containers running"
tags: [health]
- name: Check for unhealthy containers
ansible.builtin.shell: |
docker ps --filter health=unhealthy --format '{{ '{{' }}.Names{{ '}}' }}'
register: unhealthy
changed_when: false
tags: [health]
- name: Report unhealthy containers
ansible.builtin.debug:
msg: "WARNING: Unhealthy containers on {{ inventory_hostname }}: {{ unhealthy.stdout_lines }}"
when: unhealthy.stdout | length > 0
tags: [health]
- name: Check disk usage
ansible.builtin.shell: |
df -h / /data | tail -n +2 | awk '{print $1, $5, $6}'
register: disk_usage
changed_when: false
tags: [health]
- name: Display disk usage
ansible.builtin.debug:
msg: "{{ disk_usage.stdout_lines }}"
tags: [health]
- name: Check memory usage
ansible.builtin.shell: |
free -h | grep Mem | awk '{print "Total:", $2, "Used:", $3, "Free:", $4}'
register: mem_usage
changed_when: false
tags: [health]
- name: Display memory usage
ansible.builtin.debug:
msg: "{{ mem_usage.stdout }}"
tags: [health]
- name: Check key services via HTTP
ansible.builtin.uri:
url: "https://{{ item }}.{{ domain }}"
validate_certs: false
status_code: [200, 301, 302, 303, 401, 403]
timeout: 10
loop:
- traefik
- vault
- grafana
- jellyfin
- gitea
- ha
register: http_checks
ignore_errors: true
tags: [health]
when: inventory_hostname == 'peggy'
- name: Report HTTP check results
ansible.builtin.debug:
msg: "{{ item.item }}.{{ domain }}: {{ item.status | default('FAILED') }}"
loop: "{{ http_checks.results | default([]) }}"
loop_control:
label: "{{ item.item | default('unknown') }}"
when: inventory_hostname == 'peggy'
tags: [health]
@@ -0,0 +1,47 @@
---
- name: Update Docker containers
hosts: docker_hosts
become: true
vars:
update_stack: all
tasks:
- name: Get list of compose directories
ansible.builtin.find:
paths: "{{ docker_compose_base }}"
file_type: directory
recurse: false
register: compose_dirs
tags: [update]
- name: Pull latest images for each stack
community.docker.docker_compose_v2_pull:
project_src: "{{ item.path }}"
loop: "{{ compose_dirs.files }}"
loop_control:
label: "{{ item.path | basename }}"
when: update_stack == 'all' or (item.path | basename) == update_stack
tags: [update]
- name: Recreate containers with new images
community.docker.docker_compose_v2:
project_src: "{{ item.path }}"
state: present
recreate: auto
loop: "{{ compose_dirs.files }}"
loop_control:
label: "{{ item.path | basename }}"
when: update_stack == 'all' or (item.path | basename) == update_stack
tags: [update]
- name: Prune unused Docker images
community.docker.docker_prune:
images: true
images_filters:
dangling: false
tags: [cleanup]
- name: Prune unused Docker volumes
community.docker.docker_prune:
volumes: true
tags: [cleanup]
when: prune_volumes | default(false) | bool
+8
View File
@@ -0,0 +1,8 @@
---
- name: Deploy media stack
hosts: peggy
roles:
- role: docker
tags: [docker]
- role: media
tags: [media, jellyfin, sonarr, radarr]
+8
View File
@@ -0,0 +1,8 @@
---
- name: Deploy monitoring stack
hosts: peggy
roles:
- role: docker
tags: [docker]
- role: monitoring
tags: [monitoring, prometheus, grafana, alertmanager]
+8
View File
@@ -0,0 +1,8 @@
---
- name: Apply security hardening
hosts: docker_hosts
roles:
- role: common
tags: [common]
- role: security
tags: [security]
+71
View File
@@ -0,0 +1,71 @@
---
# Full site deployment for hobbs.farm
- name: Apply common configuration
hosts: docker_hosts
tags: [common]
roles:
- common
- name: Configure Docker
hosts: docker_hosts
tags: [docker]
roles:
- docker
- name: Deploy core infrastructure
hosts: peggy
tags: [core]
roles:
- traefik
- vaultwarden
- adguard
- name: Deploy monitoring stack
hosts: peggy
tags: [monitoring]
roles:
- monitoring
- name: Deploy media stack
hosts: peggy
tags: [media]
roles:
- media
- name: Deploy communication stack
hosts: peggy
tags: [communication]
roles:
- matrix
- name: Deploy smart home stack
hosts: peggy
tags: [smarthome]
roles:
- home_assistant
- name: Deploy automation stack
hosts: peggy
tags: [automation]
roles:
- gitea
- semaphore
- openclaw
- name: Deploy Frigate NVR
hosts: bart
tags: [frigate]
roles:
- frigate
- name: Apply security hardening
hosts: docker_hosts
tags: [security]
roles:
- security
- name: Configure backups
hosts: docker_hosts
tags: [backup]
roles:
- backup
+8
View File
@@ -0,0 +1,8 @@
---
- name: Deploy smart home stack
hosts: peggy
roles:
- role: docker
tags: [docker]
- role: home_assistant
tags: [smarthome, homeassistant, esphome]
+14
View File
@@ -0,0 +1,14 @@
---
collections:
- name: community.docker
version: ">=3.4.0"
- name: community.general
version: ">=7.0.0"
- name: ansible.posix
version: ">=1.5.0"
roles:
- name: geerlingguy.docker
version: "6.1.0"
- name: geerlingguy.pip
version: "2.2.0"
+5
View File
@@ -0,0 +1,5 @@
---
adguard_compose_dir: "{{ docker_compose_base }}/core-infrastructure"
adguard_data_dir: /data/adguard
adguard_image: "adguard/adguardhome:latest"
adguard_host: "adguard.{{ domain }}"
+29
View File
@@ -0,0 +1,29 @@
---
- name: Create AdGuard directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ adguard_data_dir }}/work"
- "{{ adguard_data_dir }}/conf"
tags: [adguard]
- name: Deploy AdGuard compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ adguard_compose_dir }}/docker-compose.adguard.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [adguard]
- name: Start AdGuard
community.docker.docker_compose_v2:
project_src: "{{ adguard_compose_dir }}"
files:
- docker-compose.adguard.yml
state: present
tags: [adguard]
@@ -0,0 +1,24 @@
services:
adguard:
image: {{ adguard_image }}
container_name: adguard
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "8053:8053/tcp"
volumes:
- {{ adguard_data_dir }}/work:/opt/adguardhome/work
- {{ adguard_data_dir }}/conf:/opt/adguardhome/conf
labels:
traefik.enable: "true"
traefik.http.routers.adguard.rule: "Host(`{{ adguard_host }}`)"
traefik.http.routers.adguard.entrypoints: websecure
traefik.http.routers.adguard.tls.certresolver: cloudflare
traefik.http.services.adguard.loadbalancer.server.port: "3000"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+9
View File
@@ -0,0 +1,9 @@
---
backup_script_path: /usr/local/bin/homelab-backup.sh
backup_schedule: "0 2 * * *"
backup_retention_days: 30
backup_sources:
- /data/docker-compose
backup_destination: /mnt/nas/backups/{{ inventory_hostname }}
backup_pre_commands: []
backup_post_commands: []
+43
View File
@@ -0,0 +1,43 @@
---
- name: Ensure backup destination directory exists
ansible.builtin.file:
path: "{{ backup_destination }}"
state: directory
owner: glyptic
group: glyptic
mode: "0750"
tags: [backup]
- name: Deploy backup script
ansible.builtin.template:
src: backup.sh.j2
dest: "{{ backup_script_path }}"
owner: root
group: root
mode: "0750"
tags: [backup]
- name: Configure backup cron job
ansible.builtin.cron:
name: "homelab-backup"
minute: "0"
hour: "2"
job: "{{ backup_script_path }} >> /var/log/homelab-backup.log 2>&1"
user: root
tags: [backup]
- name: Configure backup log rotation
ansible.builtin.copy:
content: |
/var/log/homelab-backup.log {
weekly
rotate 4
compress
missingok
notifempty
}
dest: /etc/logrotate.d/homelab-backup
owner: root
group: root
mode: "0644"
tags: [backup]
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# Homelab Backup Script - Managed by Ansible
# Host: {{ inventory_hostname }}
set -euo pipefail
BACKUP_DIR="{{ backup_destination }}"
DATE=$(date +%Y%m%d_%H%M%S)
HOSTNAME="{{ inventory_hostname }}"
BACKUP_FILE="${BACKUP_DIR}/${HOSTNAME}_${DATE}.tar.gz"
RETENTION_DAYS="{{ backup_retention_days }}"
echo "=== Backup started at $(date) ==="
echo "Destination: ${BACKUP_FILE}"
# Pre-backup commands
{% for cmd in backup_pre_commands %}
echo "Running pre-backup: {{ cmd }}"
{{ cmd }}
{% endfor %}
# Create backup
tar czf "${BACKUP_FILE}" \
{% for source in backup_sources %}
{{ source }} \
{% endfor %}
2>/dev/null || true
BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1)
echo "Backup created: ${BACKUP_FILE} (${BACKUP_SIZE})"
# Post-backup commands
{% for cmd in backup_post_commands %}
echo "Running post-backup: {{ cmd }}"
{{ cmd }}
{% endfor %}
# Cleanup old backups
echo "Removing backups older than ${RETENTION_DAYS} days..."
find "${BACKUP_DIR}" -name "${HOSTNAME}_*.tar.gz" -mtime +${RETENTION_DAYS} -delete
echo "=== Backup completed at $(date) ==="
+24
View File
@@ -0,0 +1,24 @@
---
common_packages:
- htop
- iotop
- curl
- wget
- git
- jq
- unzip
- net-tools
- ncdu
- tmux
- rsync
- lsof
- vim
- ufw
- fail2ban
common_timezone: America/Chicago
common_hostname: "{{ hostname | default(inventory_hostname) }}"
sshd_port: 22
sshd_permit_root: "no"
sshd_password_auth: "no"
sshd_pubkey_auth: "yes"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart sshd
ansible.builtin.systemd:
name: sshd
state: restarted
+98
View File
@@ -0,0 +1,98 @@
---
- name: Set hostname
ansible.builtin.hostname:
name: "{{ common_hostname }}"
tags: [common, hostname]
- name: Set timezone
community.general.timezone:
name: "{{ common_timezone }}"
tags: [common, timezone]
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tags: [common, packages]
- name: Install common packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
tags: [common, packages]
- name: Upgrade all packages
ansible.builtin.apt:
upgrade: safe
tags: [common, packages, upgrade]
when: apt_upgrade | default(false) | bool
- name: Configure SSH daemon
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0644"
validate: "sshd -t -f %s"
notify: Restart sshd
tags: [common, ssh]
- name: Ensure SSH is running
ansible.builtin.systemd:
name: sshd
state: started
enabled: true
tags: [common, ssh]
- name: Configure UFW defaults
community.general.ufw:
direction: incoming
policy: deny
tags: [common, firewall]
- name: Allow SSH through UFW
community.general.ufw:
rule: allow
port: "{{ sshd_port }}"
proto: tcp
tags: [common, firewall]
- name: Allow traffic from local network
community.general.ufw:
rule: allow
src: 192.168.86.0/24
tags: [common, firewall]
- name: Enable UFW
community.general.ufw:
state: enabled
tags: [common, firewall]
- name: Configure fail2ban
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: true
tags: [common, security]
- name: Set vm.swappiness
ansible.posix.sysctl:
name: vm.swappiness
value: "10"
state: present
reload: true
tags: [common, sysctl]
- name: Configure NTP
ansible.builtin.apt:
name: chrony
state: present
tags: [common, ntp]
- name: Start chrony
ansible.builtin.systemd:
name: chrony
state: started
enabled: true
tags: [common, ntp]
+26
View File
@@ -0,0 +1,26 @@
# Managed by Ansible - do not edit manually
Port {{ sshd_port }}
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
PermitRootLogin {{ sshd_permit_root }}
PubkeyAuthentication {{ sshd_pubkey_auth }}
PasswordAuthentication {{ sshd_password_auth }}
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
MaxAuthTries 3
MaxSessions 10
ClientAliveInterval 300
ClientAliveCountMax 2
Subsystem sftp /usr/lib/openssh/sftp-server
+13
View File
@@ -0,0 +1,13 @@
---
docker_edition: ce
docker_users:
- glyptic
docker_install_compose_plugin: true
docker_daemon_options:
log-driver: json-file
log-opts:
max-size: "10m"
max-file: "3"
storage-driver: overlay2
live-restore: true
docker_network_name: homelab_network
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart docker
ansible.builtin.systemd:
name: docker
state: restarted
+76
View File
@@ -0,0 +1,76 @@
---
- name: Install Docker prerequisites
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-pip
- python3-docker
state: present
tags: [docker, install]
- name: Add Docker GPG key
ansible.builtin.apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
tags: [docker, install]
- name: Add Docker repository
ansible.builtin.apt_repository:
repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
tags: [docker, install]
- name: Install Docker CE
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: true
tags: [docker, install]
- name: Configure Docker daemon
ansible.builtin.copy:
content: "{{ docker_daemon_options | to_nice_json }}"
dest: /etc/docker/daemon.json
owner: root
group: root
mode: "0644"
notify: Restart docker
tags: [docker, config]
- name: Add users to docker group
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: true
loop: "{{ docker_users }}"
tags: [docker, users]
- name: Ensure Docker is started and enabled
ansible.builtin.systemd:
name: docker
state: started
enabled: true
tags: [docker]
- name: Create homelab Docker network
community.docker.docker_network:
name: "{{ docker_network_name }}"
driver: bridge
tags: [docker, network]
- name: Ensure docker-compose base directory exists
ansible.builtin.file:
path: "{{ docker_compose_base }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
tags: [docker]
+6
View File
@@ -0,0 +1,6 @@
---
frigate_compose_dir: /opt/frigate
frigate_config_dir: /opt/frigate/config
frigate_media_dir: /media/frigate
frigate_image: "ghcr.io/blakeblackshear/frigate:stable"
frigate_host: "frigate.{{ domain }}"
+28
View File
@@ -0,0 +1,28 @@
---
- name: Create Frigate directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ frigate_compose_dir }}"
- "{{ frigate_config_dir }}"
- "{{ frigate_media_dir }}"
tags: [frigate]
- name: Deploy Frigate compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ frigate_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [frigate]
- name: Start Frigate
community.docker.docker_compose_v2:
project_src: "{{ frigate_compose_dir }}"
state: present
tags: [frigate]
@@ -0,0 +1,30 @@
services:
frigate:
image: {{ frigate_image }}
container_name: frigate
restart: unless-stopped
privileged: true
shm_size: "256mb"
ports:
- "5000:5000"
- "8554:8554"
- "8555:8555/tcp"
- "8555:8555/udp"
volumes:
- {{ frigate_config_dir }}:/config
- {{ frigate_media_dir }}:/media/frigate
- /etc/localtime:/etc/localtime:ro
environment:
FRIGATE_RTSP_PASSWORD: "{{ vault_frigate_rtsp_password | default('changeme') }}"
labels:
traefik.enable: "true"
traefik.http.routers.frigate.rule: "Host(`{{ frigate_host }}`)"
traefik.http.routers.frigate.entrypoints: websecure
traefik.http.routers.frigate.tls.certresolver: cloudflare
traefik.http.services.frigate.loadbalancer.server.port: "5000"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+8
View File
@@ -0,0 +1,8 @@
---
gitea_compose_dir: "{{ docker_compose_base }}/automation"
gitea_data_dir: /data/gitea
gitea_image: "gitea/gitea:latest"
gitea_db_image: "postgres:16-alpine"
gitea_host: "git.{{ domain }}"
gitea_ssh_port: 2222
gitea_db_password: "{{ vault_gitea_db_password }}"
+28
View File
@@ -0,0 +1,28 @@
---
- name: Create Gitea directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ gitea_compose_dir }}"
- "{{ gitea_data_dir }}"
- "{{ gitea_data_dir }}/db"
tags: [gitea, automation]
- name: Deploy Gitea compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ gitea_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [gitea, automation]
- name: Start Gitea stack
community.docker.docker_compose_v2:
project_src: "{{ gitea_compose_dir }}"
state: present
tags: [gitea, automation]
@@ -0,0 +1,50 @@
services:
gitea:
image: {{ gitea_image }}
container_name: gitea
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: gitea-db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: {{ gitea_db_password }}
GITEA__server__DOMAIN: {{ gitea_host }}
GITEA__server__SSH_DOMAIN: {{ gitea_host }}
GITEA__server__SSH_PORT: {{ gitea_ssh_port }}
GITEA__server__ROOT_URL: "https://{{ gitea_host }}/"
volumes:
- {{ gitea_data_dir }}:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "{{ gitea_ssh_port }}:22"
depends_on:
- gitea-db
labels:
traefik.enable: "true"
traefik.http.routers.gitea.rule: "Host(`{{ gitea_host }}`)"
traefik.http.routers.gitea.entrypoints: websecure
traefik.http.routers.gitea.tls.certresolver: cloudflare
traefik.http.services.gitea.loadbalancer.server.port: "3000"
networks:
- {{ docker_network_name }}
gitea-db:
image: {{ gitea_db_image }}
container_name: gitea-db
restart: unless-stopped
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: {{ gitea_db_password }}
POSTGRES_DB: gitea
volumes:
- {{ gitea_data_dir }}/db:/var/lib/postgresql/data
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+12
View File
@@ -0,0 +1,12 @@
---
smarthome_compose_dir: "{{ docker_compose_base }}/smarthome"
ha_data_dir: /data/homeassistant
esphome_data_dir: /data/esphome
mosquitto_data_dir: /data/mosquitto
ha_image: "ghcr.io/home-assistant/home-assistant:stable"
esphome_image: "ghcr.io/esphome/esphome:latest"
mosquitto_image: "eclipse-mosquitto:latest"
ha_host: "ha.{{ domain }}"
esphome_host: "esphome.{{ domain }}"
+32
View File
@@ -0,0 +1,32 @@
---
- name: Create smart home directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ smarthome_compose_dir }}"
- "{{ ha_data_dir }}"
- "{{ esphome_data_dir }}"
- "{{ mosquitto_data_dir }}"
- "{{ mosquitto_data_dir }}/config"
- "{{ mosquitto_data_dir }}/data"
- "{{ mosquitto_data_dir }}/log"
tags: [smarthome, homeassistant]
- name: Deploy smart home compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ smarthome_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [smarthome, homeassistant]
- name: Start smart home stack
community.docker.docker_compose_v2:
project_src: "{{ smarthome_compose_dir }}"
state: present
tags: [smarthome, homeassistant]
@@ -0,0 +1,52 @@
services:
homeassistant:
image: {{ ha_image }}
container_name: homeassistant
restart: unless-stopped
privileged: true
environment:
TZ: {{ timezone }}
volumes:
- {{ ha_data_dir }}:/config
labels:
traefik.enable: "true"
traefik.http.routers.ha.rule: "Host(`{{ ha_host }}`)"
traefik.http.routers.ha.entrypoints: websecure
traefik.http.routers.ha.tls.certresolver: cloudflare
traefik.http.services.ha.loadbalancer.server.port: "8123"
networks:
- {{ docker_network_name }}
esphome:
image: {{ esphome_image }}
container_name: esphome
restart: unless-stopped
environment:
TZ: {{ timezone }}
volumes:
- {{ esphome_data_dir }}:/config
labels:
traefik.enable: "true"
traefik.http.routers.esphome.rule: "Host(`{{ esphome_host }}`)"
traefik.http.routers.esphome.entrypoints: websecure
traefik.http.routers.esphome.tls.certresolver: cloudflare
traefik.http.services.esphome.loadbalancer.server.port: "6052"
networks:
- {{ docker_network_name }}
mosquitto:
image: {{ mosquitto_image }}
container_name: mosquitto
restart: unless-stopped
ports:
- "1883:1883"
volumes:
- {{ mosquitto_data_dir }}/config:/mosquitto/config
- {{ mosquitto_data_dir }}/data:/mosquitto/data
- {{ mosquitto_data_dir }}/log:/mosquitto/log
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+7
View File
@@ -0,0 +1,7 @@
---
matrix_compose_dir: "{{ docker_compose_base }}/communication"
synapse_data_dir: /data/synapse
synapse_image: "matrixdotorg/synapse:latest"
matrix_host: "matrix.{{ domain }}"
matrix_server_name: "{{ domain }}"
matrix_shared_secret: "{{ vault_matrix_shared_secret }}"
+27
View File
@@ -0,0 +1,27 @@
---
- name: Create Matrix directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ matrix_compose_dir }}"
- "{{ synapse_data_dir }}"
tags: [matrix, communication]
- name: Deploy Matrix compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ matrix_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [matrix, communication]
- name: Start Matrix stack
community.docker.docker_compose_v2:
project_src: "{{ matrix_compose_dir }}"
state: present
tags: [matrix, communication]
@@ -0,0 +1,22 @@
services:
synapse:
image: {{ synapse_image }}
container_name: synapse
restart: unless-stopped
environment:
SYNAPSE_SERVER_NAME: {{ matrix_server_name }}
SYNAPSE_REPORT_STATS: "no"
volumes:
- {{ synapse_data_dir }}:/data
labels:
traefik.enable: "true"
traefik.http.routers.synapse.rule: "Host(`{{ matrix_host }}`)"
traefik.http.routers.synapse.entrypoints: websecure
traefik.http.routers.synapse.tls.certresolver: cloudflare
traefik.http.services.synapse.loadbalancer.server.port: "8008"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+27
View File
@@ -0,0 +1,27 @@
---
media_compose_dir: "{{ docker_compose_base }}/media"
jellyfin_data_dir: /data/jellyfin
sonarr_data_dir: /data/sonarr
radarr_data_dir: /data/radarr
sabnzbd_data_dir: /data/sabnzbd
nzbhydra_data_dir: /data/nzbhydra2
jellyseerr_data_dir: /data/jellyseerr
jellyfin_image: "jellyfin/jellyfin:latest"
sonarr_image: "lscr.io/linuxserver/sonarr:latest"
radarr_image: "lscr.io/linuxserver/radarr:latest"
sabnzbd_image: "lscr.io/linuxserver/sabnzbd:latest"
nzbhydra_image: "lscr.io/linuxserver/nzbhydra2:latest"
jellyseerr_image: "fallenbagel/jellyseerr:latest"
media_path: /mnt/nas/media
downloads_path: /mnt/nas/downloads
jellyfin_host: "jellyfin.{{ domain }}"
sonarr_host: "sonarr.{{ domain }}"
radarr_host: "radarr.{{ domain }}"
sabnzbd_host: "sabnzbd.{{ domain }}"
jellyseerr_host: "jellyseerr.{{ domain }}"
puid: "1000"
pgid: "1000"
+32
View File
@@ -0,0 +1,32 @@
---
- name: Create media directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ media_compose_dir }}"
- "{{ jellyfin_data_dir }}"
- "{{ sonarr_data_dir }}"
- "{{ radarr_data_dir }}"
- "{{ sabnzbd_data_dir }}"
- "{{ nzbhydra_data_dir }}"
- "{{ jellyseerr_data_dir }}"
tags: [media]
- name: Deploy media compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ media_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [media]
- name: Start media stack
community.docker.docker_compose_v2:
project_src: "{{ media_compose_dir }}"
state: present
tags: [media]
+116
View File
@@ -0,0 +1,116 @@
services:
jellyfin:
image: {{ jellyfin_image }}
container_name: jellyfin
restart: unless-stopped
environment:
JELLYFIN_PublishedServerUrl: "https://{{ jellyfin_host }}"
volumes:
- {{ jellyfin_data_dir }}/config:/config
- {{ jellyfin_data_dir }}/cache:/cache
- {{ media_path }}:/media
labels:
traefik.enable: "true"
traefik.http.routers.jellyfin.rule: "Host(`{{ jellyfin_host }}`)"
traefik.http.routers.jellyfin.entrypoints: websecure
traefik.http.routers.jellyfin.tls.certresolver: cloudflare
traefik.http.services.jellyfin.loadbalancer.server.port: "8096"
networks:
- {{ docker_network_name }}
sonarr:
image: {{ sonarr_image }}
container_name: sonarr
restart: unless-stopped
environment:
PUID: "{{ puid }}"
PGID: "{{ pgid }}"
TZ: {{ timezone }}
volumes:
- {{ sonarr_data_dir }}:/config
- {{ media_path }}/tv:/tv
- {{ downloads_path }}:/downloads
labels:
traefik.enable: "true"
traefik.http.routers.sonarr.rule: "Host(`{{ sonarr_host }}`)"
traefik.http.routers.sonarr.entrypoints: websecure
traefik.http.routers.sonarr.tls.certresolver: cloudflare
traefik.http.services.sonarr.loadbalancer.server.port: "8989"
networks:
- {{ docker_network_name }}
radarr:
image: {{ radarr_image }}
container_name: radarr
restart: unless-stopped
environment:
PUID: "{{ puid }}"
PGID: "{{ pgid }}"
TZ: {{ timezone }}
volumes:
- {{ radarr_data_dir }}:/config
- {{ media_path }}/movies:/movies
- {{ downloads_path }}:/downloads
labels:
traefik.enable: "true"
traefik.http.routers.radarr.rule: "Host(`{{ radarr_host }}`)"
traefik.http.routers.radarr.entrypoints: websecure
traefik.http.routers.radarr.tls.certresolver: cloudflare
traefik.http.services.radarr.loadbalancer.server.port: "7878"
networks:
- {{ docker_network_name }}
sabnzbd:
image: {{ sabnzbd_image }}
container_name: sabnzbd
restart: unless-stopped
environment:
PUID: "{{ puid }}"
PGID: "{{ pgid }}"
TZ: {{ timezone }}
volumes:
- {{ sabnzbd_data_dir }}:/config
- {{ downloads_path }}:/downloads
labels:
traefik.enable: "true"
traefik.http.routers.sabnzbd.rule: "Host(`{{ sabnzbd_host }}`)"
traefik.http.routers.sabnzbd.entrypoints: websecure
traefik.http.routers.sabnzbd.tls.certresolver: cloudflare
traefik.http.services.sabnzbd.loadbalancer.server.port: "8080"
networks:
- {{ docker_network_name }}
nzbhydra2:
image: {{ nzbhydra_image }}
container_name: nzbhydra2
restart: unless-stopped
environment:
PUID: "{{ puid }}"
PGID: "{{ pgid }}"
TZ: {{ timezone }}
volumes:
- {{ nzbhydra_data_dir }}:/config
- {{ downloads_path }}:/downloads
networks:
- {{ docker_network_name }}
jellyseerr:
image: {{ jellyseerr_image }}
container_name: jellyseerr
restart: unless-stopped
environment:
TZ: {{ timezone }}
volumes:
- {{ jellyseerr_data_dir }}:/app/config
labels:
traefik.enable: "true"
traefik.http.routers.jellyseerr.rule: "Host(`{{ jellyseerr_host }}`)"
traefik.http.routers.jellyseerr.entrypoints: websecure
traefik.http.routers.jellyseerr.tls.certresolver: cloudflare
traefik.http.services.jellyseerr.loadbalancer.server.port: "5055"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+18
View File
@@ -0,0 +1,18 @@
---
monitoring_compose_dir: "{{ docker_compose_base }}/monitoring"
prometheus_data_dir: /data/prometheus
grafana_data_dir: /data/grafana
alertmanager_data_dir: /data/alertmanager
prometheus_image: "prom/prometheus:latest"
grafana_image: "grafana/grafana:latest"
alertmanager_image: "prom/alertmanager:latest"
node_exporter_image: "prom/node-exporter:latest"
cadvisor_image: "gcr.io/cadvisor/cadvisor:latest"
prometheus_retention: 30d
prometheus_scrape_interval: 15s
grafana_admin_password: "{{ vault_grafana_admin_password }}"
grafana_host: "grafana.{{ domain }}"
prometheus_host: "prometheus.{{ domain }}"
alertmanager_host: "alertmanager.{{ domain }}"
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart monitoring stack
community.docker.docker_compose_v2:
project_src: "{{ monitoring_compose_dir }}"
state: present
recreate: always
+61
View File
@@ -0,0 +1,61 @@
---
- name: Create monitoring directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ monitoring_compose_dir }}"
- "{{ prometheus_data_dir }}"
- "{{ prometheus_data_dir }}/config"
- "{{ grafana_data_dir }}"
- "{{ alertmanager_data_dir }}"
- "{{ alertmanager_data_dir }}/config"
tags: [monitoring]
- name: Set Grafana data directory ownership
ansible.builtin.file:
path: "{{ grafana_data_dir }}"
state: directory
owner: "472"
group: "472"
mode: "0755"
tags: [monitoring, grafana]
- name: Deploy Prometheus configuration
ansible.builtin.template:
src: prometheus.yml.j2
dest: "{{ prometheus_data_dir }}/config/prometheus.yml"
owner: glyptic
group: docker
mode: "0644"
notify: Restart monitoring stack
tags: [monitoring, prometheus]
- name: Deploy Alertmanager configuration
ansible.builtin.template:
src: alertmanager.yml.j2
dest: "{{ alertmanager_data_dir }}/config/alertmanager.yml"
owner: glyptic
group: docker
mode: "0644"
notify: Restart monitoring stack
tags: [monitoring, alertmanager]
- name: Deploy monitoring compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ monitoring_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
notify: Restart monitoring stack
tags: [monitoring]
- name: Start monitoring stack
community.docker.docker_compose_v2:
project_src: "{{ monitoring_compose_dir }}"
state: present
tags: [monitoring]
@@ -0,0 +1,29 @@
global:
resolve_timeout: 5m
{% if alertmanager_smtp_host is defined and alertmanager_smtp_host %}
smtp_smarthost: "{{ alertmanager_smtp_host }}"
smtp_from: "{{ alertmanager_smtp_from }}"
{% endif %}
route:
group_by: ["alertname", "instance"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: default
receivers:
- name: default
{% if alertmanager_slack_webhook is defined and alertmanager_slack_webhook %}
slack_configs:
- api_url: "{{ alertmanager_slack_webhook }}"
channel: "#alerts"
send_resolved: true
{% endif %}
inhibit_rules:
- source_match:
severity: critical
target_match:
severity: warning
equal: ["alertname", "instance"]
@@ -0,0 +1,86 @@
services:
prometheus:
image: {{ prometheus_image }}
container_name: prometheus
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time={{ prometheus_retention }}
- --web.enable-lifecycle
volumes:
- {{ prometheus_data_dir }}/config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- {{ prometheus_data_dir }}/data:/prometheus
labels:
traefik.enable: "true"
traefik.http.routers.prometheus.rule: "Host(`{{ prometheus_host }}`)"
traefik.http.routers.prometheus.entrypoints: websecure
traefik.http.routers.prometheus.tls.certresolver: cloudflare
traefik.http.services.prometheus.loadbalancer.server.port: "9090"
networks:
- {{ docker_network_name }}
grafana:
image: {{ grafana_image }}
container_name: grafana
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: {{ grafana_admin_password }}
GF_SERVER_ROOT_URL: "https://{{ grafana_host }}"
GF_SERVER_DOMAIN: "{{ grafana_host }}"
volumes:
- {{ grafana_data_dir }}:/var/lib/grafana
labels:
traefik.enable: "true"
traefik.http.routers.grafana.rule: "Host(`{{ grafana_host }}`)"
traefik.http.routers.grafana.entrypoints: websecure
traefik.http.routers.grafana.tls.certresolver: cloudflare
traefik.http.services.grafana.loadbalancer.server.port: "3000"
networks:
- {{ docker_network_name }}
alertmanager:
image: {{ alertmanager_image }}
container_name: alertmanager
restart: unless-stopped
command:
- --config.file=/etc/alertmanager/alertmanager.yml
volumes:
- {{ alertmanager_data_dir }}/config/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
labels:
traefik.enable: "true"
traefik.http.routers.alertmanager.rule: "Host(`{{ alertmanager_host }}`)"
traefik.http.routers.alertmanager.entrypoints: websecure
traefik.http.routers.alertmanager.tls.certresolver: cloudflare
traefik.http.services.alertmanager.loadbalancer.server.port: "9093"
networks:
- {{ docker_network_name }}
node-exporter:
image: {{ node_exporter_image }}
container_name: node-exporter
restart: unless-stopped
command:
- --path.rootfs=/host
volumes:
- /:/host:ro,rslave
pid: host
networks:
- {{ docker_network_name }}
cadvisor:
image: {{ cadvisor_image }}
container_name: cadvisor
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
@@ -0,0 +1,37 @@
global:
scrape_interval: {{ prometheus_scrape_interval }}
evaluation_interval: {{ prometheus_scrape_interval }}
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node-exporter
static_configs:
- targets:
- node-exporter:9100
labels:
instance: peggy
- targets:
- 192.168.86.181:9100
labels:
instance: bart
- job_name: cadvisor
static_configs:
- targets: ["cadvisor:8080"]
- job_name: traefik
static_configs:
- targets: ["traefik:8080"]
- job_name: adguard
static_configs:
- targets: ["adguard-exporter:9617"]
+5
View File
@@ -0,0 +1,5 @@
---
openclaw_compose_dir: "{{ docker_compose_base }}/automation"
openclaw_data_dir: /data/openclaw
openclaw_image: "ghcr.io/openclaw/openclaw:latest"
openclaw_host: "openclaw.{{ domain }}"
+28
View File
@@ -0,0 +1,28 @@
---
- name: Create OpenClaw directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ openclaw_data_dir }}"
tags: [openclaw, automation]
- name: Deploy OpenClaw compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ openclaw_compose_dir }}/docker-compose.openclaw.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [openclaw, automation]
- name: Start OpenClaw
community.docker.docker_compose_v2:
project_src: "{{ openclaw_compose_dir }}"
files:
- docker-compose.openclaw.yml
state: present
tags: [openclaw, automation]
@@ -0,0 +1,22 @@
services:
openclaw:
image: {{ openclaw_image }}
container_name: openclaw
restart: unless-stopped
environment:
TZ: {{ timezone }}
volumes:
- {{ openclaw_data_dir }}:/data
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
traefik.enable: "true"
traefik.http.routers.openclaw.rule: "Host(`{{ openclaw_host }}`)"
traefik.http.routers.openclaw.entrypoints: websecure
traefik.http.routers.openclaw.tls.certresolver: cloudflare
traefik.http.services.openclaw.loadbalancer.server.port: "3000"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+5
View File
@@ -0,0 +1,5 @@
---
security_sysctl_hardening: true
security_audit_enabled: true
security_auto_updates: true
security_ssh_max_auth_tries: 3
+9
View File
@@ -0,0 +1,9 @@
---
- name: Reload sysctl
ansible.builtin.command: sysctl --system
changed_when: true
- name: Restart auditd
ansible.builtin.systemd:
name: auditd
state: restarted
+73
View File
@@ -0,0 +1,73 @@
---
- name: Deploy hardened sysctl configuration
ansible.builtin.template:
src: sysctl.conf.j2
dest: /etc/sysctl.d/99-hardening.conf
owner: root
group: root
mode: "0644"
notify: Reload sysctl
when: security_sysctl_hardening
tags: [security, sysctl]
- name: Install auditd
ansible.builtin.apt:
name:
- auditd
- audispd-plugins
state: present
when: security_audit_enabled
tags: [security, audit]
- name: Deploy audit rules
ansible.builtin.template:
src: audit.rules.j2
dest: /etc/audit/rules.d/hardening.rules
owner: root
group: root
mode: "0640"
notify: Restart auditd
when: security_audit_enabled
tags: [security, audit]
- name: Enable unattended upgrades
ansible.builtin.apt:
name: unattended-upgrades
state: present
when: security_auto_updates
tags: [security, updates]
- name: Configure unattended upgrades
ansible.builtin.copy:
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
when: security_auto_updates
tags: [security, updates]
- name: Disable core dumps
ansible.posix.sysctl:
name: fs.suid_dumpable
value: "0"
state: present
reload: true
tags: [security]
- name: Set secure permissions on cron directories
ansible.builtin.file:
path: "{{ item }}"
owner: root
group: root
mode: "0700"
loop:
- /etc/cron.d
- /etc/cron.daily
- /etc/cron.hourly
- /etc/cron.weekly
- /etc/cron.monthly
tags: [security]
+24
View File
@@ -0,0 +1,24 @@
# Audit rules - Managed by Ansible
# Monitor changes to authentication configuration
-w /etc/pam.d/ -p wa -k auth_changes
-w /etc/nsswitch.conf -p wa -k auth_changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Monitor user/group changes
-w /etc/passwd -p wa -k user_changes
-w /etc/group -p wa -k group_changes
-w /etc/shadow -p wa -k shadow_changes
# Monitor sudo usage
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# Monitor Docker
-w /usr/bin/docker -p rwxa -k docker
-w /var/lib/docker -p wa -k docker
-w /etc/docker -p wa -k docker
-w /usr/lib/systemd/system/docker.service -p wa -k docker
# Monitor cron
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
+24
View File
@@ -0,0 +1,24 @@
# Network hardening - Managed by Ansible
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Memory hardening
kernel.randomize_va_space = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
# Docker needs IP forwarding
net.ipv4.ip_forward = 1
+6
View File
@@ -0,0 +1,6 @@
---
semaphore_compose_dir: "{{ docker_compose_base }}/automation"
semaphore_data_dir: /data/semaphore
semaphore_image: "semaphoreui/semaphore:latest"
semaphore_host: "semaphore.{{ domain }}"
semaphore_db_password: "{{ vault_semaphore_db_password }}"
+28
View File
@@ -0,0 +1,28 @@
---
- name: Create Semaphore directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ semaphore_data_dir }}"
tags: [semaphore, automation]
- name: Deploy Semaphore compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ semaphore_compose_dir }}/docker-compose.semaphore.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [semaphore, automation]
- name: Start Semaphore
community.docker.docker_compose_v2:
project_src: "{{ semaphore_compose_dir }}"
files:
- docker-compose.semaphore.yml
state: present
tags: [semaphore, automation]
@@ -0,0 +1,26 @@
services:
semaphore:
image: {{ semaphore_image }}
container_name: semaphore
restart: unless-stopped
environment:
SEMAPHORE_DB_DIALECT: bolt
SEMAPHORE_ADMIN_PASSWORD: "{{ vault_semaphore_admin_password | default('changeme') }}"
SEMAPHORE_ADMIN_NAME: admin
SEMAPHORE_ADMIN_EMAIL: admin@{{ domain }}
SEMAPHORE_ADMIN: admin
SEMAPHORE_ACCESS_KEY_ENCRYPTION: "{{ vault_semaphore_encryption_key | default('generate-a-key') }}"
volumes:
- {{ semaphore_data_dir }}:/tmp/semaphore
labels:
traefik.enable: "true"
traefik.http.routers.semaphore.rule: "Host(`{{ semaphore_host }}`)"
traefik.http.routers.semaphore.entrypoints: websecure
traefik.http.routers.semaphore.tls.certresolver: cloudflare
traefik.http.services.semaphore.loadbalancer.server.port: "3000"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+10
View File
@@ -0,0 +1,10 @@
---
traefik_version: "v3.1"
traefik_image: "traefik:{{ traefik_version }}"
traefik_compose_dir: "{{ docker_compose_base }}/core-infrastructure"
traefik_data_dir: /data/traefik
traefik_acme_email: "{{ vault_acme_email }}"
cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
traefik_log_level: INFO
traefik_dashboard_enabled: true
traefik_dashboard_host: "traefik.{{ domain }}"
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart traefik stack
community.docker.docker_compose_v2:
project_src: "{{ traefik_compose_dir }}"
state: present
recreate: always
+41
View File
@@ -0,0 +1,41 @@
---
- name: Create Traefik directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
loop:
- "{{ traefik_compose_dir }}"
- "{{ traefik_data_dir }}"
- "{{ traefik_data_dir }}/acme"
- "{{ traefik_data_dir }}/dynamic"
tags: [traefik]
- name: Ensure acme.json exists with correct permissions
ansible.builtin.file:
path: "{{ traefik_data_dir }}/acme/acme.json"
state: touch
owner: root
group: root
mode: "0600"
modification_time: preserve
access_time: preserve
tags: [traefik]
- name: Deploy Traefik compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ traefik_compose_dir }}/docker-compose.yml"
owner: glyptic
group: docker
mode: "0644"
notify: Restart traefik stack
tags: [traefik]
- name: Start Traefik stack
community.docker.docker_compose_v2:
project_src: "{{ traefik_compose_dir }}"
state: present
tags: [traefik]
@@ -0,0 +1,46 @@
services:
traefik:
image: {{ traefik_image }}
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
environment:
CF_DNS_API_TOKEN: {{ cloudflare_api_token }}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- {{ traefik_data_dir }}/acme/acme.json:/acme.json
- {{ traefik_data_dir }}/dynamic:/dynamic
command:
- --api.dashboard={{ traefik_dashboard_enabled | lower }}
- --log.level={{ traefik_log_level }}
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network={{ docker_network_name }}
- --providers.file.directory=/dynamic
- --providers.file.watch=true
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.websecure.address=:443
- --certificatesresolvers.cloudflare.acme.dnschallenge=true
- --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cloudflare.acme.email={{ traefik_acme_email }}
- --certificatesresolvers.cloudflare.acme.storage=/acme.json
- --certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53
labels:
traefik.enable: "true"
traefik.http.routers.traefik.rule: "Host(`{{ traefik_dashboard_host }}`)"
traefik.http.routers.traefik.entrypoints: websecure
traefik.http.routers.traefik.tls.certresolver: cloudflare
traefik.http.routers.traefik.tls.domains[0].main: "{{ domain }}"
traefik.http.routers.traefik.tls.domains[0].sans: "*.{{ domain }}"
traefik.http.routers.traefik.service: api@internal
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true
+7
View File
@@ -0,0 +1,7 @@
---
vaultwarden_image: "vaultwarden/server:latest"
vaultwarden_compose_dir: "{{ docker_compose_base }}/core-infrastructure"
vaultwarden_data_dir: /data/vaultwarden
vaultwarden_host: "vault.{{ domain }}"
vaultwarden_admin_token: "{{ vault_vaultwarden_admin_token }}"
vaultwarden_signups_allowed: false
+26
View File
@@ -0,0 +1,26 @@
---
- name: Create Vaultwarden data directory
ansible.builtin.file:
path: "{{ vaultwarden_data_dir }}"
state: directory
owner: glyptic
group: docker
mode: "0755"
tags: [vaultwarden]
- name: Deploy Vaultwarden compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ vaultwarden_compose_dir }}/docker-compose.vaultwarden.yml"
owner: glyptic
group: docker
mode: "0644"
tags: [vaultwarden]
- name: Start Vaultwarden
community.docker.docker_compose_v2:
project_src: "{{ vaultwarden_compose_dir }}"
files:
- docker-compose.vaultwarden.yml
state: present
tags: [vaultwarden]
@@ -0,0 +1,24 @@
services:
vaultwarden:
image: {{ vaultwarden_image }}
container_name: vaultwarden
restart: unless-stopped
environment:
ADMIN_TOKEN: {{ vaultwarden_admin_token }}
SIGNUPS_ALLOWED: "{{ vaultwarden_signups_allowed | lower }}"
DOMAIN: "https://{{ vaultwarden_host }}"
WEBSOCKET_ENABLED: "true"
volumes:
- {{ vaultwarden_data_dir }}:/data
labels:
traefik.enable: "true"
traefik.http.routers.vaultwarden.rule: "Host(`{{ vaultwarden_host }}`)"
traefik.http.routers.vaultwarden.entrypoints: websecure
traefik.http.routers.vaultwarden.tls.certresolver: cloudflare
traefik.http.services.vaultwarden.loadbalancer.server.port: "80"
networks:
- {{ docker_network_name }}
networks:
{{ docker_network_name }}:
external: true