You've already forked RekomenciBackend
@@ -0,0 +1,4 @@
|
||||
# Change all vars before going to production and remove all comments (!)
|
||||
# Below all environment variables and default values
|
||||
|
||||
TAILSCALE_KEY=
|
||||
@@ -0,0 +1,15 @@
|
||||
# Ansible Galaxy roles
|
||||
external_roles/*
|
||||
|
||||
# Python files
|
||||
__pycache__/
|
||||
|
||||
# dotenv file
|
||||
.env
|
||||
|
||||
# Inventory
|
||||
inventory/host_vars/*
|
||||
inventory/hosts
|
||||
|
||||
# Unignore .gitkeep files
|
||||
!.gitkeep
|
||||
@@ -0,0 +1,6 @@
|
||||
# VM Setup
|
||||
|
||||
```bash
|
||||
ansible-galaxy collection install -r requirements.yaml
|
||||
ansible-galaxy install -r requirements.yaml -p external_roles
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts
|
||||
roles_path = roles:external_roles
|
||||
library = plugins/modules
|
||||
filter_plugins = plugins/filter
|
||||
callback_plugins = plugins/callback
|
||||
|
||||
host_key_checking = False
|
||||
allow_world_readable_tmpfiles = False
|
||||
remote_tmp = ~/.ansible/tmp
|
||||
local_tmp = ~/.ansible/tmp
|
||||
|
||||
any_errors_fatal = False
|
||||
forks = 20
|
||||
poll_interval = 15
|
||||
timeout = 30
|
||||
gathering = smart
|
||||
fact_caching = memory
|
||||
fact_caching_timeout = 300
|
||||
|
||||
stdout_callback = yaml
|
||||
bin_ansible_callbacks = True
|
||||
display_skipped_hosts = False
|
||||
deprecation_warnings = True
|
||||
command_warnings = False
|
||||
|
||||
interpreter_python = auto_silent
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args =
|
||||
-o ControlMaster=auto
|
||||
-o ControlPersist=60s
|
||||
-o ServerAliveInterval=30
|
||||
-o ServerAliveCountMax=3
|
||||
-o ConnectTimeout=10
|
||||
-o TCPKeepAlive=yes
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
-o StrictHostKeyChecking=no
|
||||
-o IdentitiesOnly=yes
|
||||
|
||||
pipelining = True
|
||||
control_path_dir = ~/.ansible/cp
|
||||
scp_if_ssh = True
|
||||
retries = 3
|
||||
|
||||
[inventory]
|
||||
cache = True
|
||||
cache_plugin = memory
|
||||
cache_timeout = 300
|
||||
enable_plugins = host_list, script, auto, yaml, ini, toml
|
||||
|
||||
[galaxy]
|
||||
role_file = requirements.yaml
|
||||
ignore_certs = False
|
||||
no_deps = False
|
||||
|
||||
[persistent_connection]
|
||||
connect_timeout = 600
|
||||
command_timeout = 600
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
- name: Application Deployment
|
||||
hosts: servers
|
||||
gather_facts: true
|
||||
become: true
|
||||
serial: 1
|
||||
|
||||
roles:
|
||||
- role: dokploy
|
||||
when: applications.dokploy.enabled | bool
|
||||
vars:
|
||||
dokploy_state: "{{ applications.dokploy.state | default('present') }}"
|
||||
tags: dokploy, apps
|
||||
|
||||
- role: coolify
|
||||
when: applications.coolify.enabled | bool
|
||||
vars:
|
||||
coolify_state: "{{ applications.coolify.state | default('present') }}"
|
||||
tags: coolify, apps
|
||||
|
||||
- role: artis3n.tailscale.machine
|
||||
when: applications.tailscale.enabled | bool
|
||||
tags: tailscale, apps
|
||||
|
||||
- role: borgbase.ansible_role_borgbackup
|
||||
when: applications.borgbackup.enabled | bool
|
||||
tags: borgbackup, apps
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
- name: Base system setup
|
||||
hosts: servers
|
||||
gather_facts: true
|
||||
become: true
|
||||
serial: "100%"
|
||||
|
||||
pre_tasks:
|
||||
- name: Update apt cache and upgrade system
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
upgrade: dist
|
||||
register: apt_upgrade
|
||||
tags: system, updates
|
||||
|
||||
- name: Autoremove and clean
|
||||
ansible.builtin.apt:
|
||||
autoremove: true
|
||||
autoclean: true
|
||||
tags: system, updates
|
||||
|
||||
- name: Check system requirements
|
||||
block:
|
||||
- name: Verify Python 3 is available
|
||||
ansible.builtin.command: which python3
|
||||
register: python_check
|
||||
changed_when: false
|
||||
failed_when: python_check.rc != 0
|
||||
|
||||
- name: Check available memory
|
||||
ansible.builtin.setup:
|
||||
filter: ansible_memtotal_mb
|
||||
register: memory_info
|
||||
failed_when: memory_info.ansible_facts.ansible_memtotal_mb < 512
|
||||
tags: validation
|
||||
|
||||
roles:
|
||||
- role: common
|
||||
tags: common, system, bootstrap
|
||||
|
||||
- role: security
|
||||
tags: security, harden
|
||||
|
||||
- role: monitoring
|
||||
when: monitoring_enabled | bool
|
||||
tags: monitoring
|
||||
|
||||
post_tasks:
|
||||
- name: Display system summary
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
System setup completed on {{ inventory_hostname }}
|
||||
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
|
||||
Kernel: {{ ansible_kernel }}
|
||||
Architecture: {{ ansible_architecture }}
|
||||
Memory: {{ ansible_memtotal_mb }}MB
|
||||
CPUs: {{ ansible_processor_vcpus }}
|
||||
Storage: {{ ansible_devices.vda.size if ansible_devices.vda is defined
|
||||
else (ansible_devices.sda.size if ansible_devices.sda is defined
|
||||
else 'N/A') }}
|
||||
tags: always, info
|
||||
|
||||
- name: Check if a reboot is required after updates
|
||||
ansible.builtin.stat:
|
||||
path: /var/run/reboot-required
|
||||
register: reboot_required_file
|
||||
tags: system
|
||||
|
||||
- name: Reboot if required
|
||||
ansible.builtin.reboot:
|
||||
msg: "Reboot triggered by Ansible for system updates"
|
||||
connect_timeout: 10
|
||||
reboot_timeout: 600
|
||||
pre_reboot_delay: 5
|
||||
post_reboot_delay: 45
|
||||
test_command: uptime
|
||||
when: reboot_required_file.stat.exists
|
||||
register: reboot_result
|
||||
async: 600
|
||||
poll: 0
|
||||
tags: system
|
||||
|
||||
- name: Wait for reboot to complete
|
||||
ansible.builtin.wait_for_connection:
|
||||
connect_timeout: 20
|
||||
sleep: 5
|
||||
delay: 5
|
||||
timeout: 600
|
||||
when: reboot_required_file.stat.exists
|
||||
tags: system
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Docker Setup
|
||||
hosts: servers
|
||||
gather_facts: true
|
||||
become: true
|
||||
serial: "100%"
|
||||
|
||||
roles:
|
||||
- role: geerlingguy.docker
|
||||
tags: docker
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
ansible_become: true
|
||||
ansible_become_method: sudo
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
# System configuration
|
||||
timezone: UTC
|
||||
system_locale: en_US.UTF-8
|
||||
|
||||
# Monitoring
|
||||
monitoring_enabled: true
|
||||
monitoring_prometheus_node_exporter: true
|
||||
|
||||
# User management
|
||||
admin_users:
|
||||
- name: "{{ ansible_user }}"
|
||||
groups: "sudo"
|
||||
shell: /bin/bash
|
||||
ssh_keys: "{{ admin_ssh_keys | default([]) }}"
|
||||
state: present
|
||||
|
||||
# Applications management
|
||||
applications:
|
||||
coolify:
|
||||
enabled: false
|
||||
state: latest
|
||||
dokploy:
|
||||
enabled: false
|
||||
state: latest
|
||||
tailscale:
|
||||
enabled: false
|
||||
borgbackup:
|
||||
enabled: false
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
borg_repository:
|
||||
borg_install_method: pip
|
||||
borg_lock_wait_time: 10
|
||||
borg_source_directories:
|
||||
- /home
|
||||
- /root
|
||||
- /etc
|
||||
- /srv
|
||||
- /var/www
|
||||
- /var/lib/docker/volumes
|
||||
borg_ssh_key_type: "ed25519"
|
||||
borg_retention_policy:
|
||||
keep_hourly: 1
|
||||
keep_daily: 1
|
||||
keep_weekly: 1
|
||||
keep_monthly: 3
|
||||
|
||||
borgmatic_timer_cron_name: "borgmatic"
|
||||
borgmatic_timer: cron
|
||||
borgmatic_timer_hour: "{{ range(0, 5) | random(seed=inventory_hostname) }}"
|
||||
borgmatic_timer_minute: "{{ range(0, 59) | random(seed=inventory_hostname) }}"
|
||||
borgmatic_timer_flags: ""
|
||||
|
||||
borgmatic_config_name: config.yaml
|
||||
borgmatic_hooks:
|
||||
on_error:
|
||||
- echo "`date` - Error while creating a backup."
|
||||
before_backup:
|
||||
- echo "`date` - Starting backup."
|
||||
after_backup:
|
||||
- echo "`date` - Finished backup."
|
||||
borgmatic_checks:
|
||||
- name: repository
|
||||
frequency: "4 weeks"
|
||||
- name: archives
|
||||
frequency: "6 weeks"
|
||||
borgmatic_check_last: 3
|
||||
borgmatic_store_atime: false
|
||||
borgmatic_store_ctime: false
|
||||
borgmatic_relocated_repo_access_is_ok: false
|
||||
borgmatic_version: ">=1.7.11"
|
||||
|
||||
borg_venv_path: /opt/borgmatic
|
||||
borg_user: root
|
||||
borg_group: root
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
docker_edition: "ce"
|
||||
docker_install_compose_plugin: true
|
||||
docker_users:
|
||||
- "{{ ansible_user }}"
|
||||
docker_daemon_options:
|
||||
storage-driver: "overlay2"
|
||||
|
||||
log-driver: "json-file"
|
||||
log-opts:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
live-restore: false
|
||||
icc: false
|
||||
userland-proxy: false
|
||||
default-address-pools: [{"base": "10.200.0.0/16", "size": 24}]
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
ntp_enabled: true
|
||||
ntp_timezone: Etc/UTC
|
||||
ntp_manage_config: true
|
||||
|
||||
ntp_servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
- 3.pool.ntp.org
|
||||
ntp_restrict:
|
||||
- "127.0.0.1"
|
||||
- "::1"
|
||||
|
||||
ntp_cron_handler_enabled: true
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
system_packages:
|
||||
essential:
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- htop
|
||||
- vim
|
||||
- gnupg
|
||||
- ca-certificates
|
||||
- apt-transport-https
|
||||
- software-properties-common
|
||||
- iproute2
|
||||
- net-tools
|
||||
- unzip
|
||||
- jq
|
||||
- tree
|
||||
- bash-completion
|
||||
- tmux
|
||||
- rsync
|
||||
- python3-docker
|
||||
- borgbackup
|
||||
monitoring:
|
||||
- atop
|
||||
- iotop
|
||||
- nethogs
|
||||
- nload
|
||||
- sysstat
|
||||
- dstat
|
||||
- smartmontools
|
||||
security:
|
||||
- fail2ban
|
||||
- nftables
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
security_firewall_default_policy: drop
|
||||
security_firewall_allowed_ports:
|
||||
- "{{ security_ssh_port }}/tcp"
|
||||
- "80/tcp"
|
||||
- "443/tcp"
|
||||
- "443/udp"
|
||||
- "53/udp"
|
||||
|
||||
security_ssh_port: 2424
|
||||
security_fail2ban_enabled: true
|
||||
security_fail2ban_custom_configuration_template: "jail.local.j2"
|
||||
security_autoupdate_enabled: true
|
||||
|
||||
ssh_config:
|
||||
permit_root_login: "no"
|
||||
password_authentication: "no"
|
||||
challenge_response_authentication: "no"
|
||||
use_pam: "yes"
|
||||
x11_forwarding: "no"
|
||||
client_alive_interval: 300
|
||||
client_alive_count_max: 2
|
||||
max_auth_tries: 3
|
||||
max_sessions: 10
|
||||
allow_users: "root {{ admin_users | map(attribute='name') | join(' ') }}"
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
sysctl_tuning:
|
||||
# Network tuning
|
||||
net.core.somaxconn: 65535
|
||||
net.ipv4.tcp_max_syn_backlog: 65535
|
||||
net.ipv4.tcp_fin_timeout: 30
|
||||
net.ipv4.tcp_keepalive_time: 600
|
||||
net.ipv4.tcp_keepalive_probes: 5
|
||||
net.ipv4.tcp_keepalive_intvl: 15
|
||||
net.ipv4.ip_local_port_range: "1024 65535"
|
||||
|
||||
# Memory tuning
|
||||
vm.swappiness: 10
|
||||
vm.vfs_cache_pressure: 50
|
||||
vm.dirty_ratio: 15
|
||||
vm.dirty_background_ratio: 5
|
||||
vm.overcommit_memory: 1
|
||||
vm.overcommit_ratio: 90
|
||||
|
||||
# Security tuning
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
state: "{{ applications.tailscale.state | default('latest') }}"
|
||||
|
||||
tailscale_authkey: "{{ lookup('env', 'TAILSCALE_KEY') }}"
|
||||
tailscale_tags: >-
|
||||
{{
|
||||
['vm'] +
|
||||
([hostname.split('-')[-1]] if '-' in hostname else [])
|
||||
}}
|
||||
tailscale_args: "--accept-dns=true --accept-routes=false --netfilter-mode on --shields-up=false --ssh=true --stateful-filtering=false"
|
||||
|
||||
tailscale_oauth_ephemeral: false
|
||||
tailscale_oauth_preauthorized: true
|
||||
insecurely_log_authkey: false
|
||||
release_stability: stable
|
||||
tailscale_up_timeout: 120
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
version: 2.1.0
|
||||
- name: community.general
|
||||
version: 11.4.0
|
||||
- name: community.docker
|
||||
version: 4.8.1
|
||||
- name: artis3n.tailscale
|
||||
version: 1.1.0
|
||||
|
||||
roles:
|
||||
- src: geerlingguy.docker
|
||||
version: 7.8.0
|
||||
- src: geerlingguy.security
|
||||
version: 3.0.0
|
||||
- src: geerlingguy.ntp
|
||||
version: 3.0.0
|
||||
- src: borgbase.ansible_role_borgbackup
|
||||
version: v1.1.3
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart ssh
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
- name: Include optimization tasks
|
||||
include_tasks: optimization.yaml
|
||||
tags: optimization
|
||||
|
||||
- name: Install essential packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ system_packages.essential }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
tags: packages
|
||||
|
||||
- name: Set hostname and FQDN
|
||||
block:
|
||||
- name: Set hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ hostname | default(inventory_hostname) }}"
|
||||
|
||||
- name: Configure FQDN in hosts file
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
regexp: '^127\.0\.1\.1.*'
|
||||
line: "127.0.1.1 {{ fqdn | default(hostname) }} {{ hostname | default(inventory_hostname) }}"
|
||||
state: present
|
||||
tags: system
|
||||
|
||||
- name: Deploy MOTD template
|
||||
template:
|
||||
src: motd.j2
|
||||
dest: /etc/motd
|
||||
mode: '0644'
|
||||
|
||||
- name: Configure timezone
|
||||
community.general.timezone:
|
||||
name: "{{ timezone }}"
|
||||
tags: system, ntp
|
||||
|
||||
- name: Install and configure NTP
|
||||
include_role:
|
||||
name: geerlingguy.ntp
|
||||
tags: system, ntp
|
||||
|
||||
- name: Deploy SSH configuration
|
||||
ansible.builtin.template:
|
||||
src: sshd_config.j2
|
||||
dest: /etc/ssh/sshd_config
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
validate: '/usr/sbin/sshd -t -f %s'
|
||||
notify: Restart ssh
|
||||
|
||||
- name: Create admin users with proper SSH keys
|
||||
block:
|
||||
- name: Ensure user exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ item.name }}"
|
||||
shell: "{{ item.shell | default('/bin/bash') }}"
|
||||
groups: "{{ item.groups }}"
|
||||
append: true
|
||||
state: "{{ item.state | default('present') }}"
|
||||
create_home: true
|
||||
home: "/home/{{ item.name }}"
|
||||
loop: "{{ admin_users }}"
|
||||
tags: users
|
||||
|
||||
- name: Deploy SSH authorized keys
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ item.0.name }}"
|
||||
state: present
|
||||
key: "{{ item.1 }}"
|
||||
manage_dir: true
|
||||
with_subelements:
|
||||
- "{{ admin_users }}"
|
||||
- ssh_keys
|
||||
tags: users, ssh
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
- name: Configure sysctl parameters
|
||||
ansible.builtin.sysctl:
|
||||
name: "{{ item.key }}"
|
||||
value: "{{ item.value }}"
|
||||
state: present
|
||||
reload: true
|
||||
loop: "{{ sysctl_tuning | dict2items }}"
|
||||
tags: optimization
|
||||
|
||||
- name: Configure file handle limits
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/limits.conf
|
||||
regexp: "^{{ item.user | regex_escape }}.*{{ item.type }}"
|
||||
line: "{{ item.user }} - nofile {{ item.limit }}"
|
||||
create: true
|
||||
loop:
|
||||
- {user: "root", type: "soft", limit: "65536"}
|
||||
- {user: "root", type: "hard", limit: "65536"}
|
||||
- {user: "*", type: "soft", limit: "65536"}
|
||||
- {user: "*", type: "hard", limit: "65536"}
|
||||
tags: limits
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ ansible_hostname }}
|
||||
--------------------
|
||||
Welcome to {{ ansible_distribution }} {{ ansible_distribution_version }}
|
||||
Kernel: {{ ansible_kernel }}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Managed by Ansible - do not modify manually
|
||||
# Security hardened SSH configuration
|
||||
|
||||
Include /etc/ssh/sshd_config.d/*.conf
|
||||
|
||||
# Basic settings
|
||||
Port {{ security_ssh_port }}
|
||||
AddressFamily any
|
||||
ListenAddress 0.0.0.0
|
||||
ListenAddress ::
|
||||
Protocol 2
|
||||
|
||||
# Host keys (modern algorithms first)
|
||||
HostKey /etc/ssh/ssh_host_ed25519_key
|
||||
HostKey /etc/ssh/ssh_host_rsa_key
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_key
|
||||
|
||||
# Cryptography settings (modern ciphers)
|
||||
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
|
||||
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
|
||||
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
|
||||
|
||||
# Authentication security
|
||||
PermitRootLogin {{ ssh_config.permit_root_login }}
|
||||
MaxAuthTries {{ ssh_config.max_auth_tries }}
|
||||
MaxSessions {{ ssh_config.max_sessions }}
|
||||
ClientAliveInterval {{ ssh_config.client_alive_interval }}
|
||||
ClientAliveCountMax {{ ssh_config.client_alive_count_max }}
|
||||
LoginGraceTime 60
|
||||
|
||||
# General security settings
|
||||
UsePAM {{ ssh_config.use_pam }}
|
||||
X11Forwarding {{ ssh_config.x11_forwarding }}
|
||||
PrintMotd no
|
||||
Compression no
|
||||
UseDNS no
|
||||
IgnoreRhosts yes
|
||||
StrictModes yes
|
||||
PermitEmptyPasswords no
|
||||
TCPKeepAlive yes
|
||||
KbdInteractiveAuthentication no
|
||||
PrintLastLog yes
|
||||
|
||||
# Authorization settings
|
||||
AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
|
||||
|
||||
# Logging
|
||||
LogLevel INFO
|
||||
SyslogFacility AUTH
|
||||
|
||||
# User restrictions
|
||||
{% if ssh_config.allow_users is defined and ssh_config.allow_users %}
|
||||
AllowUsers {{ ssh_config.allow_users }}
|
||||
{% endif %}
|
||||
{% if ssh_config.allow_groups is defined and ssh_config.allow_groups %}
|
||||
AllowGroups {{ ssh_config.allow_groups }}
|
||||
{% endif %}
|
||||
|
||||
# Key-based auth enforcement
|
||||
PasswordAuthentication {{ ssh_config.password_authentication }}
|
||||
PermitEmptyPasswords no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
ChallengeResponseAuthentication {{ ssh_config.challenge_response_authentication }}
|
||||
|
||||
# Rekey limits
|
||||
RekeyLimit 512M 1h
|
||||
|
||||
# Allow client to pass locale environment variables
|
||||
AcceptEnv LANG LC_*
|
||||
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server
|
||||
|
||||
# Authorized key and principal controls
|
||||
AuthorizedPrincipalsFile none
|
||||
AuthorizedKeysCommand none
|
||||
AuthorizedKeysCommandUser nobody
|
||||
|
||||
# Disable forwarding and tunnels unless explicitly needed
|
||||
AllowAgentForwarding no
|
||||
AllowTcpForwarding no
|
||||
GatewayPorts no
|
||||
PermitTunnel no
|
||||
|
||||
# Disable user-controlled environments and TTY manipulations
|
||||
PermitUserEnvironment no
|
||||
PermitTTY yes
|
||||
X11UseLocalhost yes
|
||||
X11DisplayOffset 10
|
||||
|
||||
# Limit connection attempts
|
||||
MaxStartups 2:30:100
|
||||
|
||||
# Misc hardening
|
||||
IgnoreUserKnownHosts yes
|
||||
VersionAddendum none
|
||||
ChrootDirectory none
|
||||
|
||||
Match Address 10.0.0.0/8,192.168.0.0/16,172.16.0.0/12
|
||||
PermitRootLogin yes
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
coolify_state: present # latest | present | absent
|
||||
coolify_remove_data_on_absent: false
|
||||
coolify_base_dir: /data/coolify
|
||||
|
||||
coolify_docker_network: coolify
|
||||
coolify_http_port: 8000
|
||||
|
||||
coolify_owner: 9999
|
||||
coolify_group: root
|
||||
coolify_compose_files:
|
||||
- docker-compose.yml
|
||||
- docker-compose.prod.yml
|
||||
|
||||
coolify_container_names:
|
||||
- coolify
|
||||
- coolify-db
|
||||
- coolify-redis
|
||||
- coolify-realtime
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
- name: Check if Coolify is running before uninstallation
|
||||
community.docker.docker_container_info:
|
||||
name: "{{ item }}"
|
||||
loop: "{{ coolify_container_names }}"
|
||||
register: coolify_containers_pre_uninstall
|
||||
ignore_errors: true
|
||||
tags: coolify, deletion, pre-check
|
||||
|
||||
- name: Stop and remove Coolify services (compose down)
|
||||
become: true
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ coolify_base_dir }}/source"
|
||||
files: "{{ coolify_compose_files }}"
|
||||
state: absent
|
||||
remove_orphans: true
|
||||
remove_volumes: false
|
||||
ignore_errors: true
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Force remove any remaining Coolify containers
|
||||
community.docker.docker_container:
|
||||
name: "{{ item.item }}"
|
||||
state: absent
|
||||
force_kill: true
|
||||
force_remove: true
|
||||
loop: "{{ coolify_containers_pre_uninstall.results }}"
|
||||
when:
|
||||
- item.exists | default(false)
|
||||
- coolify_remove_data_on_absent | bool
|
||||
ignore_errors: true
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Remove Coolify files and data (conditional)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ coolify_base_dir }}/source"
|
||||
- "{{ coolify_base_dir }}/applications"
|
||||
- "{{ coolify_base_dir }}/databases"
|
||||
- "{{ coolify_base_dir }}/backups"
|
||||
- "{{ coolify_base_dir }}/services"
|
||||
- "{{ coolify_base_dir }}/proxy"
|
||||
- "{{ coolify_base_dir }}/webhooks-during-maintenance"
|
||||
- "{{ coolify_base_dir }}/ssh"
|
||||
- "{{ coolify_base_dir }}/sentinel"
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, files, deletion, data
|
||||
|
||||
- name: Remove base directory if empty (only when requested)
|
||||
ansible.builtin.file:
|
||||
path: "{{ coolify_base_dir }}"
|
||||
state: absent
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, files, deletion, data
|
||||
|
||||
- name: Remove Coolify docker volumes (find)
|
||||
community.docker.docker_volume_info:
|
||||
name: "^coolify_.*"
|
||||
register: coolify_volumes
|
||||
ignore_errors: true
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Remove Coolify docker volumes
|
||||
community.docker.docker_volume:
|
||||
name: "{{ item.Name }}"
|
||||
state: absent
|
||||
force: true
|
||||
loop: "{{ coolify_volumes.volumes | default([]) }}"
|
||||
when: coolify_volumes is defined and coolify_volumes.volumes | length > 0
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Remove Coolify docker network
|
||||
community.docker.docker_network:
|
||||
name: "{{ coolify_docker_network }}"
|
||||
state: absent
|
||||
force: "{{ coolify_remove_data_on_absent | bool }}"
|
||||
ignore_errors: true
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Prune Coolify images
|
||||
become: true
|
||||
community.docker.docker_prune:
|
||||
images: true
|
||||
images_filters:
|
||||
reference: "ghcr.io/coollabsio/*"
|
||||
build_cache: true
|
||||
ignore_errors: true
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Prune unused Docker resources
|
||||
become: true
|
||||
community.docker.docker_prune:
|
||||
containers: true
|
||||
images: false
|
||||
networks: true
|
||||
volumes: false
|
||||
builder_cache: true
|
||||
ignore_errors: true
|
||||
when: coolify_remove_data_on_absent | bool
|
||||
tags: coolify, docker, deletion
|
||||
|
||||
- name: Verify Coolify removal
|
||||
community.docker.docker_container_info:
|
||||
name: "{{ item }}"
|
||||
loop: "{{ coolify_container_names }}"
|
||||
register: coolify_containers_post_uninstall
|
||||
ignore_errors: true
|
||||
tags: coolify, deletion, verification
|
||||
|
||||
- name: Display uninstallation message
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Coolify has been uninstalled"
|
||||
- "Application data preserved: {{ not coolify_remove_data_on_absent }}"
|
||||
tags: coolify, deletion
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
- name: Install prerequisites (apt)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- curl
|
||||
- openssl
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
tags: coolify, prerequisites, installation
|
||||
|
||||
- name: Ensure Docker service is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
tags: coolify, docker, installation
|
||||
|
||||
- name: Create Coolify directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ coolify_owner }}"
|
||||
group: "{{ coolify_group }}"
|
||||
mode: '0750'
|
||||
loop:
|
||||
- "{{ coolify_base_dir }}/source"
|
||||
- "{{ coolify_base_dir }}/ssh/keys"
|
||||
- "{{ coolify_base_dir }}/ssh/mux"
|
||||
- "{{ coolify_base_dir }}/applications"
|
||||
- "{{ coolify_base_dir }}/databases"
|
||||
- "{{ coolify_base_dir }}/backups"
|
||||
- "{{ coolify_base_dir }}/services"
|
||||
- "{{ coolify_base_dir }}/proxy/dynamic"
|
||||
- "{{ coolify_base_dir }}/webhooks-during-maintenance"
|
||||
tags: coolify, files, installation
|
||||
|
||||
- name: Download Coolify configuration files
|
||||
ansible.builtin.get_url:
|
||||
url: "https://cdn.coollabs.io/coolify/{{ item.file }}"
|
||||
dest: "{{ coolify_base_dir }}/source/{{ item.dest }}"
|
||||
mode: '0644'
|
||||
loop:
|
||||
- { file: "docker-compose.yml", dest: "docker-compose.yml" }
|
||||
- { file: "docker-compose.prod.yml", dest: "docker-compose.prod.yml" }
|
||||
- { file: "upgrade.sh", dest: "upgrade.sh" }
|
||||
tags: coolify, files, installation
|
||||
|
||||
- name: Ensure .env exists from template (only when missing)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ coolify_base_dir }}/source/.env"
|
||||
register: env_file_check
|
||||
tags: coolify, files, installation
|
||||
|
||||
- name: Create .env from template when missing
|
||||
ansible.builtin.template:
|
||||
src: "templates/.env.production.j2"
|
||||
dest: "{{ coolify_base_dir }}/source/.env"
|
||||
mode: '0640'
|
||||
when: not env_file_check.stat.exists
|
||||
tags: coolify, files, installation
|
||||
|
||||
- name: Ensure correct ownership and permissions recursively
|
||||
ansible.builtin.file:
|
||||
path: "{{ coolify_base_dir }}"
|
||||
owner: "{{ coolify_owner }}"
|
||||
group: "{{ coolify_group }}"
|
||||
mode: '0750'
|
||||
recurse: true
|
||||
tags: coolify, permissions, installation
|
||||
|
||||
- name: Ensure Docker network exists
|
||||
become: true
|
||||
community.docker.docker_network:
|
||||
name: "{{ coolify_docker_network }}"
|
||||
driver: bridge
|
||||
attachable: true
|
||||
state: present
|
||||
tags: coolify, docker, installation
|
||||
|
||||
- name: Start Coolify services
|
||||
become: true
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ coolify_base_dir }}/source"
|
||||
files: "{{ coolify_compose_files }}"
|
||||
pull: always
|
||||
state: present
|
||||
wait: true
|
||||
wait_timeout: 300
|
||||
tags: coolify, docker, installation
|
||||
|
||||
- name: Wait for Coolify HTTP to respond
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ coolify_http_port }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
body_format: json
|
||||
register: coolify_health
|
||||
until: coolify_health.status == 200
|
||||
retries: 10
|
||||
delay: 10
|
||||
tags: coolify, health, installation
|
||||
|
||||
- name: Show installed message
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Coolify installed successfully"
|
||||
- "All containers are healthy and responding"
|
||||
- "Access at: http://{{ ansible_host }}:{{ coolify_http_port }}"
|
||||
tags: coolify, installation
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
- name: Validate coolify_state parameter
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- coolify_state in ['present', 'absent', 'latest']
|
||||
msg: "coolify_state must be one of: present, absent, latest"
|
||||
tags: always
|
||||
|
||||
- name: Check if Coolify is installed (compose file exists)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ coolify_base_dir }}/source/{{ coolify_compose_files[0] }}"
|
||||
register: coolify_installed
|
||||
tags: always
|
||||
|
||||
- name: Check Coolify container status
|
||||
community.docker.docker_container_info:
|
||||
name: "{{ item }}"
|
||||
loop: "{{ coolify_container_names }}"
|
||||
register: coolify_containers
|
||||
when: coolify_installed.stat.exists
|
||||
tags: always
|
||||
|
||||
- name: Set Coolify health fact
|
||||
ansible.builtin.set_fact:
|
||||
coolify_healthy: "{{ coolify_containers.results | selectattr('container', 'defined') | selectattr('container.State.Health.Status', 'defined') | selectattr('container.State.Health.Status', 'equalto', 'healthy') | list | length == coolify_container_names | length }}"
|
||||
when: coolify_containers is defined and coolify_containers.results is defined
|
||||
tags: always
|
||||
|
||||
- name: Include deletion tasks if state is absent
|
||||
ansible.builtin.include_tasks: delete.yaml
|
||||
when: coolify_state == 'absent'
|
||||
tags: coolify, deletion
|
||||
|
||||
- name: Include installation tasks when desired and not installed
|
||||
ansible.builtin.include_tasks: install.yaml
|
||||
when: (coolify_state in ['present','latest']) and not coolify_installed.stat.exists
|
||||
tags: coolify, installation
|
||||
|
||||
- name: Include update tasks when latest requested and already installed
|
||||
ansible.builtin.include_tasks: update.yaml
|
||||
when: (coolify_state == 'latest') and coolify_installed.stat.exists
|
||||
tags: coolify, update
|
||||
|
||||
- name: Show status when present and already installed
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Coolify is already installed and running"
|
||||
- "Containers healthy: {{ coolify_healthy | default('unknown') }}"
|
||||
- "Access at: http://{{ ansible_host }}:{{ coolify_http_port }}"
|
||||
when: (coolify_state == 'present') and coolify_installed.stat.exists | default(false)
|
||||
tags: coolify, status
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Update Coolify services with recreate
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ coolify_base_dir }}/source"
|
||||
files: "{{ coolify_compose_files }}"
|
||||
pull: always
|
||||
state: present
|
||||
recreate: always
|
||||
wait: true
|
||||
wait_timeout: 300
|
||||
tags: coolify, update
|
||||
|
||||
- name: Wait for Coolify HTTP to respond after update
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ coolify_http_port }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
body_format: json
|
||||
register: coolify_health_after_update
|
||||
until: coolify_health_after_update.status == 200
|
||||
retries: 10
|
||||
delay: 10
|
||||
tags: coolify, update, health
|
||||
|
||||
- name: Show update success message
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Coolify updated successfully to latest version"
|
||||
- "All containers are healthy and responding"
|
||||
- "Access at: http://{{ ansible_host }}:{{ coolify_http_port }}"
|
||||
tags: coolify, update
|
||||
@@ -0,0 +1,18 @@
|
||||
APP_ID={{ coolify_app_id | default(lookup('pipe','openssl rand -hex 16')) }}
|
||||
APP_NAME={{ coolify_app_name | default('Coolify') }}
|
||||
APP_KEY={{ coolify_app_key | default('base64:' ~ lookup('pipe','openssl rand -base64 32')) }}
|
||||
|
||||
DB_USERNAME={{ coolify_db_username | default('coolify') }}
|
||||
DB_PASSWORD={{ coolify_db_password | default(lookup('pipe','openssl rand -base64 32')) }}
|
||||
|
||||
REDIS_PASSWORD={{ coolify_redis_password | default(lookup('pipe','openssl rand -base64 32')) }}
|
||||
|
||||
PUSHER_APP_ID={{ coolify_pusher_app_id | default(lookup('pipe','openssl rand -hex 32')) }}
|
||||
PUSHER_APP_KEY={{ coolify_pusher_app_key | default(lookup('pipe','openssl rand -hex 32')) }}
|
||||
PUSHER_APP_SECRET={{ coolify_pusher_app_secret | default(lookup('pipe','openssl rand -hex 32')) }}
|
||||
|
||||
ROOT_USERNAME={{ coolify_root_username | default('') }}
|
||||
ROOT_USER_EMAIL={{ coolify_root_email | default('') }}
|
||||
ROOT_USER_PASSWORD={{ coolify_root_password | default('') }}
|
||||
|
||||
REGISTRY_URL={{ coolify_registry_url | default('ghcr.io') }}
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
dokploy_state: present # present | absent | latest
|
||||
dokploy_remove_data_on_absent: false
|
||||
dokploy_update_all_services: true
|
||||
dokploy_config_dir: /etc/dokploy
|
||||
dokploy_http_port: 3000
|
||||
dokploy_advertise_addr: ""
|
||||
|
||||
dokploy_postgres_image: postgres:16-alpine
|
||||
dokploy_redis_image: redis:8-alpine
|
||||
dokploy_dokploy_image: dokploy/dokploy:latest
|
||||
dokploy_traefik_image: traefik:v3.6.1
|
||||
|
||||
dokploy_postgres_user: dokploy
|
||||
dokploy_postgres_db: dokploy
|
||||
dokploy_postgres_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
|
||||
|
||||
dokploy_docker_network: dokploy-network
|
||||
|
||||
dokploy_constraint_node_role: manager
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
- name: Remove Dokploy services
|
||||
become: true
|
||||
community.docker.docker_swarm_service:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- dokploy-traefik
|
||||
- dokploy
|
||||
- dokploy-redis
|
||||
- dokploy-postgres
|
||||
ignore_errors: true
|
||||
tags: dokploy, deletion
|
||||
|
||||
- name: Leave Docker Swarm
|
||||
become: true
|
||||
community.docker.docker_swarm:
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
tags: dokploy, deletion
|
||||
|
||||
- name: Remove Dokploy network
|
||||
become: true
|
||||
community.docker.docker_network:
|
||||
name: "{{ dokploy_docker_network }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
tags: dokploy, deletion
|
||||
|
||||
- name: Remove Dokploy configuration directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ dokploy_config_dir }}"
|
||||
state: absent
|
||||
when: dokploy_remove_data_on_absent | bool
|
||||
tags: dokploy, deletion, files
|
||||
|
||||
- name: Remove Dokploy volumes
|
||||
become: true
|
||||
community.docker.docker_volume:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- dokploy-postgres-database
|
||||
- redis-data-volume
|
||||
- dokploy-docker-config
|
||||
when: dokploy_remove_data_on_absent | bool
|
||||
ignore_errors: true
|
||||
tags: dokploy, deletion, volumes
|
||||
|
||||
- name: Display uninstallation message
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Dokploy has been uninstalled"
|
||||
- "Application data preserved: {{ not dokploy_remove_data_on_absent }}"
|
||||
tags: dokploy, deletion
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
- name: Check if Docker is installed
|
||||
ansible.builtin.command:
|
||||
cmd: docker --version
|
||||
ignore_errors: false
|
||||
changed_when: false
|
||||
tags: dokploy, docker, installation
|
||||
|
||||
- name: Ensure Docker service is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
tags: dokploy, docker, installation
|
||||
|
||||
- name: Leave existing Docker Swarm if any
|
||||
become: true
|
||||
community.docker.docker_swarm:
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
tags: dokploy, swarm, installation
|
||||
|
||||
- name: Determine advertise address
|
||||
block:
|
||||
- name: Get private IP address
|
||||
ansible.builtin.set_fact:
|
||||
private_ip: "{{ ansible_default_ipv4.address }}"
|
||||
when: dokploy_advertise_addr == ""
|
||||
|
||||
- name: Set advertise address
|
||||
ansible.builtin.set_fact:
|
||||
effective_advertise_addr: "{{ dokploy_advertise_addr | default(private_ip) }}"
|
||||
tags: dokploy, network, installation
|
||||
|
||||
- name: Validate advertise address
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- effective_advertise_addr is defined
|
||||
- effective_advertise_addr != ""
|
||||
msg: "Could not determine advertise address. Please set dokploy_advertise_addr variable."
|
||||
tags: dokploy, network, installation
|
||||
|
||||
- name: Initialize Docker Swarm
|
||||
become: true
|
||||
community.docker.docker_swarm:
|
||||
state: present
|
||||
advertise_addr: "{{ effective_advertise_addr }}"
|
||||
listen_addr: "{{ effective_advertise_addr }}"
|
||||
tags: dokploy, swarm, installation
|
||||
|
||||
- name: Create dokploy overlay network
|
||||
become: true
|
||||
community.docker.docker_network:
|
||||
name: "{{ dokploy_docker_network }}"
|
||||
driver: overlay
|
||||
attachable: true
|
||||
state: present
|
||||
tags: dokploy, network, installation
|
||||
|
||||
- name: Create Dokploy configuration directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ dokploy_config_dir }}"
|
||||
state: directory
|
||||
mode: "0777"
|
||||
tags: dokploy, files, installation
|
||||
|
||||
- name: Pull all service images (when update all services requested)
|
||||
become: true
|
||||
community.docker.docker_image:
|
||||
name: "{{ item }}"
|
||||
source: pull
|
||||
loop:
|
||||
- "{{ dokploy_postgres_image }}"
|
||||
- "{{ dokploy_redis_image }}"
|
||||
- "{{ dokploy_dokploy_image }}"
|
||||
- "{{ dokploy_traefik_image }}"
|
||||
when: dokploy_update_all_services | bool
|
||||
tags: dokploy, images, installation
|
||||
|
||||
- name: Pull only Dokploy image (when latest requested and not updating all)
|
||||
become: true
|
||||
community.docker.docker_image:
|
||||
name: "{{ dokploy_dokploy_image }}"
|
||||
source: pull
|
||||
when:
|
||||
- not (dokploy_update_all_services | bool)
|
||||
- dokploy_state == 'latest'
|
||||
tags: dokploy, images, installation
|
||||
|
||||
- name: Deploy PostgreSQL service
|
||||
become: true
|
||||
community.docker.docker_swarm_service:
|
||||
name: dokploy-postgres
|
||||
image: "{{ dokploy_postgres_image }}"
|
||||
placement:
|
||||
constraints:
|
||||
- "node.role=={{ dokploy_constraint_node_role }}"
|
||||
networks:
|
||||
- name: "{{ dokploy_docker_network }}"
|
||||
aliases:
|
||||
- dokploy-postgres
|
||||
force_update: "{{ (dokploy_state == 'latest') | bool }}"
|
||||
update_config:
|
||||
parallelism: 1
|
||||
order: stop-first
|
||||
failure_action: rollback
|
||||
restart_config:
|
||||
condition: any
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
env:
|
||||
POSTGRES_USER: "{{ dokploy_postgres_user }}"
|
||||
POSTGRES_DB: "{{ dokploy_postgres_db }}"
|
||||
POSTGRES_PASSWORD: "{{ dokploy_postgres_password }}"
|
||||
mounts:
|
||||
- type: volume
|
||||
source: dokploy-postgres-database
|
||||
target: /var/lib/postgresql/data
|
||||
state: present
|
||||
tags: dokploy, database, installation
|
||||
|
||||
- name: Deploy Redis service
|
||||
become: true
|
||||
community.docker.docker_swarm_service:
|
||||
name: dokploy-redis
|
||||
image: "{{ dokploy_redis_image }}"
|
||||
placement:
|
||||
constraints:
|
||||
- "node.role=={{ dokploy_constraint_node_role }}"
|
||||
networks:
|
||||
- name: "{{ dokploy_docker_network }}"
|
||||
aliases:
|
||||
- dokploy-redis
|
||||
force_update: "{{ (dokploy_state == 'latest') | bool }}"
|
||||
update_config:
|
||||
parallelism: 1
|
||||
order: stop-first
|
||||
failure_action: rollback
|
||||
restart_config:
|
||||
condition: any
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
mounts:
|
||||
- type: volume
|
||||
source: redis-data-volume
|
||||
target: /data
|
||||
state: present
|
||||
tags: dokploy, redis, installation
|
||||
|
||||
- name: Deploy Dokploy main service
|
||||
become: true
|
||||
community.docker.docker_swarm_service:
|
||||
name: dokploy
|
||||
image: "{{ dokploy_dokploy_image }}"
|
||||
replicas: 1
|
||||
networks:
|
||||
- name: "{{ dokploy_docker_network }}"
|
||||
aliases:
|
||||
- dokploy
|
||||
force_update: "{{ (dokploy_state == 'latest') | bool }}"
|
||||
update_config:
|
||||
parallelism: 1
|
||||
order: stop-first
|
||||
failure_action: rollback
|
||||
restart_config:
|
||||
condition: any
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
mounts:
|
||||
- type: bind
|
||||
source: /var/run/docker.sock
|
||||
target: /var/run/docker.sock
|
||||
- type: bind
|
||||
source: "{{ dokploy_config_dir }}"
|
||||
target: /etc/dokploy
|
||||
- type: volume
|
||||
source: dokploy-docker-config
|
||||
target: /root/.docker
|
||||
publish:
|
||||
- published_port: "{{ dokploy_http_port }}"
|
||||
target_port: 3000
|
||||
protocol: tcp
|
||||
mode: host
|
||||
placement:
|
||||
constraints:
|
||||
- "node.role=={{ dokploy_constraint_node_role }}"
|
||||
env:
|
||||
ADVERTISE_ADDR: "{{ effective_advertise_addr }}"
|
||||
DATABASE_URL: "postgres://{{ dokploy_postgres_user }}:{{ dokploy_postgres_password }}@dokploy-postgres:5432/{{ dokploy_postgres_db }}"
|
||||
state: present
|
||||
tags: dokploy, main, installation
|
||||
|
||||
- name: Wait for Dokploy to start
|
||||
ansible.builtin.pause:
|
||||
seconds: 4
|
||||
tags: dokploy, wait, installation
|
||||
|
||||
- name: Create Traefik configuration directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ dokploy_config_dir }}/traefik"
|
||||
- "{{ dokploy_config_dir }}/traefik/dynamic"
|
||||
tags: dokploy, traefik, installation
|
||||
|
||||
- name: Deploy Traefik service
|
||||
become: true
|
||||
community.docker.docker_swarm_service:
|
||||
name: dokploy-traefik
|
||||
image: "{{ dokploy_traefik_image }}"
|
||||
placement:
|
||||
constraints:
|
||||
- "node.role=={{ dokploy_constraint_node_role }}"
|
||||
networks:
|
||||
- name: "{{ dokploy_docker_network }}"
|
||||
aliases:
|
||||
- traefik
|
||||
update_config:
|
||||
parallelism: 1
|
||||
order: start-first
|
||||
restart_config:
|
||||
condition: any
|
||||
mounts:
|
||||
- type: bind
|
||||
source: "{{ dokploy_config_dir }}/traefik/traefik.yml"
|
||||
target: /etc/traefik/traefik.yml
|
||||
- type: bind
|
||||
source: "{{ dokploy_config_dir }}/traefik/dynamic"
|
||||
target: /etc/dokploy/traefik/dynamic
|
||||
- type: bind
|
||||
source: /var/run/docker.sock
|
||||
target: /var/run/docker.sock
|
||||
publish:
|
||||
- published_port: 443
|
||||
target_port: 443
|
||||
protocol: tcp
|
||||
mode: host
|
||||
- published_port: 80
|
||||
target_port: 80
|
||||
protocol: tcp
|
||||
mode: host
|
||||
- published_port: 443
|
||||
target_port: 443
|
||||
protocol: udp
|
||||
mode: host
|
||||
state: present
|
||||
force_update: "{{ (dokploy_state == 'latest') | bool }}"
|
||||
tags: dokploy, traefik, installation
|
||||
|
||||
- name: Wait for Dokploy HTTP to respond
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ dokploy_http_port }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
timeout: 30
|
||||
register: dokploy_health
|
||||
until: dokploy_health.status == 200
|
||||
retries: 10
|
||||
delay: 10
|
||||
tags: dokploy, health, installation
|
||||
|
||||
- name: Show installation message
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Dokploy installed successfully"
|
||||
- "Using advertise address: {{ effective_advertise_addr }}"
|
||||
- "Access at: http://{{ ansible_host }}:{{ dokploy_http_port }}"
|
||||
tags: dokploy, installation
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
- name: Validate dokploy_state parameter
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- dokploy_state in ['present', 'absent', 'latest']
|
||||
msg: "dokploy_state must be one of: present, absent, latest"
|
||||
tags: always
|
||||
|
||||
- name: Check if Dokploy services exist
|
||||
become: true
|
||||
community.docker.docker_swarm_service_info:
|
||||
name: dokploy
|
||||
register: dokploy_services
|
||||
ignore_errors: true
|
||||
tags: always
|
||||
|
||||
- name: Include deletion tasks if state is absent
|
||||
ansible.builtin.include_tasks: delete.yaml
|
||||
when: dokploy_state == 'absent'
|
||||
tags: dokploy, deletion
|
||||
|
||||
- name: Include installation/update tasks when desired
|
||||
ansible.builtin.include_tasks: install.yaml
|
||||
when: dokploy_state in ['present', 'latest']
|
||||
tags: dokploy, installation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart prometheus-node-exporter
|
||||
ansible.builtin.service:
|
||||
name: prometheus-node-exporter
|
||||
state: restarted
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
- name: Install monitoring tools
|
||||
ansible.builtin.apt:
|
||||
name: "{{ system_packages.monitoring }}"
|
||||
state: present
|
||||
tags: monitoring
|
||||
|
||||
- name: Manage Prometheus node exporter
|
||||
block:
|
||||
- name: Install Prometheus node exporter
|
||||
ansible.builtin.apt:
|
||||
name: prometheus-node-exporter
|
||||
state: present
|
||||
notify: Restart prometheus-node-exporter
|
||||
|
||||
- name: Ensure Prometheus node exporter is running and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: prometheus-node-exporter
|
||||
state: started
|
||||
enabled: true
|
||||
when: monitoring_prometheus_node_exporter | bool
|
||||
tags: monitoring, prometheus
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Reload nftables
|
||||
ansible.builtin.systemd:
|
||||
name: nftables
|
||||
state: reloaded
|
||||
tags: security, nftables
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
- name: Install security packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ system_packages.security }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
tags: security
|
||||
|
||||
- name: Install nftables
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- nftables
|
||||
state: present
|
||||
update_cache: true
|
||||
tags: security, nftables
|
||||
|
||||
- name: Render nftables configuration
|
||||
ansible.builtin.template:
|
||||
src: nftables.conf.j2
|
||||
dest: /etc/nftables.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
validate: 'nft -c -f %s'
|
||||
notify: Reload nftables
|
||||
tags: security, nftables
|
||||
|
||||
- name: Enable and start nftables
|
||||
ansible.builtin.systemd:
|
||||
name: nftables
|
||||
state: started
|
||||
enabled: true
|
||||
tags: security, nftables
|
||||
|
||||
- name: Install and configure fail2ban
|
||||
include_role:
|
||||
name: geerlingguy.security
|
||||
tags: security
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/sbin/nft -f
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0;
|
||||
policy {{ security_firewall_default_policy | default('drop') }};
|
||||
|
||||
ct state established,related accept
|
||||
iifname lo accept
|
||||
|
||||
# allow ICMP
|
||||
ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded } accept
|
||||
ip6 nexthdr icmpv6 icmpv6 type { echo-request, echo-reply, destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept
|
||||
|
||||
{% for p in security_firewall_allowed_ports %}
|
||||
{% set parts = p.split('/') %}
|
||||
{% set port = parts[0] %}
|
||||
{% set proto = parts[1] if parts|length > 1 else 'tcp' %}
|
||||
{{ proto }} dport {{ port }} accept
|
||||
{% endfor %}
|
||||
|
||||
reject with icmpx type port-unreachable
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority 0;
|
||||
policy accept;
|
||||
}
|
||||
|
||||
chain output {
|
||||
type filter hook output priority 0;
|
||||
policy accept;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: Import base system setup
|
||||
ansible.builtin.import_playbook: base_setup.yaml
|
||||
|
||||
- name: Import Docker setup
|
||||
ansible.builtin.import_playbook: docker.yaml
|
||||
|
||||
- name: Import application deployment
|
||||
ansible.builtin.import_playbook: apps.yaml
|
||||
|
||||
- name: Import post-deployment validation
|
||||
ansible.builtin.import_playbook: validation.yaml
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
- name: Post-deployment validation and health checks
|
||||
hosts: servers
|
||||
gather_facts: true
|
||||
become: false
|
||||
serial: "100%"
|
||||
|
||||
tasks:
|
||||
- name: Verify SSH connectivity on custom port
|
||||
ansible.builtin.wait_for:
|
||||
port: "{{ security_ssh_port | default(22) }}"
|
||||
host: "{{ ansible_host | default(inventory_hostname) }}"
|
||||
timeout: 60
|
||||
delay: 5
|
||||
state: started
|
||||
tags: validation, networking
|
||||
|
||||
- name: Check critical system services
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
enabled: true
|
||||
loop:
|
||||
- ssh
|
||||
- docker
|
||||
- fail2ban
|
||||
- nftables
|
||||
tags: validation
|
||||
ignore_errors: true
|
||||
|
||||
- name: Run comprehensive system health checks
|
||||
block:
|
||||
- name: Set root mount fact
|
||||
ansible.builtin.set_fact:
|
||||
root_mount: "{{ ansible_mounts | selectattr('mount', 'equalto', '/') | list | first }}"
|
||||
tags: validation, health
|
||||
|
||||
- name: Check load average
|
||||
ansible.builtin.shell: cat /proc/loadavg | awk '{print $1}'
|
||||
register: load_avg
|
||||
changed_when: false
|
||||
|
||||
- name: Check Docker status
|
||||
ansible.builtin.shell:
|
||||
cmd: docker info >/dev/null 2>&1 && echo "healthy" || echo "unhealthy"
|
||||
register: docker_status
|
||||
changed_when: false
|
||||
ignore_errors: true
|
||||
|
||||
- name: Display comprehensive health status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Health check results for {{ inventory_hostname }}:"
|
||||
- "Disk usage: {{ ((root_mount.size_total - root_mount.size_available) / root_mount.size_total * 100) | round(2) }}%"
|
||||
- "Memory usage: {{ ((ansible_memtotal_mb - ansible_memfree_mb) / ansible_memtotal_mb * 100) | round(2) }}%"
|
||||
- "Load average (1m): {{ load_avg.stdout }}"
|
||||
- "Docker: {{ docker_status.stdout }}"
|
||||
tags: always, health
|
||||
|
||||
tags: validation, health
|
||||
Reference in New Issue
Block a user