first commit

This commit is contained in:
2026-06-25 22:45:18 -04:00
commit 6c89f12fad
25 changed files with 1397 additions and 0 deletions

13
homelab-iac/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
.sops/
*.age
*.key
*.pem
*.sql
*.tar.gz
acme.json
secrets.sops.yml.dec
exports/
__pycache__/
*.pyc
.vscode/
.idea/

5
homelab-iac/.sops.yaml Normal file
View File

@@ -0,0 +1,5 @@
creation_rules:
- path_regex: "inventory/group_vars/.*\\.sops\\.yml$"
age: "age1g759cwnlt6nq5u87jcs6m76fz3spw5awcwrchuck8ythxpf4g9rqarhy9y"
- path_regex: "files/.*\\.sops\\.*$"
age: "age1g759cwnlt6nq5u87jcs6m76fz3spw5awcwrchuck8ythxpf4g9rqarhy9y"

49
homelab-iac/Makefile Normal file
View File

@@ -0,0 +1,49 @@
SOPS_KEY ?= .sops/age-key.txt
export SOPS_AGE_KEY_FILE=$(SOPS_KEY)
.PHONY: help secrets edit-secrets decrypt-secrets bootstrap swarm dokploy supporting backup dry-run lint
help:
@echo "Available targets:"
@echo " init - Install Ansible collections"
@echo " edit-secrets - Edit encrypted secrets"
@echo " decrypt-secrets - View encrypted secrets"
@echo " dry-run - Syntax-check all playbooks"
@echo " bootstrap - Bootstrap all nodes (Docker, sysctl, NFS)"
@echo " swarm - Initialize/join Swarm and apply labels"
@echo " dokploy - Deploy Dokploy control plane"
@echo " supporting - Deploy registry, gitea, DNS"
@echo " backup - Backup Dokploy state and volumes"
@echo " all - Run bootstrap + swarm + dokploy + supporting"
init:
ansible-galaxy collection install -r requirements.yml
edit-secrets:
sops inventory/group_vars/all/secrets.sops.yml
decrypt-secrets:
sops --decrypt inventory/group_vars/all/secrets.sops.yml
dry-run:
ansible-playbook -i inventory/production.yml --syntax-check playbooks/*.yml
export:
ansible-playbook -i inventory/production.yml playbooks/91_export_dokploy.yml
bootstrap:
ansible-playbook -i inventory/production.yml playbooks/00_bootstrap_nodes.yml
swarm:
ansible-playbook -i inventory/production.yml playbooks/10_swarm_init.yml
dokploy:
ansible-playbook -i inventory/production.yml playbooks/20_dokploy.yml
supporting:
ansible-playbook -i inventory/production.yml playbooks/30_supporting.yml
backup:
ansible-playbook -i inventory/production.yml playbooks/90_backup.yml
all: bootstrap swarm dokploy supporting

57
homelab-iac/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Homelab IaC
Ansible-based infrastructure-as-code for a mixed-arch (ARM/x86) Docker Swarm managed by Dokploy.
## Quick start
1. Install prerequisites on your control machine:
```bash
brew install ansible sops age
```
2. Generate an Age key for SOPS:
```bash
./scripts/init-age.sh
```
Update `.sops.yaml` with the public key printed by the script.
3. Edit encrypted secrets:
```bash
sops inventory/group_vars/all/secrets.sops.yml
```
Fill in the values captured from the live cluster.
4. Bootstrap nodes and deploy Dokploy:
```bash
ansible-playbook -i inventory/production.yml playbooks/00_bootstrap_nodes.yml
ansible-playbook -i inventory/production.yml playbooks/10_swarm_init.yml
ansible-playbook -i inventory/production.yml playbooks/20_dokploy.yml
```
## Capturing live secrets
On the manager (`tpi-n1`), run:
```bash
# Docker secrets (values are base64 in inspect output)
docker secret inspect dokploy_postgres_password --format='{{range .}}{{.Spec.Data}}{{end}}' | base64 -d
docker secret inspect dokploy-auth-secret --format='{{range .}}{{.Spec.Data}}{{end}}' | base64 -d
# Registry HTTP secret
grep REGISTRY_HTTP_SECRET /etc/dokploy/compose/gitea-registry-vdftrt/code/.env
# Technitium admin password
grep DNS_SERVER_ADMIN_PASSWORD /etc/dokploy/compose/dns-technitum-6ojgo2/code/.env
```
Place these into `inventory/group_vars/all/secrets.sops.yml`.
## Maintenance tasks
See [docs/maintenance.md](docs/maintenance.md).
## Runbooks
- [Restore Dokploy from backup](docs/runbook-restore-dokploy.md)
- [Add a new Swarm node](docs/runbook-add-node.md)
- [Rotate secrets](docs/runbook-rotate-secrets.md)

View File

@@ -0,0 +1,58 @@
# Maintenance tasks
These are operational tasks that are NOT fully automated yet and need your attention on a schedule.
## Immediate / urgent
- [ ] **tpi-n2 disk space**: `/dev/mmcblk0p2` is 100% full (27G / 29G). Clean up Docker build cache, old images, logs, and unused volumes before it causes service failures.
```bash
ssh ubuntu@192.168.2.19
docker system prune -a --volumes
journalctl --vacuum-time=7d
du -sh /var/lib/docker/* /var/log/*
```
- [ ] **Capture live secrets** and place them into `inventory/group_vars/all/secrets.sops.yml`:
```bash
ssh ubuntu@192.168.2.130
docker secret inspect dokploy_postgres_password --format='{{range .}}{{.Spec.Data}}{{end}}' | base64 -d
docker secret inspect dokploy-auth-secret --format='{{range .}}{{.Spec.Data}}{{end}}' | base64 -d
grep REGISTRY_HTTP_SECRET /etc/dokploy/compose/gitea-registry-vdftrt/code/.env
grep DNS_SERVER_ADMIN_PASSWORD /etc/dokploy/compose/dns-technitum-6ojgo2/code/.env
cat /etc/dokploy/compose/gitea-registry-vdftrt/files/auth/registry.password
```
- [ ] **Generate Age key** and configure SOPS:
```bash
./scripts/init-age.sh
# edit .sops.yaml with the public key
sops inventory/group_vars/all/secrets.sops.yml
```
- [ ] **Take first encrypted backups** of Dokploy DB, `/etc/dokploy`, and `acme.json`.
## Weekly
- [ ] Review disk usage on all nodes (`df -h`, `docker system df -v`).
- [ ] Verify Dokploy DB backups completed and are restorable.
- [ ] Check `acme.json` is backed up (TLS cert loss means rate-limit pain).
- [ ] Review failed / 0-replica services (`docker service ls`).
## Monthly
- [ ] Update Docker CE to latest patch release on a test node first.
- [ ] Review Dokploy release notes and plan upgrades (`dokploy_version`).
- [ ] Rotate any manually-set passwords stored outside SOPS.
- [ ] Test restore procedure from backup on a spare VM or cloud instance.
- [ ] Prune old container images, build caches, and Dokploy logs.
## Quarterly
- [ ] Rotate Ansible Vault / Age key and re-encrypt secrets.
- [ ] Validate IaC still reproduces the cluster from scratch on a test environment.
- [ ] Audit user access in Dokploy and Gitea.
- [ ] Review offsite backup integrity.
## Before any disaster
- [ ] Ensure `secrets.sops.yml`, the Age private key, and the latest `.tar.gz.age` backup are stored in at least two offline locations.

View File

@@ -0,0 +1,22 @@
# Runbook: Add a new Swarm node
1. Add the node to `inventory/production.yml` under the correct group (`swarm_managers` or `swarm_workers`).
2. Run bootstrap:
```bash
ansible-playbook -i inventory/production.yml playbooks/00_bootstrap_nodes.yml --limit newnode
```
3. Run Swarm join:
```bash
ansible-playbook -i inventory/production.yml playbooks/10_swarm_init.yml --limit newnode
```
4. Verify labels and placement:
```bash
ssh ubuntu@192.168.2.130
docker node ls
docker node inspect newnode --pretty
```
5. If the node is a manager, ensure an odd number of managers (1, 3, 5).

View File

@@ -0,0 +1,63 @@
# Runbook: Restore Dokploy from backup
## Scenario
The Dokploy control plane (Postgres, Redis, Traefik) has been lost but Swarm is intact or rebuilt.
## Prerequisites
- Age private key available.
- Latest backup bundle: `*.tar.gz.age`.
- Ansible inventory is correct.
## Steps
1. Deploy the base Swarm and Dokploy stack WITHOUT starting the dokploy service:
```bash
ansible-playbook -i inventory/production.yml playbooks/00_bootstrap_nodes.yml
ansible-playbook -i inventory/production.yml playbooks/10_swarm_init.yml
ansible-playbook -i inventory/production.yml playbooks/20_dokploy.yml
```
2. Stop the Dokploy app and Postgres services:
```bash
ssh ubuntu@192.168.2.130
docker service scale dokploy_dokploy=0 dokploy_dokploy-postgres=0
```
3. Restore the Postgres volume:
```bash
docker run --rm -v dokploy-postgres-database:/target -v /path/to/restore:/backup alpine \
sh -c "rm -rf /target/* && tar xzf /backup/vol-dokploy-postgres-database.tar.gz -C /target"
```
4. Restore `/etc/dokploy`:
```bash
rm -rf /etc/dokploy
tar xzf /path/to/restore/etc-dokploy.tar.gz -C /
```
5. Restore `acme.json`:
```bash
cp /path/to/restore/acme.json /etc/dokploy/traefik/dynamic/acme.json
chmod 600 /etc/dokploy/traefik/dynamic/acme.json
```
6. Scale services back up:
```bash
docker service scale dokploy_dokploy-postgres=1 dokploy_dokploy=1
```
7. Verify:
```bash
docker service ls
docker logs -f dokploy_dokploy.1.<taskid>
```
## If Swarm is completely gone
- Rebuild nodes with `00_bootstrap_nodes.yml`.
- Join as a new Swarm.
- Re-create Docker secrets with the same values from `secrets.sops.yml` before running `20_dokploy.yml`.
- Restore volumes and `/etc/dokploy` as above.
- Re-deploy application stacks through Dokploy UI or API once Dokploy is running.

View File

@@ -0,0 +1,51 @@
---
# Unencrypted group variables.
# Place sensitive values in inventory/group_vars/all/secrets.sops.yml
# OS packages
common_packages:
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-pip
- python3-docker
- jq
- nvme-cli
- nfs-common
- age
# Docker CE version pinning (empty = latest available)
docker_version: ""
docker_compose_plugin: true
# Swarm
docker_swarm_init: true
# Dokploy deployment directory on the first manager
dokploy_etc_dir: /etc/dokploy
dokploy_compose_dir: /opt/dokploy
# Registry settings
registry_image: registry:2
registry_http_secret: "{{ vault_registry_http_secret }}"
registry_htpasswd_path: files/registry/htpasswd
# NFS shared storage
nfs_subdir_name: shared
minio_data_dir: /mnt/swarm-data/minio
# Backup settings
backup_retention_days: 30
backup_base_dir: /mnt/swarm-data/backups/homelab
# MinIO S3 backup target (on Synology NAS)
backup_s3_enabled: true
backup_s3_endpoint: "http://192.168.2.18:9000"
backup_s3_bucket: "dokploy-backups"
backup_s3_region: "us-east-1"
backup_s3_path: "homelab"
backup_s3_storage_class: ""
# Monitoring / alerting placeholders
alert_email: ""

View File

@@ -0,0 +1,32 @@
#ENC[AES256_GCM,data:qAF9E1uTKEk9olKbTZmdyi/Sdlwa7AU+82M=,iv:2Z1D5EeueXR+2GAePPMNzRr9Tt3Db9PkFRlBDYF3Pew=,tag:pF7CxjMFjs0a2sCBVJq9xg==,type:comment]
#ENC[AES256_GCM,data:5Xk//VJYvefv0mZcLbDW1O46Ha7rG4S1ts3G3gqA0T1aMDku898FzElNzVDo6xIjAU02CMilr1NTMQ==,iv:0w9+c/ZjeVCXoZqCER97CpU7RpP5cEQrrN/1Qj0Kz5M=,tag:VPWTZFT2lFRCz5bbRp70zg==,type:comment]
vault_dokploy_postgres_password: ENC[AES256_GCM,data:UoDCPLC/NXNLc08VxurgKmNEopiT2cOAin5k7bEWWOg=,iv:m9Omki8b123DCZXywIfLdocI98xW6nh2JwrorXWDd24=,tag:3oJDQV7sO74RielR7YlIyA==,type:str]
vault_dokploy_auth_secret: ENC[AES256_GCM,data:+D1yrIxtRNMRdNNft0ieTPBKMvSjBqU/Ygm+BjfBtztfjWQJUgV3ENTrKiUyR+o/ifPilKQzZXqBhyHXtAUNKg==,iv:/qJA4o4ZrpUDilXw21X5OjHFjdBxyvCkKRXuc1PUaOQ=,tag:gcOFk4j9FmEpqttXRALOqw==,type:str]
vault_registry_http_secret: ENC[AES256_GCM,data:BXoG+/k6InJKImK3KEADb2ppU/jM4q7UC4sLzhjj,iv:kFNBGZnwyTPB0nWW0EZSiH/xO9UP5qccCs61xeq8010=,tag:AwFteLIuNMg2dX6WXJcK2A==,type:str]
vault_technitium_admin_password: ENC[AES256_GCM,data:rbaJvjZuxW5SoujvPmh07g==,iv:DTjTF2/pzlkaf7UAg9S31oLU6AQQ+0M3q9yK+TcIAHA=,tag:6uQUO1bt2zj4rTCroFQgBg==,type:str]
vault_registry_htpasswd: ENC[AES256_GCM,data:RVFpZDDwEHxirJu+81GTkYAOXHxHd0Xj0RUvB+d+vcrXjntLAe/0b/Pcv2cFogB8q5X0nD8SGyQnokX7sPvU8Ig+GQ==,iv:HTS6nA1054KZ0BMGPzp1aoIzjZaxvFypU6lDvCU1yKs=,tag:XAsD7Qo0zZpD1nJ6Dep+8Q==,type:str]
#ENC[AES256_GCM,data:+bAUnUUaSmOGlAW7OphyUkHp6rpHaWp/4CTCdjPz/lYK1nAvgNk=,iv:TSVBbanbgB94hqa8e9mKvF+r/3V3NXyFeFx5afoXWP4=,tag:U+rsgcOS6tB60LY9VfKUAA==,type:comment]
#ENC[AES256_GCM,data:1AcztTHc96l/Ok9yPpHFqA1CRkl8lLllc5/ePhIP8g==,iv:8J+8kkWYWyfDKzGA0dnL2l7sKHZ5TMeZ+vuF6UaZQ8s=,tag:uitNuP/Fv1Qza+UUi0I4XA==,type:comment]
#ENC[AES256_GCM,data:yLiFcWg316L+HgNxDR6QMaOIcdGz8T0DS/XGHx0dvQ==,iv:ts2vr6giSBQcFB3QSKG9+oQDZa3Qb4uro+JIxmMFgwk=,tag:lx+ABAfuBKXJ/wSiuCRgvw==,type:comment]
#ENC[AES256_GCM,data:PWm0slUV/mmt0l10WEie1sF3hpe86G3f5puobqs=,iv:Uhuj0i08uO+LbCB1nxFTaDVAwe60d91iF8qN1MdY9GI=,tag:5mFfJ46anoS5OLCtLHQOfw==,type:comment]
#ENC[AES256_GCM,data:IOECwipNIQmzD5Z8qRrIX07IkjuXjiUU11Hw,iv:BslYy41gJdiAiBtio/oe/Yk++yV4LN4ZtqnPXK/w3+Y=,tag:1MHjDhbia8myT4bL1zJGVw==,type:comment]
vault_backup_s3_access_key: ENC[AES256_GCM,data:WTmPqGHFmLe3y17aB4c=,iv:qN0d0yhhxjucNgD2bT4HChsScZ5o5rdoayhnP4jGHZQ=,tag:x3xu6Z34EK/2qSHlFWnVBg==,type:str]
vault_backup_s3_secret_key: ENC[AES256_GCM,data:rYdFbL7cEtoDcJLUogcunqPM++yVycNZXtNbfT2YmBU217/MOya5N1nbEy8=,iv:PNDkoI5I19uBKuIG8zGylrwm3pOgR7qAWotkUsP60Rc=,tag:hUO2QFHI79sJM8Way/vMag==,type:str]
vault_minio_root_user: ENC[AES256_GCM,data:2P9+k9Q=,iv:KHh3+vYf2E6ArZMI7al9lRLeiyG+EZ4ApprBfIwhbxA=,tag:TQqOjYU4QwnoQoYv6oRgFg==,type:str]
vault_minio_root_password: ENC[AES256_GCM,data:iz1M/3sHGoca0Ag6pxRP,iv:m36+kif7BROc4KKWaIlN9MFA8wa+gxXs6R1D16FD3MU=,tag:Xm8XWIPU1RkOrNY0DbNhkQ==,type:str]
vault_nas_sudo_password: ENC[AES256_GCM,data:8TVzULuNcUQqrfyI,iv:OF64G6vykCvRJOicUsFmxcMtLS9YIKRRrZ8VmAkkJx4=,tag:eFtpB1Db4rEbvhxQ8z2Sig==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2cHhjZERWRjdyc1FsdkR1
bUttdTZPbjBPenZqUC85dGZ6aUlYTmtXblZZCi9NV0lGRU5SdWw3Kzl2NWU2bXlR
REZTNkcxajIzclF6OTBsMzV5WnJ6TzgKLS0tIHJFRVJ3RVQ0T2xycWtGcmRTV0ZZ
bkVrQTRLUUJxTXpQcUs4amNHdi9nQkEK9QphmUym4WxSbFcGzkkjkWemeSldP6ng
ILe8MOejUdOmLUH5N6KpYy26CHuX1bjx/e1OMRPtjHan4a6WrsIklg==
-----END AGE ENCRYPTED FILE-----
recipient: age1g759cwnlt6nq5u87jcs6m76fz3spw5awcwrchuck8ythxpf4g9rqarhy9y
lastmodified: "2026-06-26T01:33:15Z"
mac: ENC[AES256_GCM,data:NwX7stu3VDNTEyf2xrW0qhigtZS7B3zvu7Pt2M9uIpE7XTU07UzEL+yWpuhmSZ8EgvIt9cVvpmzsEuIYzl1l+eXdY7wVCr/s5FQyr4GCU2G81KlAv2TUYXqDEu/T5be7+CxpNu1VZYTbM4o9+MCoGb00E5oEkOJCdALwzMW2o14=,iv:1VHSe1gqfKi8geRnykEuqjes3pNSNxUtR/q+DBdC2m4=,tag:O5nPfqCuyBou6t+j/T5XrQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1

View File

@@ -0,0 +1,84 @@
---
# Homelab production inventory
# Nodes are mixed-arch Docker Swarm managed by Dokploy.
all:
children:
swarm_managers:
hosts:
tpi-n1:
ansible_host: 192.168.2.130
ansible_user: ubuntu
swarm_advertise_addr: 192.168.2.130
docker_data_root: /mnt/nvme/docker
node_labels:
infra: "true"
role: storage
storage: high
nfs_mounts:
- src: "192.168.2.59:/volume1/swarm-data"
dst: /mnt/swarm-data
opts: defaults
- src: "192.168.2.59:/volume1/photo"
dst: /mnt/photos
opts: defaults
swarm_workers:
hosts:
tpi-n2:
ansible_host: 192.168.2.19
ansible_user: ubuntu
docker_data_root: /var/lib/docker
node_labels:
npu: "true"
rk1: "true"
nfs_mounts:
- src: "192.168.2.59:/volume1/swarm-data"
dst: /mnt/swarm-data
opts: defaults
- src: "192.168.2.59:/volume1/photo"
dst: /mnt/photos
opts: defaults
node-nas:
ansible_host: 192.168.2.18
ansible_user: tim
docker_data_root: /mnt/nvme/docker
node_labels:
type: nas
nfs_mounts:
- src: "192.168.2.59:/volume1/swarm-data"
dst: /mnt/swarm-data
opts: defaults
- src: "192.168.2.59:/volume1/photo"
dst: /mnt/photos
opts: defaults
docker_hosts:
children:
swarm_managers:
swarm_workers:
vars:
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
# Swarm settings
swarm_manager_address: 192.168.2.130
swarm_manager_port: 2377
# Dokploy version and networking
dokploy_version: v0.29.8
dokploy_traefik_version: v3.6.1
dokploy_postgres_version: "16"
dokploy_redis_version: "7"
dokploy_network_name: dokploy-network
dokploy_ui_port: 3000
dokploy_traefik_dashboard_port: 8080
# DNS / Docker daemon defaults
homelab_dns_server: 192.168.2.130
fallback_dns:
- 8.8.8.8
- 8.8.4.4
insecure_registries:
- registry.lan

View File

@@ -0,0 +1,161 @@
---
- name: Bootstrap all Docker Swarm nodes
hosts: docker_hosts
become: true
gather_facts: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install common packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
when: ansible_os_family == "Debian"
- name: Ensure Docker keyring directory exists
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
when: ansible_os_family == "Debian"
- name: Check if Docker is already installed
ansible.builtin.command: docker --version
register: docker_installed
ignore_errors: true
changed_when: false
- name: Add Docker GPG key
ansible.builtin.shell: |
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
args:
creates: /etc/apt/keyrings/docker.gpg
executable: /bin/bash
when:
- ansible_os_family == "Debian"
- docker_installed.rc != 0
- name: Add Docker apt repository (deb822)
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: "https://download.docker.com/linux/ubuntu"
suites: "{{ ansible_lsb.codename | default(ansible_distribution_release) }}"
components: stable
signed_by: /etc/apt/keyrings/docker.gpg
architectures: "{{ ansible_architecture | regex_replace('aarch64', 'arm64') | regex_replace('x86_64', 'amd64') }}"
state: present
when:
- ansible_os_family == "Debian"
- docker_installed.rc != 0
- name: Install Docker CE
ansible.builtin.apt:
name:
- docker-ce{% if docker_version %}{{ "=" ~ docker_version }}{% endif %}
- docker-ce-cli{% if docker_version %}{{ "=" ~ docker_version }}{% endif %}
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: "{% if docker_version %}present{% else %}latest{% endif %}"
update_cache: true
when:
- ansible_os_family == "Debian"
- docker_installed.rc != 0
- name: Ensure Docker socket is enabled and started
ansible.builtin.systemd:
name: docker.socket
state: started
enabled: true
- name: Ensure Docker service is running
ansible.builtin.systemd:
name: docker
state: started
enabled: true
- name: Add user to docker group
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
- name: Ensure Docker data-root directory exists
ansible.builtin.file:
path: "{{ docker_data_root }}"
state: directory
mode: "0711"
- name: Configure Docker daemon
ansible.builtin.template:
src: docker-daemon.json.j2
dest: /etc/docker/daemon.json
mode: "0600"
notify: restart docker
- name: Configure sysctl for Swarm / containers
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: net.ipv4.ip_forward, value: "1" }
- { name: vm.max_map_count, value: "1048576" }
- name: Ensure ansible user has passwordless sudo
ansible.builtin.lineinfile:
path: /etc/sudoers.d/99-ansible-user
line: "{{ ansible_user }} ALL=(ALL) NOPASSWD:ALL"
create: true
mode: "0440"
validate: /usr/sbin/visudo -cf %s
- name: Mount NFS shares
ansible.posix.mount:
src: "{{ item.src }}"
path: "{{ item.dst }}"
fstype: nfs
opts: "{{ item.opts }}"
state: mounted
loop: "{{ nfs_mounts | default([]) }}"
when: nfs_mounts is defined
tags:
- nfs
- mounts
- name: Ensure NFS-backed subdirectories exist
ansible.builtin.file:
path: "{{ item.dst }}/{{ subdir }}"
state: directory
mode: "0755"
loop: "{{ nfs_mounts | default([]) }}"
loop_control:
label: "{{ item.dst }}/{{ subdir }}"
vars:
subdir: "{{ nfs_subdir_name }}"
when:
- nfs_mounts is defined
- nfs_subdir_name is defined
tags:
- nfs
- mounts
- name: Ensure MinIO data directory exists
ansible.builtin.file:
path: "{{ minio_data_dir }}"
state: directory
mode: "0700"
when: minio_data_dir is defined
handlers:
- name: restart docker
ansible.builtin.systemd:
name: docker
state: restarted

View File

@@ -0,0 +1,68 @@
---
- name: Initialize and join Docker Swarm
hosts: docker_hosts
become: true
gather_facts: true
vars:
manager_token: "{{ hostvars[groups['swarm_managers'][0]]['swarm_manager_token'] | default(omit) }}"
worker_token: "{{ hostvars[groups['swarm_managers'][0]]['swarm_worker_token'] | default(omit) }}"
tasks:
- name: Check current Swarm status
ansible.builtin.command: docker info --format '{{ '{{' }} .Swarm.LocalNodeState {{ '}}' }}'
register: swarm_status
changed_when: false
- name: Initialize Swarm on first manager
ansible.builtin.command: |
docker swarm init --advertise-addr {{ swarm_advertise_addr }}:{{ swarm_manager_port }}
when:
- inventory_hostname in groups['swarm_managers']
- swarm_status.stdout == "inactive"
register: swarm_init
- name: Retrieve Swarm tokens
ansible.builtin.command: "docker swarm {{ item }} -q"
loop:
- join-token manager
- join-token worker
register: swarm_tokens
changed_when: false
run_once: true
delegate_to: "{{ groups['swarm_managers'][0] }}"
when: inventory_hostname == groups['swarm_managers'][0]
- name: Set tokens as host facts
ansible.builtin.set_fact:
swarm_manager_token: "{{ swarm_tokens.results[0].stdout }}"
swarm_worker_token: "{{ swarm_tokens.results[1].stdout }}"
run_once: true
delegate_to: "{{ groups['swarm_managers'][0] }}"
- name: Join additional managers
ansible.builtin.command: |
docker swarm join --token {{ hostvars[groups['swarm_managers'][0]]['swarm_manager_token'] }}
{{ swarm_manager_address }}:{{ swarm_manager_port }}
--advertise-addr {{ swarm_advertise_addr | default(ansible_default_ipv4.address) }}:{{ swarm_manager_port }}
when:
- inventory_hostname in groups['swarm_managers']
- swarm_status.stdout == "inactive"
- inventory_hostname != groups['swarm_managers'][0]
- name: Join workers
ansible.builtin.command: |
docker swarm join --token {{ hostvars[groups['swarm_managers'][0]]['swarm_worker_token'] }}
{{ swarm_manager_address }}:{{ swarm_manager_port }}
--advertise-addr {{ ansible_default_ipv4.address }}:{{ swarm_manager_port }}
when:
- inventory_hostname in groups['swarm_workers']
- swarm_status.stdout == "inactive"
- name: Apply node labels
ansible.builtin.command: |
docker node update --label-add {{ item.key }}={{ item.value }} {{ inventory_hostname_short }}
loop: "{{ node_labels | default({}) | dict2items }}"
when: inventory_hostname in groups['swarm_managers'] or inventory_hostname in groups['swarm_workers']
run_once: true
delegate_to: "{{ groups['swarm_managers'][0] }}"
changed_when: true

View File

@@ -0,0 +1,61 @@
---
- name: Deploy Dokploy control plane
hosts: swarm_managers
become: true
gather_facts: false
run_once: true
tasks:
- name: Ensure Dokploy network exists
ansible.builtin.command: docker network create --driver overlay --attachable {{ dokploy_network_name }}
register: net_create
changed_when: "'already exists' not in net_create.stderr"
failed_when:
- net_create.rc != 0
- "'already exists' not in net_create.stderr"
- name: Ensure /etc/dokploy directory exists
ansible.builtin.file:
path: "{{ dokploy_etc_dir }}"
state: directory
mode: "0755"
- name: Ensure Traefik dynamic directory exists
ansible.builtin.file:
path: "{{ dokploy_etc_dir }}/traefik/dynamic"
state: directory
mode: "0755"
- name: Create or update Traefik static config
ansible.builtin.template:
src: traefik.yml.j2
dest: "{{ dokploy_etc_dir }}/traefik/traefik.yml"
mode: "0644"
- name: Create Dokploy dynamic config
ansible.builtin.template:
src: traefik-dokploy.yml.j2
dest: "{{ dokploy_etc_dir }}/traefik/dynamic/dokploy.yml"
mode: "0644"
- name: Create or update Docker secrets
ansible.builtin.shell: |
set -e
printf '%s' '{{ item.value }}' | docker secret create {{ item.name }} - 2>/dev/null || \
printf '%s' '{{ item.value }}' | docker secret create --replace {{ item.name }} -
loop:
- { name: dokploy_postgres_password, value: "{{ vault_dokploy_postgres_password }}" }
- { name: dokploy-auth-secret, value: "{{ vault_dokploy_auth_secret }}" }
no_log: true
- name: Render Dokploy stack file
ansible.builtin.template:
src: dokploy-stack.yml.j2
dest: "{{ dokploy_compose_dir }}/dokploy-stack.yml"
mode: "0644"
- name: Deploy Dokploy stack
ansible.builtin.command: |
docker stack deploy -c {{ dokploy_compose_dir }}/dokploy-stack.yml dokploy
register: stack_deploy
changed_when: "'Nothing found in stack' not in stack_deploy.stdout"

View File

@@ -0,0 +1,84 @@
---
- name: Deploy supporting Swarm services
hosts: swarm_managers
become: true
gather_facts: false
run_once: true
vars:
secrets_file: "{{ playbook_dir }}/../inventory/group_vars/all/secrets.sops.yml"
tasks:
- name: Decrypt MinIO root user
ansible.builtin.shell: |
export SOPS_AGE_KEY_FILE="{{ playbook_dir }}/../.sops/age-key.txt"
sops --extract '["vault_minio_root_user"]' --decrypt {{ secrets_file | quote }}
register: _minio_root_user_raw
no_log: true
delegate_to: localhost
become: false
- name: Decrypt MinIO root password
ansible.builtin.shell: |
export SOPS_AGE_KEY_FILE="{{ playbook_dir }}/../.sops/age-key.txt"
sops --extract '["vault_minio_root_password"]' --decrypt {{ secrets_file | quote }}
register: _minio_root_password_raw
no_log: true
delegate_to: localhost
become: false
- name: Decrypt registry htpasswd
ansible.builtin.shell: |
export SOPS_AGE_KEY_FILE="{{ playbook_dir }}/../.sops/age-key.txt"
sops --extract '["vault_registry_htpasswd"]' --decrypt {{ secrets_file | quote }}
register: _registry_htpasswd_raw
no_log: true
delegate_to: localhost
become: false
- name: Set decrypted secrets as facts
ansible.builtin.set_fact:
_minio_root_user: "{{ _minio_root_user_raw.stdout }}"
_minio_root_password: "{{ _minio_root_password_raw.stdout }}"
_registry_htpasswd: "{{ _registry_htpasswd_raw.stdout }}"
no_log: true
- name: Ensure compose directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ dokploy_compose_dir }}"
- "{{ dokploy_compose_dir }}/apps"
- "{{ dokploy_compose_dir }}/files/auth"
- name: Render supporting compose files
ansible.builtin.template:
src: "{{ playbook_dir }}/../templates/apps/{{ item }}.yml"
dest: "{{ dokploy_compose_dir }}/apps/{{ item }}.yml"
mode: "0644"
loop:
- gitea-registry
- technitium-dns
- minio
- name: Ensure registry htpasswd file exists
ansible.builtin.copy:
content: "{{ _registry_htpasswd }}"
dest: "{{ dokploy_compose_dir }}/files/auth/registry.password"
mode: "0600"
directory_mode: "0700"
no_log: true
- name: Deploy Gitea and registry stack
ansible.builtin.command: |
docker stack deploy -c {{ dokploy_compose_dir }}/apps/gitea-registry.yml gitea
- name: Deploy Technitium DNS stack
ansible.builtin.command: |
docker stack deploy -c {{ dokploy_compose_dir }}/apps/technitium-dns.yml dns
- name: Deploy MinIO stack
ansible.builtin.command: |
docker stack deploy -c {{ dokploy_compose_dir }}/apps/minio.yml minio

View File

@@ -0,0 +1,145 @@
---
- name: Backup Dokploy state and critical volumes
hosts: swarm_managers
become: true
gather_facts: true
run_once: true
vars:
backup_timestamp: "{{ ansible_facts.date_time.iso8601_basic_short }}"
backup_dir: "{{ backup_base_dir }}/{{ backup_timestamp }}"
secrets_file: "{{ playbook_dir }}/../inventory/group_vars/all/secrets.sops.yml"
age_key_file: "{{ playbook_dir }}/../.sops/age-key.txt"
tasks:
- name: Decrypt S3 access key
ansible.builtin.shell: |
export SOPS_AGE_KEY_FILE="{{ age_key_file }}"
sops --extract '["vault_backup_s3_access_key"]' --decrypt {{ secrets_file | quote }}
register: _backup_s3_access_key_raw
no_log: true
delegate_to: localhost
become: false
- name: Decrypt S3 secret key
ansible.builtin.shell: |
export SOPS_AGE_KEY_FILE="{{ age_key_file }}"
sops --extract '["vault_backup_s3_secret_key"]' --decrypt {{ secrets_file | quote }}
register: _backup_s3_secret_key_raw
no_log: true
delegate_to: localhost
become: false
- name: Read Age public key
ansible.builtin.shell: |
grep 'public key' "{{ age_key_file }}" | cut -d':' -f2 | tr -d ' '
register: _age_public_key_raw
no_log: true
delegate_to: localhost
become: false
- name: Set decrypted backup credentials
ansible.builtin.set_fact:
_backup_s3_access_key: "{{ _backup_s3_access_key_raw.stdout }}"
_backup_s3_secret_key: "{{ _backup_s3_secret_key_raw.stdout }}"
_age_public_key: "{{ _age_public_key_raw.stdout }}"
no_log: true
- name: Ensure age is installed
ansible.builtin.apt:
name: age
state: present
when: ansible_os_family == "Debian"
- name: Ensure backup directory exists
ansible.builtin.file:
path: "{{ backup_dir }}"
state: directory
mode: "0700"
- name: Dump Dokploy Postgres database
ansible.builtin.shell: |
docker exec $(docker ps -q -f name=dokploy-postgres) \
pg_dumpall -U dokploy > {{ backup_dir }}/dokploy-postgres.sql
args:
creates: "{{ backup_dir }}/dokploy-postgres.sql"
- name: Backup Dokploy /etc directory
ansible.builtin.archive:
path: /etc/dokploy
dest: "{{ backup_dir }}/etc-dokploy.tar.gz"
mode: "0600"
- name: Backup Traefik acme.json
ansible.builtin.copy:
src: /etc/dokploy/traefik/dynamic/acme.json
dest: "{{ backup_dir }}/acme.json"
mode: "0600"
remote_src: true
ignore_errors: true
- name: Backup critical named volumes
ansible.builtin.shell: |
set -e
for vol in dokploy-postgres-database redis-data-volume dokploy-docker-config gitea-giteasqlite-bhymqw_gitea-data gitea-registry-vdftrt_registry-data dns-technitum-6ojgo2_config-dns; do
docker run --rm -v "${vol}:/source:ro" -v "{{ backup_dir }}:/backup" alpine \
tar czf "/backup/vol-${vol}.tar.gz" -C /source .
done
args:
creates: "{{ backup_dir }}/vol-dokploy-postgres-database.tar.gz"
- name: Encrypt backup bundle with Age
ansible.builtin.shell: |
tar czf - -C {{ backup_base_dir }} {{ backup_timestamp }} | \
age -r {{ _age_public_key }} -o {{ backup_dir }}.tar.gz.age
args:
creates: "{{ backup_dir }}.tar.gz.age"
when: _age_public_key is defined
- name: Clean old unencrypted backup tree
ansible.builtin.file:
path: "{{ backup_dir }}"
state: absent
when: _age_public_key is defined
- name: Ensure rclone is installed
ansible.builtin.apt:
name: rclone
state: present
when: backup_s3_enabled | bool
tags:
- upload
- name: Upload encrypted backup to MinIO S3
ansible.builtin.shell: |
set -e
export RCLONE_CONFIG_S3_TYPE=s3
export RCLONE_CONFIG_S3_PROVIDER=Minio
export RCLONE_CONFIG_S3_ENDPOINT={{ backup_s3_endpoint }}
export RCLONE_CONFIG_S3_ACCESS_KEY_ID={{ _backup_s3_access_key }}
export RCLONE_CONFIG_S3_SECRET_ACCESS_KEY={{ _backup_s3_secret_key }}
export RCLONE_CONFIG_S3_REGION={{ backup_s3_region }}
export RCLONE_CONFIG_S3_NO_CHECK_BUCKET=true
export RCLONE_CONFIG_S3_ACL=
rclone copy "{{ backup_dir }}.tar.gz.age" "s3:{{ backup_s3_bucket }}/{{ backup_s3_path }}/"
args:
creates: "{{ backup_dir }}.s3-uploaded"
when: backup_s3_enabled | bool
no_log: true
tags:
- upload
- name: Mark S3 upload complete
ansible.builtin.file:
path: "{{ backup_dir }}.s3-uploaded"
state: touch
when: backup_s3_enabled | bool
tags:
- upload
- name: Prune local backups older than retention period
ansible.builtin.shell: |
find {{ backup_base_dir }} -maxdepth 1 -type f -name '*.age' -mtime +{{ backup_retention_days }} -delete
changed_when: true
tags:
- upload

View File

@@ -0,0 +1,86 @@
---
- name: Export Dokploy configuration metadata from Postgres
hosts: swarm_managers
become: true
gather_facts: true
run_once: true
vars:
export_dir: "{{ dokploy_compose_dir }}/dokploy-export"
export_timestamp: "{{ ansible_facts.date_time.iso8601_basic_short }}"
local_export_dir: "{{ playbook_dir }}/../exports"
tasks:
- name: Ensure export directory exists on manager
ansible.builtin.file:
path: "{{ export_dir }}"
state: directory
mode: "0700"
- name: Export key Dokploy tables as JSON
ansible.builtin.shell: |
set -e
docker exec $(docker ps -q -f name=dokploy-postgres) \
psql -U dokploy -t -A -F"," -c "SELECT json_agg(t) FROM (SELECT * FROM \"{{ item }}\") t" \
> "{{ export_dir }}/{{ item | regex_replace('-', '_') }}.json"
loop:
- application
- compose
- domain
- mount
- redirect
- security
- port
- server
- registry
- destination
- backup
- schedule
- notification
- ssh-key
- git_provider
- github
- gitlab
- gitea
- bitbucket
- environment
- project
- user
- organization
- apikey
- name: Fetch exports to control machine
ansible.builtin.fetch:
src: "{{ export_dir }}/{{ item | regex_replace('-', '_') }}.json"
dest: "{{ local_export_dir }}/{{ export_timestamp }}/{{ item | regex_replace('-', '_') }}.json"
flat: true
loop:
- application
- compose
- domain
- mount
- redirect
- security
- port
- server
- registry
- destination
- backup
- schedule
- notification
- ssh-key
- git_provider
- github
- gitlab
- gitea
- bitbucket
- environment
- project
- user
- organization
- apikey
- name: Clean old exports on manager
ansible.builtin.file:
path: "{{ export_dir }}"
state: absent

View File

@@ -0,0 +1,3 @@
collections:
- name: community.docker
- name: ansible.posix

25
homelab-iac/scripts/init-age.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
KEYS_DIR="${REPO_ROOT}/.sops"
KEY_FILE="${KEYS_DIR}/age-key.txt"
if [ -f "$KEY_FILE" ]; then
echo "Age key already exists at ${KEY_FILE}"
echo "Public key: $(grep 'public key' "$KEY_FILE" | cut -d':' -f2 | tr -d ' ')"
exit 0
fi
mkdir -p "$KEYS_DIR"
echo "Generating Age key..."
age-keygen -o "$KEY_FILE"
echo ""
echo "Key written to ${KEY_FILE}"
echo "BACK THIS FILE UP OFFLINE. If it is lost, all encrypted secrets are unrecoverable."
echo ""
echo "Add the following to .sops.yaml in the repo root:"
echo "creation_rules:"
echo " - path_regex: \"inventory/group_vars/.*\\.sops\\.yml$\""
echo " age: \"$(grep 'public key' "$KEY_FILE" | cut -d':' -f2 | tr -d ' ')\""

View File

@@ -0,0 +1,67 @@
version: "3.9"
services:
gitea:
image: docker.gitea.com/gitea:1.24.4
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
volumes:
- gitea-data:/data
expose:
- "3000"
ports:
- "2222:22"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/"]
interval: 15s
timeout: 5s
retries: 10
networks:
- gitea-giteasqlite
labels:
- traefik.enable=true
- traefik.http.routers.gitea-web.rule=Host(`gitea.bendtstudio.com`)
- traefik.http.routers.gitea-web.entrypoints=web
- traefik.http.routers.gitea-web.middlewares=redirect-to-https@file
- traefik.http.routers.gitea-websecure.rule=Host(`gitea.bendtstudio.com`)
- traefik.http.routers.gitea-websecure.entrypoints=websecure
- traefik.http.routers.gitea-websecure.tls.certresolver=letsencrypt
- traefik.http.services.gitea.loadbalancer.server.port=3000
registry:
image: registry:2
restart: always
environment:
REGISTRY_STORAGE_DELETE_ENABLED: "true"
REGISTRY_HEALTH_STORAGEDRIVER_ENABLED: "false"
REGISTRY_HTTP_SECRET: "{{ vault_registry_http_secret }}"
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
volumes:
- ./files/auth/registry.password:/auth/registry.password:ro
- registry-data:/var/lib/registry
networks:
- gitea-registry
- dokploy-network
labels:
- traefik.enable=true
- traefik.http.routers.registry-web.rule=Host(`registry.lan`)
- traefik.http.routers.registry-web.entrypoints=web
- traefik.http.routers.registry-web.middlewares=redirect-to-https@file
- traefik.http.routers.registry-websecure.rule=Host(`registry.lan`)
- traefik.http.routers.registry-websecure.entrypoints=websecure
- traefik.http.routers.registry-websecure.tls.certresolver=letsencrypt
- traefik.http.services.registry.loadbalancer.server.port=5000
volumes:
gitea-data:
registry-data:
networks:
gitea-giteasqlite:
gitea-registry:
dokploy-network:
external: true

View File

@@ -0,0 +1,40 @@
version: "3.9"
services:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
deploy:
replicas: 1
placement:
constraints:
- node.labels.type == nas
restart_policy:
condition: any
environment:
MINIO_ROOT_USER: "{{ _minio_root_user }}"
MINIO_ROOT_PASSWORD: "{{ _minio_root_password }}"
volumes:
- "{{ minio_data_dir }}:/data"
networks:
- dokploy-network
ports:
- target: 9000
published: 9000
mode: host
- target: 9001
published: 9001
mode: host
labels:
- traefik.enable=true
- traefik.http.routers.minio-web.rule=Host(`minio.bendtstudio.com`)
- traefik.http.routers.minio-web.entrypoints=web
- traefik.http.routers.minio-web.middlewares=redirect-to-https@file
- traefik.http.routers.minio-websecure.rule=Host(`minio.bendtstudio.com`)
- traefik.http.routers.minio-websecure.entrypoints=websecure
- traefik.http.routers.minio-websecure.tls.certresolver=letsencrypt
- traefik.http.services.minio.loadbalancer.server.port=9001
networks:
dokploy-network:
external: true

View File

@@ -0,0 +1,45 @@
version: "3.9"
services:
technitium-dns:
image: technitium/dns-server:latest
container_name: technitium-dns
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "5380:5380/tcp"
- "53443:53443/tcp"
volumes:
- config-dns:/etc/dns
environment:
DNS_SERVER_DOMAIN: dns.bendtstudio.com
DNS_SERVER_ADMIN_PASSWORD: "{{ vault_technitium_admin_password }}"
TZ: America/New_York
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.hostname == tpi-n1
restart_policy:
condition: any
networks:
- dokploy-network
labels:
- traefik.enable=true
- traefik.docker.network=dokploy-network
- traefik.http.routers.dns-web.rule=Host(`dns.bendtstudio.com`)
- traefik.http.routers.dns-web.entrypoints=web
- traefik.http.routers.dns-web.middlewares=redirect-to-https@file
- traefik.http.routers.dns-websecure.rule=Host(`dns.bendtstudio.com`)
- traefik.http.routers.dns-websecure.entrypoints=websecure
- traefik.http.routers.dns-websecure.tls.certresolver=letsencrypt
- traefik.http.services.dns.loadbalancer.server.port=5380
volumes:
config-dns:
networks:
dokploy-network:
external: true

View File

@@ -0,0 +1,5 @@
{
"data-root": "{{ docker_data_root }}",
"dns": {{ ([homelab_dns_server] + fallback_dns) | to_json }},
"insecure-registries": {{ insecure_registries | to_json }}
}

View File

@@ -0,0 +1,111 @@
version: "3.9"
services:
dokploy:
image: dokploy/dokploy:{{ dokploy_version }}
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: any
environment:
- ADVERTISE_ADDR={{ swarm_advertise_addr }}
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
- BETTER_AUTH_SECRET_FILE=/run/secrets/dokploy-auth-secret
volumes:
- /etc/dokploy:/etc/dokploy
- /var/run/docker.sock:/var/run/docker.sock
- dokploy-docker-config:/root/.docker
secrets:
- postgres_password
- dokploy-auth-secret
networks:
- dokploy-network
ports:
- target: 3000
published: {{ dokploy_ui_port }}
mode: host
dokploy-postgres:
image: postgres:{{ dokploy_postgres_version }}
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: any
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
- POSTGRES_USER=dokploy
- POSTGRES_DB=dokploy
- PGDATA=/var/lib/postgresql/data
volumes:
- dokploy-postgres-database:/var/lib/postgresql/data
secrets:
- postgres_password
networks:
- dokploy-network
dokploy-redis:
image: redis:{{ dokploy_redis_version }}
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: any
volumes:
- redis-data-volume:/data
networks:
- dokploy-network
dokploy-traefik:
image: traefik:{{ dokploy_traefik_version }}
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
restart_policy:
condition: any
command:
- --configFile=/etc/traefik/traefik.yml
volumes:
- /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- dokploy-network
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
- target: 443
published: 443
protocol: udp
mode: host
- target: 8080
published: {{ dokploy_traefik_dashboard_port }}
mode: host
secrets:
postgres_password:
external: true
dokploy-auth-secret:
external: true
volumes:
dokploy-docker-config:
dokploy-postgres-database:
redis-data-volume:
networks:
dokploy-network:
external: true

View File

@@ -0,0 +1,21 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
routers:
dokploy-router-app:
rule: Host(`dokploy.docker.localhost`) && PathPrefix(`/`)
service: dokploy-service-app
entryPoints:
- web
middlewares: []
services:
dokploy-service-app:
loadBalancer:
servers:
- url: http://dokploy:3000
passHostHeader: true

View File

@@ -0,0 +1,41 @@
global:
sendAnonymousUsage: false
providers:
swarm:
exposedByDefault: false
watch: true
docker:
exposedByDefault: false
watch: true
network: {{ dokploy_network_name }}
file:
directory: /etc/dokploy/traefik/dynamic
watch: true
entryPoints:
web:
address: :80
websecure:
address: :443
http3:
advertisedPort: 443
http:
tls:
certResolver: letsencrypt
api:
insecure: true
certificatesResolvers:
letsencrypt:
acme:
email: {{ letsencrypt_email }}
storage: /etc/dokploy/traefik/dynamic/acme.json
httpChallenge:
entryPoint: web
accessLog:
filePath: /etc/dokploy/traefik/dynamic/access.log
format: json
bufferingSize: 100