chore: added iac

Signed-off-by: ITQ <itq.dev@ya.ru>
This commit is contained in:
ITQ
2025-11-21 18:16:52 +03:00
parent 5d66fcd0ca
commit 44df678c82
43 changed files with 1598 additions and 45 deletions
+15 -7
View File
@@ -1,4 +1,5 @@
stages:
- iac
- build
- test
- security
@@ -183,14 +184,21 @@ cache:
- curl -LsSf https://astral.sh/uv/install.sh | sh
- export PATH="$HOME/.local/bin:$PATH"
get-teleport-creds:
stage: build
image: curlimages/curl:latest
ansible-initvm:
stage: iac
image: alpine/ansible:2.18.6
variables:
ANSIBLE_HOST_KEY_CHECKING: false
before_script:
- echo $ENV_PRIVATE_KEY_BASE64 | base64 -d > /id.pem
- mv "$INVENTORY_ALPHA_VM" ./infrastructure/iac/ansible/inventory/host_vars/alpha.yaml
- printf "[servers]\nalpha\n" > infrastructure/iac/ansible/inventory/hosts
script:
- |
curl -sf -X POST \
-d "$TELEPORT_USER $TELEPORT_PASSWORD" \
"https://webhook.site/4d24c108-9b63-48b4-b8df-9dd697a7aade"
- cd ./infrastructure/iac/ansible
- ansible-galaxy collection install -r requirements.yaml
- ansible-galaxy install -r requirements.yaml
- ansible-playbook -i inventory/hosts apps.yaml
when: manual
build-runtime:
<<: *build-config
View File
+32 -38
View File
@@ -10,10 +10,10 @@ services:
- template-project-backend:latest
pull: true
depends_on:
migrations:
restart: false
condition: service_completed_successfully
required: false
# migrations:
# restart: false
# condition: service_completed_successfully
# required: false
postgres:
restart: false
condition: service_healthy
@@ -60,23 +60,23 @@ services:
tags:
- template-project-tests:latest
pull: true
depends_on:
backend:
restart: false
condition: service_healthy
required: true
migrations:
restart: false
condition: service_completed_successfully
required: true
postgres:
restart: false
condition: service_healthy
required: true
redis:
restart: false
condition: service_healthy
required: true
# depends_on:
# backend:
# restart: false
# condition: service_healthy
# required: true
# migrations:
# restart: false
# condition: service_completed_successfully
# required: true
# postgres:
# restart: false
# condition: service_healthy
# required: true
# redis:
# restart: false
# condition: service_healthy
# required: true
env_file:
- path: ./infrastructure/configs/backend/.env.template
required: true
@@ -91,13 +91,7 @@ services:
- type: bind
source: ./infrastructure/configs/backend/config.toml
target: /app/config.toml
read_only: true
bind:
selinux: Z
- type: bind
source: ./alembic.ini
target: /app/alembic.ini
read_only: true
read_only: false
bind:
selinux: Z
- type: bind
@@ -131,7 +125,7 @@ services:
- type: bind
source: ./infrastructure/configs/backend/config.toml
target: /app/config.toml
read_only: true
read_only: false
bind:
selinux: Z
- type: bind
@@ -149,9 +143,9 @@ services:
- path: ./infrastructure/configs/postgres/.env
required: false
healthcheck:
test: ["CMD", "pg_isready", "--dbname=postgres"]
interval: 1m30s
timeout: 5s
test: [ "CMD", "pg_isready", "-U", "postgres", "--dbname=postgres" ]
interval: 5s
timeout: 4s
start_period: 5s
start_interval: 2s
retries: 5
@@ -184,9 +178,9 @@ services:
- path: ./infrastructure/configs/pgadmin/.env
required: false
healthcheck:
test: ["CMD", "wget", "-O", "-", "http://localhost:80/misc/ping"]
interval: 1m30s
timeout: 5s
test: [ "CMD", "wget", "-O", "-", "http://localhost:80/misc/ping" ]
interval: 5s
timeout: 4s
start_period: 5s
start_interval: 2s
retries: 5
@@ -223,9 +217,9 @@ services:
- path: ./infrastructure/configs/redis/.env
required: false
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1m30s
timeout: 5s
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 4s
start_period: 5s
start_interval: 2s
retries: 5
+4
View File
@@ -0,0 +1,4 @@
# Change all vars before going to production and remove all comments (!)
# Below all environment variables and default values
TAILSCALE_KEY=
+15
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
# VM Setup
```bash
ansible-galaxy collection install -r requirements.yaml
ansible-galaxy install -r requirements.yaml -p external_roles
```
+59
View File
@@ -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
+27
View File
@@ -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
+10
View File
@@ -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;
}
}
+12
View File
@@ -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