Compare commits

..

91 Commits

Author SHA1 Message Date
git c89b8129ec Merge branch 'main' of https://github.com/beatz174-bit/docker 2026-05-13 14:52:27 +10:00
git c98b2d8232 Merge branch 'main' of https://github.com/beatz174-bit/docker 2026-05-13 14:51:42 +10:00
beatzaplenty 3a9b3786c5 merge 2026-05-13 14:47:26 +10:00
beatzaplenty 1131eceb94 add socket proxy support 2026-05-13 14:45:32 +10:00
git 586be50567 Merge branch 'main' of https://github.com/beatz174-bit/docker 2026-05-13 14:34:03 +10:00
beatzaplenty d0e08b8fc1 corrected docker-compose package name 2026-05-13 14:33:40 +10:00
git 487117b31a Merge branch 'main' of https://github.com/beatz174-bit/docker 2026-05-13 14:29:09 +10:00
beatzaplenty 82fa90d843 install docker tooling to generate docs 2026-05-13 14:28:28 +10:00
git ef1e7432ee Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/docker 2026-05-13 14:16:08 +10:00
beatzaplenty 427ea20e43 updated gitea validation workflow 2026-05-13 14:15:05 +10:00
beatzaplenty 3298219ccf Update .gitea/workflows/generate-docs.yml
Validate Docs (Gitea) / validate (push) Failing after 3s
2026-05-13 04:05:39 +00:00
beatzaplenty b30c7f3a8a Update .gitea/workflows/validate-docs.yml
Validate Docs (Gitea) / validate (push) Failing after 5s
2026-05-13 04:05:12 +00:00
beatz174-bit 374511c123 Merge pull request #77 from beatz174-bit/codex/implement-dual-ci-workflow-for-docs-generation
Validate Docs (Gitea) / validate (push) Has been cancelled
ci: split docs generation to Gitea and docs validation/publish to GitHub
2026-05-13 14:01:16 +10:00
beatz174-bit 313f7f1c21 ci: use extended regex for docs secret scan 2026-05-13 14:00:52 +10:00
beatz174-bit 6aa78525c2 ci: split docs generation and publishing across gitea/github 2026-05-13 13:55:02 +10:00
beatz174-bit 5e0de23ff7 Merge pull request #75 from beatz174-bit/codex/update-docs-generation-workflow-for-reliability
Harden docs generation pipeline to refresh compose and Terraform data first
2026-05-13 13:42:20 +10:00
beatz174-bit 3c655dabcf Harden docs generation pipeline with strict source refresh 2026-05-13 13:40:50 +10:00
git 60e59f95a8 Merge branch 'main' of https://github.com/beatz174-bit/docker 2026-05-13 10:32:02 +10:00
git cdf37fb2c9 Regenerate docs 2026-05-13 10:30:45 +10:00
beatz174-bit f56b734cb4 Merge pull request #74 from beatz174-bit/codex/fix-public-diagrams-generation-scripts
Fix generated public topology and routing diagrams to use inventory-aware concepts
2026-05-13 10:18:20 +10:00
beatz174-bit 10c4373c0e Fix invalid DOT cluster label emission for host inventory 2026-05-13 10:17:37 +10:00
beatz174-bit 2619d86dc1 Fix public topology/routing diagram generation and layout 2026-05-13 10:08:42 +10:00
git 1b679a4f09 Refine public docs diagrams 2026-05-13 09:48:45 +10:00
beatz174-bit 02033cd3f9 Merge pull request #73 from beatz174-bit/codex/refine-github-pages-documentation-and-diagrams
Remove Prometheus Rules from public docs; simplify physical and Traefik/DNS diagrams
2026-05-13 09:37:07 +10:00
beatz174-bit d932bc57d0 Remove stale Prometheus Rules nav from public MkDocs config 2026-05-13 09:36:23 +10:00
beatz174-bit c4cfa8081f Refine public docs generation and simplify topology diagrams 2026-05-13 09:33:29 +10:00
beatz174-bit 90c9094a6f Merge pull request #72 from beatz174-bit/codex/improve-documentation-with-generated-diagrams
Generate physical and Docker/Traefik/Dynu diagrams; require Graphviz and validate in CI
2026-05-13 09:18:29 +10:00
beatz174-bit 36c5f2b1e3 Restore legacy docker-compose diagram semantics 2026-05-13 09:17:47 +10:00
beatz174-bit 7bc2729ef6 Fix Traefik router backend targets in generated diagrams 2026-05-13 09:17:41 +10:00
beatz174-bit 9d79f828e4 Improve docs pipeline with generated topology diagrams 2026-05-13 09:08:47 +10:00
beatz174-bit 22b3659cdf Update publish-docs.yml 2026-05-13 08:53:35 +10:00
git 4440fddb2b docs: regenerate public docs 2026-05-13 08:51:51 +10:00
beatz174-bit d74000fbd0 Merge pull request #71 from beatz174-bit/codex/refactor-docs-pipeline-for-local-generation
docs: publish only committed public docs from GitHub Actions
2026-05-13 08:49:01 +10:00
beatz174-bit 9efdb5c781 Merge branch 'main' into codex/refactor-docs-pipeline-for-local-generation 2026-05-13 08:48:43 +10:00
beatz174-bit e7fd52616d docs: move public docs generation to local workflow 2026-05-13 08:47:23 +10:00
beatz174-bit b7b4b4d36d Merge pull request #70 from beatz174-bit/codex/fix-github-pages-to-deploy-public-docs-only
docs: publish GitHub Pages from docs/public only
2026-05-13 08:45:54 +10:00
beatz174-bit 40f5a3ce0d docs: publish only docs/public to GitHub Pages 2026-05-13 08:43:57 +10:00
beatz174-bit 8e43118661 docs: regenerate environment documentation 2026-05-12 22:35:56 +00:00
beatz174-bit 0e76f3cef1 Merge pull request #69 from beatz174-bit/codex/fix-docs-generation-for-docker-compose-profiles
Fix docs compose rendering by enabling `all` profile and add fail-fast check
2026-05-13 08:35:26 +10:00
beatz174-bit 8d18ab7059 Fix docs compose rendering to include all profiles 2026-05-13 08:33:26 +10:00
beatz174-bit 6a959d85c3 Merge pull request #68 from beatz174-bit/codex/add-mkdocs-publishing-to-github-pages
Add GitHub Pages MkDocs publish workflow
2026-05-13 08:29:06 +10:00
beatz174-bit 79c583eca3 Add MkDocs GitHub Pages publish workflow 2026-05-13 08:25:31 +10:00
beatz174-bit 7fcafed3f9 docs: regenerate environment documentation 2026-05-12 22:21:06 +00:00
beatz174-bit 0b67830d7f Merge pull request #67 from beatz174-bit/codex/update-github-actions-for-automated-docs-generation
docs: auto-commit generated docs on push and add browsable docs site structure
2026-05-13 08:20:21 +10:00
beatz174-bit d878a0f9b8 docs: make compose inventory generation deterministic 2026-05-13 08:19:39 +10:00
beatz174-bit 9c38910a67 docs: automate generated docs commits and add docs site structure 2026-05-13 08:09:51 +10:00
beatz174-bit 4a38a9421d Merge pull request #66 from beatz174-bit/codex/add-github-actions-for-docs-generation
docs: add GitHub Actions pipeline for compose documentation generation
2026-05-13 07:54:48 +10:00
beatz174-bit 5d32693925 docs: remove rg dependency and avoid archive compose fallback 2026-05-13 07:52:53 +10:00
beatz174-bit 696cecfecb ci: make compose installation resilient on ubuntu runners 2026-05-13 07:49:34 +10:00
beatz174-bit c0360a14b9 docs: add automated compose documentation generation pipeline 2026-05-13 07:45:57 +10:00
beatz174-bit 5589594d2c Merge pull request #65 from beatz174-bit/codex/add-dynu-dns-records-to-architecture-docs
Add optional Dynu DNS inventory support to architecture docs generation
2026-05-13 07:37:33 +10:00
beatz174-bit 0bd41b2fb1 Migrate legacy architecture heading during section upsert 2026-05-13 07:36:57 +10:00
beatz174-bit 5f10d0366e Add optional Dynu DNS inventory to architecture doc generator 2026-05-13 07:31:20 +10:00
git 9f98101c5d first dynu record generation 2026-05-13 07:23:20 +10:00
git 0c20676590 update .gitignore 2026-05-13 07:22:55 +10:00
beatz174-bit 59e2c2b9a3 Merge pull request #64 from beatz174-bit/codex/add-interactive-picker-for-terraform-outputs
Add interactive Terraform output picker for Dynu brownfield generator
2026-05-13 06:52:37 +10:00
beatz174-bit 61d49e85a3 Add interactive Terraform output picker for Dynu generator 2026-05-13 06:52:24 +10:00
beatz174-bit 8f84ed6d83 Merge pull request #63 from beatz174-bit/codex/fix-regexreplace-usage-in-terraform
Remove unsupported `regexreplace` usage from Dynu Terraform inventory
2026-05-13 06:42:07 +10:00
beatz174-bit 24cbb02bff Fix Dynu inventory by removing unsupported regexreplace usage 2026-05-13 06:40:25 +10:00
beatz174-bit 685a472572 Merge pull request #62 from beatz174-bit/codex/fix-dynu-brownfield-generator-keyerror
Fix Dynu brownfield generator output handling and state->enabled mapping
2026-05-13 06:37:33 +10:00
beatz174-bit 9de1bee542 Fix Dynu brownfield generator output handling 2026-05-13 06:37:10 +10:00
beatz174-bit 306e2c14db Merge pull request #61 from beatz174-bit/codex/update-terraform-for-dynu-dns-reconciliation
Add Dynu brownfield DNS inventory, outputs, and generator
2026-05-13 06:23:27 +10:00
beatz174-bit 63b47b59b5 Align wildcard DNS name sanitization across outputs and generator 2026-05-13 06:23:03 +10:00
beatz174-bit 52bd2d9fa2 Add Dynu brownfield DNS inventory outputs and generator 2026-05-13 06:03:32 +10:00
git 034ad17cf9 updated dynu terraform configuration 2026-05-13 05:37:59 +10:00
beatz174-bit ef2846562b Merge pull request #60 from beatz174-bit/codex/add-dynu-dns-terraform-documentation-layer-3vpdzo
dynu: add configurable root domain, import-ready domain resource, unpin provider version, and docs updates
2026-05-13 05:29:50 +10:00
beatz174-bit 52e2102f93 Merge branch 'main' into codex/add-dynu-dns-terraform-documentation-layer-3vpdzo 2026-05-13 05:29:40 +10:00
beatz174-bit a9b21912db Derive Dynu record FQDNs from configured root domain 2026-05-13 05:28:32 +10:00
beatz174-bit c9e4aeb8d6 Add dynu_record_import_id variable for record imports 2026-05-13 05:28:27 +10:00
beatz174-bit 96a44d5da6 Add dynu_root_domain variable for domain import flow 2026-05-13 05:18:02 +10:00
beatz174-bit 0d936677e5 Merge pull request #59 from beatz174-bit/codex/add-dynu-dns-terraform-documentation-layer-0fccdt
dynu: unpin provider, add domain skeleton, update imports/docs and records
2026-05-13 05:09:36 +10:00
beatz174-bit 5c38b92bc8 Remove Dynu provider version constraint 2026-05-13 05:09:17 +10:00
beatz174-bit fc5c882193 Merge pull request #58 from beatz174-bit/codex/add-host-topology-documentation-generator
Include VM-to-host mappings in Proxmox inventory output
2026-05-13 04:36:56 +10:00
beatz174-bit a4501d4034 Add VM inventory to proxmox outputs for host topology 2026-05-13 04:36:38 +10:00
beatz174-bit c0958d8f02 Merge pull request #57 from beatz174-bit/codex/add-dynu-dns-terraform-documentation-layer
Add Dynu Terraform brownfield DNS documentation layer
2026-05-13 03:36:03 +10:00
beatz174-bit 2a97864a06 Add Dynu Terraform brownfield DNS documentation layer 2026-05-13 03:17:37 +10:00
beatz174-bit bd23339edd Merge pull request #56 from beatz174-bit/codex/add-gitea-actions-runner-with-socket-proxy
Add Gitea Actions runner and configuration (compose, env, README)
2026-05-12 14:04:22 +10:00
beatz174-bit f8091a5c76 Use DOCKER_SOCKET_PROXY_HOST for gitea-runner Docker host 2026-05-12 14:03:57 +10:00
beatz174-bit 6a2639f931 Merge pull request #55 from beatz174-bit/codex/improve-dynu-dns-inventory-correlation
Harden Dynu/Traefik DNS correlation parsing and validation
2026-04-21 14:11:36 +10:00
beatz174-bit fae5e119d1 Harden Dynu/Traefik DNS correlation and validation 2026-04-21 14:11:25 +10:00
git 872038d0c9 first succesful build of dynu inventory 2026-04-21 13:57:16 +10:00
beatz174-bit 7a6db9fcfd Merge pull request #54 from beatz174-bit/codex/fix-dynu-api-authentication-error
Handle Dynu API auth failures without Python traceback
2026-04-21 13:52:16 +10:00
beatz174-bit d6a8979d55 Handle Dynu API auth failures without traceback 2026-04-21 13:52:01 +10:00
beatz174-bit a9a8a708d3 Merge pull request #53 from beatz174-bit/codex/fix-dynu-api-authentication-issue
Auto-load secrets/dynu.env, harden Dynu credential handling, and update docs
2026-04-21 13:38:52 +10:00
beatz174-bit 749c0d500d Improve Dynu env handling and document secrets/dynu.env 2026-04-21 13:38:33 +10:00
beatz174-bit 8f112af65b Merge pull request #52 from beatz174-bit/codex/integrate-dynu-dns-in-read-only-mode
Add read-only Dynu DNS inventory and Traefik correlation scripts
2026-04-21 12:45:50 +10:00
beatz174-bit 580e9b9aed Add strict read-only Dynu DNS inventory integration 2026-04-21 12:31:52 +10:00
beatz174-bit c77db36865 Merge pull request #51 from beatz174-bit/codex/add-basic-ansible-foundation
Add phase-1 Ansible foundation and safe validation hooks
2026-04-21 12:10:07 +10:00
beatz174-bit e11dc22999 Add phase-1 Ansible foundation and validation scaffolding 2026-04-21 12:07:29 +10:00
beatz174-bit 862ddd42f8 Merge pull request #50 from beatz174-bit/codex/update-documentation-for-codex-setup
docs: document Codex setup and maintenance scripts
2026-04-21 11:51:18 +10:00
beatz174-bit d0e7e52150 docs: add codex setup and maintenance script guidance 2026-04-21 11:51:02 +10:00
105 changed files with 9947 additions and 38 deletions
+71
View File
@@ -0,0 +1,71 @@
name: Generate Docs
on:
workflow_dispatch:
schedule:
- cron: "0 */6 * * *"
jobs:
generate:
runs-on: ubuntu-latest
env:
DOCKER_HOST: tcp://docker-socket-proxy:2375
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install docker CLI
run: |
apt-get update
apt-get install -y docker.io docker-compose
- name: Generate docs
run: |
scripts/docs/generate-all.sh
- name: Validate generated docs
run: |
set -e
test -s docs/generated/docker-compose.resolved.yml
test -s docs/generated/host-topology.md
test -s docs/public/physical-topology.svg
test -s docs/public/docker-traefik-dynu.svg
! grep -R "Host inventory JSON not found" docs/public docs/diagrams
! grep -R "Generate terraform inventory" docs/public docs/diagrams
# Ensure no obvious secrets leaked
! grep -R -E -i "password|token|api[_-]?key|secret" docs/public \
|| (echo "Secret-like string detected"; exit 1)
- name: Commit changes
run: |
git config user.name "docs-bot"
git config user.email "docs-bot@local"
git add docs/generated docs/diagrams docs/public data/terraform/proxmox-inventory.json || true
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "docs: regenerate documentation artifacts"
- name: Push to Gitea
run: |
git push origin HEAD:main
- name: Push to GitHub mirror
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_PUSH_TOKEN }}
GITHUB_MIRROR_REPO: ${{ vars.GITHUB_MIRROR_REPO }}
run: |
test -n "$GITHUB_TOKEN"
test -n "$GITHUB_MIRROR_REPO"
git remote add github "https://$GITHUB_TOKEN@github.com/$GITHUB_MIRROR_REPO.git" || true
git push github HEAD:main
+24
View File
@@ -0,0 +1,24 @@
name: Validate Docs (Gitea)
on:
push:
branches: [ main ]
paths:
- "docs/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate docs
run: |
set -e
test -d docs/public
test -s docs/public/physical-topology.svg
! grep -R "Host inventory JSON not found" docs/public
! grep -R "Generate terraform inventory" docs/public
+35
View File
@@ -0,0 +1,35 @@
name: Validate committed public docs
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
validate-public-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ensure committed docs/public exists
run: |
test -d docs/public
test -n "$(find docs/public -mindepth 1 -print -quit)"
- name: Install MkDocs
run: |
python3 -m pip install --user mkdocs
- name: Validate docs content
run: |
set -e
test -s docs/public/physical-topology.svg
test -s docs/public/docker-traefik-dynu.svg
! grep -R "Host inventory JSON not found" docs/public
! grep -R "Generate terraform inventory" docs/public
! rg -n -i "password|token|api[_-]?key|secret" docs/public
- name: Build MkDocs site
run: |
python3 -m mkdocs build -f mkdocs-public.yml --strict
+84
View File
@@ -0,0 +1,84 @@
name: Publish documentation site
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: github-pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure committed docs/public exists
run: |
test -d docs/public
test -n "$(find docs/public -mindepth 1 -print -quit)"
- name: Install Graphviz
run: |
sudo apt-get update
sudo apt-get install -y graphviz
dot -V
- name: Validate sanitized diagram artifacts
run: |
test -f docs/public/physical-topology.svg
test -f docs/public/docker-traefik-dynu.svg
! rg -n "Graphviz dot not found" docs/public/*.svg
! rg -n "lan\.ddnsgeek\.com" docs/public/*.svg docs/public/*.md
! rg -n -i "password|token|api_key|secret" docs/public/*.svg
- name: Install MkDocs
run: |
python3 -m pip install --user mkdocs
- name: Build public MkDocs site
run: |
python3 -m mkdocs build -f mkdocs-public.yml --strict
- name: Verify published content excludes internal/generated docs
run: |
test -d site-public
test ! -e site-public/generated
test ! -e site-public/docker
- name: Verify expected 404-only paths are not generated
run: |
test ! -e site-public/generated/compose-inventory/index.html
test ! -e site-public/generated/prometheus-rules/index.html
test ! -e site-public/docker/index.html
- name: Configure GitHub Pages
uses: actions/configure-pages@v5
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site-public
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+23
View File
@@ -4,6 +4,7 @@
#!**/Dockerfile
#!docker-compose.yml
**/data/
apps/gitea/runner-data/
**/db/
**/database/
apps/nextcloud/config/
@@ -28,3 +29,25 @@ secrets/*
!.env.example
core/traefik/certs/*
!core/traefik/certs/.gitkeep
site/
# Docs generation artifacts intentionally tracked
!data/terraform/proxmox-inventory.json
!infrastructure/terraform/dynu/generated/
!infrastructure/terraform/dynu/generated/dynu_dns_records_inventory.json
!docs/generated/
!docs/generated/docker-compose.resolved.yml
!docs/generated/host-topology.md
!docs/diagrams/
!docs/diagrams/*.svg
!docs/public/
!docs/public/*.md
!docs/public/*.svg
# Terraform local/state artifacts
**/.terraform/
**/.terraform.lock.hcl
*.tfstate
*.tfstate.*
*.tfvars
*.tfvars.json
+84 -1
View File
@@ -17,11 +17,20 @@ If you only read one section, read **[Source-of-truth boundaries](docs/source-of
- Docker environment composition and `services-up.sh`: [docs/docker-environment.md](docs/docker-environment.md)
- Terraform workflows (brownfield import/reconciliation): [docs/terraform-workflows.md](docs/terraform-workflows.md)
- Infrastructure inventory intent and outputs: [docs/infrastructure-inventory.md](docs/infrastructure-inventory.md)
- Dynu DNS read-only inventory workflow: [docs/dynu-dns-inventory.md](docs/dynu-dns-inventory.md)
- Generated host topology doc: [docs/generated/host-topology.md](docs/generated/host-topology.md)
- Ansible bootstrap workflows: [docs/ansible-workflows.md](docs/ansible-workflows.md)
- Deployment prerequisites and secrets setup: [docs/deployment-prerequisites.md](docs/deployment-prerequisites.md)
- Secrets inventory: [docs/security-secrets.md](docs/security-secrets.md)
Terraform subtrees:
Codex helper scripts:
- Initial Codex environment/bootstrap setup: [scripts/codex-setup.sh](scripts/codex-setup.sh)
- Codex environment maintenance/refresh: [scripts/codex-maintenance.sh](scripts/codex-maintenance.sh)
Infrastructure subtrees:
- Ansible foundation docs: [infrastructure/ansible/README.md](infrastructure/ansible/README.md)
- Terraform root docs: [infrastructure/terraform/README.md](infrastructure/terraform/README.md)
- Terraform Docker mirror: [infrastructure/terraform/docker/README.md](infrastructure/terraform/docker/README.md)
- Terraform Proxmox inventory: [infrastructure/terraform/proxmox/README.md](infrastructure/terraform/proxmox/README.md)
@@ -36,6 +45,13 @@ Terraform subtrees:
- `services-up.sh` composes the environment by discovering compose files and applying common env/network inputs.
- For service runtime behavior, start from Compose files and `services-up.sh` (not Terraform).
### Ansible (bootstrap foundation)
- Ansible under `infrastructure/ansible/` is a phase-1 foundation for inventory/configuration scaffolding.
- It supports safe validation (inventory parsing and playbook syntax checks) while hosts/devices are onboarded gradually.
- It does not replace Compose runtime authority or Terraform reconciliation authority at this stage.
### Terraform (inventory and reconciliation authority)
- Terraform under `infrastructure/terraform/` is used to codify and reconcile existing infrastructure.
@@ -103,3 +119,70 @@ flowchart TB
```
For request-flow and network detail, see [docs/architecture.md](docs/architecture.md).
## Public docs publication workflow
Public docs are generated on the Docker host and committed to this repository. GitHub Actions only publishes committed content from `docs/public`.
1. Generate public docs locally from the repository root:
```bash
./scripts/generate-public-docs.sh
```
2. Inspect the generated changes:
```bash
git diff -- docs/public docs/generated docs/diagrams
```
3. Commit the generated public docs (and any supporting generated files you intend to version):
```bash
git add docs/public docs/generated docs/diagrams
git commit -m "docs: regenerate public docs"
```
4. Push your branch:
```bash
git push
```
Only files under `docs/public` are published by GitHub Pages. Internal/generated documentation is not published unless it is deliberately copied/sanitized into `docs/public`.
### Regenerating architecture docs (Prometheus + Dynu DNS)
```bash
# Refresh Dynu live inventory and generated resources/import helpers
cd infrastructure/terraform/dynu
terraform apply -refresh-only
python3 scripts/generate-brownfield-records.py --overwrite
# Regenerate architecture docs from Prometheus + Dynu inventory
cd ../../..
python3 scripts/render_prometheus_docs.py \
--inventory-file docs/runtime/prometheus-inventory.json \
--dynu-dns-inventory-file infrastructure/terraform/dynu/generated/dynu_dns_records_inventory.json
```
---
## Codex setup and maintenance scripts
The repository includes helper scripts for Codex sessions that need local tooling and safe placeholder secret material for validation-only workflows:
- `scripts/codex-setup.sh`
- Installs baseline CLI dependencies (shell/yaml/terraform/ansible tooling).
- Prepares `secrets/stack-secrets.env` from templates and creates dummy file-based secret placeholders based on `secrets/inventory.json`.
- Installs/refreshed baseline Ansible collections when `infrastructure/ansible/collections/requirements.yml` is present.
- Runs safe Ansible bootstrap checks (version, inventory parse, playbook syntax check) without live connectivity operations.
- Prints installed tool versions for quick verification.
- `scripts/codex-maintenance.sh`
- Refreshes Python-based linting/automation tooling.
- Reconciles placeholder secret files against current `secrets/inventory.json` (creates missing, removes stale).
- Rebuilds `secrets/stack-secrets.env` with dummy values for compose-config validation.
- Refreshes Ansible collections and repeats safe inventory/syntax validation checks.
Both scripts are intended for local validation environments and should not be treated as production provisioning automation.
+55
View File
@@ -0,0 +1,55 @@
# Gitea
## Gitea Actions
Gitea Actions is enabled by setting:
- `GITEA__actions__ENABLED=true`
## Runner service
The repository includes a dedicated Gitea Actions runner service named:
- `gitea-runner`
The runner uses Docker through the existing Docker socket proxy:
- `DOCKER_HOST=tcp://docker-socket-proxy:2375`
The runner intentionally **does not** mount:
- `/var/run/docker.sock`
## Registration token
Generate a runner registration token from the Gitea UI:
- Site Administration → Actions → Runners
- or Repo → Settings → Actions → Runners
Put the token in your env/secrets file:
- `GITEA_RUNNER_REGISTRATION_TOKEN=...`
## Start the runner
- `./services-up.sh --profile gitea up -d gitea-runner`
- or `./services-up.sh --profile all up -d gitea-runner`
## Logs
- `docker logs -f gitea-runner`
## Labels
Common workflow label:
- `runs-on: ubuntu-latest`
This should match the configured labels, for example:
- `GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bookworm,...`
## Security note
The runner can control Docker through `docker-socket-proxy`. This is safer than mounting the raw Docker socket directly, but workflows still have meaningful control over Docker. Only trusted repositories/users should be allowed to run workflows on this runner.
+20
View File
@@ -9,6 +9,7 @@ services:
- USER_GID=${GITEA_USER_GID}
- GITEA__database__DB_TYPE=${GITEA_DB_TYPE}
- GITEA__server__ROOT_URL=${GITEA_ROOT_URL}
- GITEA__actions__ENABLED=true
volumes:
- ${PROJECT_ROOT}/apps/gitea/data:/data
networks:
@@ -31,6 +32,25 @@ services:
retries: 6
start_period: 120s
gitea-runner:
profiles: ["apps","all","gitea","ci"]
container_name: gitea-runner
image: gitea/act_runner:latest
restart: always
depends_on:
- gitea
- docker-socket-proxy
environment:
- GITEA_INSTANCE_URL=${GITEA_ROOT_URL}
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
- GITEA_RUNNER_NAME=${GITEA_RUNNER_NAME}
- GITEA_RUNNER_LABELS=${GITEA_RUNNER_LABELS}
- DOCKER_HOST=${DOCKER_SOCKET_PROXY_HOST}
volumes:
- ${PROJECT_ROOT}/apps/gitea/runner-data:/data
networks:
- traefik
#volumes:
# gitea_data:
+5
View File
@@ -13,6 +13,11 @@ GITEA_USER_UID=1000
GITEA_USER_GID=1000
GITEA_DB_TYPE=sqlite3
GITEA_ROOT_URL=https://gitea.lan.ddnsgeek.com/
# Generate a token in Gitea: Site Administration → Actions → Runners
# or Repo → Settings → Actions → Runners
GITEA_RUNNER_REGISTRATION_TOKEN=vYDNxzMvayREkXoaAR3x3UREkxQB2PU4eORzmkZ9
GITEA_RUNNER_NAME=docker-runner-01
GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://node:20-bookworm,linux:docker://node:20-bookworm,docker:docker://docker:cli
# Grafana
GRAFANA_ROOT_URL=https://grafana.lan.ddnsgeek.com/
+41
View File
@@ -0,0 +1,41 @@
# Generated Documentation
## Local generation
Install prerequisites:
```bash
sudo apt-get update
sudo apt-get install -y graphviz
```
Then generate and validate public docs:
```bash
chmod +x scripts/docs/*.sh
scripts/docs/generate-all.sh
python3 -m mkdocs build -f mkdocs-public.yml --strict
```
NixOS-friendly alternative:
```bash
nix shell nixpkgs#graphviz nixpkgs#python3 nixpkgs#python3Packages.pyyaml
```
This pipeline only runs `docker compose config` and static parsing. It does **not** start containers.
## CI behaviour
GitHub Actions workflow `.github/workflows/generate-docs.yml` validates committed public docs and diagrams and runs a strict public MkDocs build.
## Outputs
- `docs/generated`: resolved compose config and markdown inventories
- `docs/diagrams`: generated DOT and SVG diagrams
- `docs/public`: sanitized copy for public sharing
## Publication safety
- `docs/public` is intended for public sharing after sanitization.
- `docs/generated` and `docs/diagrams` may include internal details and should be treated as internal by default.
+72
View File
@@ -0,0 +1,72 @@
# Ansible Workflows (Bootstrap / Phase 1)
Ansible is being introduced as a minimal, maintainable foundation for host/device inventory and future configuration workflows.
## Why introduce Ansible now
- The repository already has strong runtime and infrastructure boundaries (Compose + Terraform).
- A small Ansible baseline allows gradual host onboarding without forcing immediate large-scale automation.
- It enables safe validation workflows (`inventory --list`, playbook syntax checks) before real execution.
## What Ansible is for in this repository (right now)
- YAML inventory structure for hosts/devices to be onboarded over time.
- Group and host variable scaffolding for future incremental adoption.
- Validation-oriented starter playbook and local tooling checks.
## What Ansible is not for yet
- Replacing Docker Compose runtime authority.
- Replacing Terraform inventory/reconciliation authority.
- Becoming the current source of truth for NixOS host management.
- Becoming the current source of truth for all network automation.
## Directory layout
- `infrastructure/ansible/ansible.cfg`
- `infrastructure/ansible/inventory/hosts.yml`
- `infrastructure/ansible/inventory/group_vars/`
- `infrastructure/ansible/inventory/host_vars/`
- `infrastructure/ansible/playbooks/ping.yml`
- `infrastructure/ansible/collections/requirements.yml`
## Add a host (gradual onboarding)
1. Open `infrastructure/ansible/inventory/hosts.yml`.
2. Add the host under an appropriate group (`linux`, `network`, `virtualization`, or `nixos`).
3. Add non-sensitive defaults under group vars only when shared across hosts.
4. Add host-specific values in `inventory/host_vars/<hostname>.yml`.
5. Keep secrets out of committed files.
Example pattern:
```yaml
linux:
hosts:
my-host:
ansible_host: my-host.local
```
## Validation commands
Run from repository root:
```bash
ansible --version
ansible-lint --version
ansible-inventory -i infrastructure/ansible/inventory/hosts.yml --list
ansible-playbook -i infrastructure/ansible/inventory/hosts.yml infrastructure/ansible/playbooks/ping.yml --syntax-check
```
Install/update baseline collections:
```bash
ansible-galaxy collection install -r infrastructure/ansible/collections/requirements.yml -p infrastructure/ansible/collections
```
## Guardrails for future expansion
- Keep changes incremental (one host/group/playbook change at a time).
- Prefer simple playbooks before introducing roles.
- Add network-platform/NixOS-specific logic only when those boundaries are explicitly adopted.
- Keep documentation aligned with source-of-truth boundaries when Ansible authority evolves.
+1
View File
@@ -88,6 +88,7 @@ Use architecture docs together with:
- [docs/source-of-truth.md](source-of-truth.md)
- [docs/terraform-workflows.md](terraform-workflows.md)
- [docs/infrastructure-inventory.md](infrastructure-inventory.md)
- [docs/generated/host-topology.md](generated/host-topology.md)
## Notes on runtime vs declared state
+14
View File
@@ -0,0 +1,14 @@
# Automation
This section describes automation systems associated with this environment.
Current state:
- CI documentation generation in GitHub Actions.
- Static parsing and rendering from repository configuration.
- No live service interaction during documentation generation.
Future content can document:
- Docker update automation.
- Node-RED automation flows and operational patterns.
View File
+75
View File
@@ -0,0 +1,75 @@
digraph Compose {
rankdir=LR;
node [fontname="Helvetica"];
"svc:authelia" [label="authelia", shape=box, style=filled, fillcolor="#dfefff"];
"svc:crowdsec" [label="crowdsec", shape=box, style=filled, fillcolor="#dfefff"];
"svc:docker-socket-proxy" [label="docker-socket-proxy", shape=box, style=filled, fillcolor="#dfefff"];
"svc:docker-update-exporter" [label="docker-update-exporter", shape=box, style=filled, fillcolor="#dfefff"];
"svc:error-pages" [label="error-pages", shape=box, style=filled, fillcolor="#dfefff"];
"svc:gitea" [label="gitea", shape=box, style=filled, fillcolor="#dfefff"];
"svc:gitea-runner" [label="gitea-runner", shape=box, style=filled, fillcolor="#dfefff"];
"svc:gotify" [label="gotify", shape=box, style=filled, fillcolor="#dfefff"];
"svc:grafana" [label="grafana", shape=box, style=filled, fillcolor="#dfefff"];
"svc:gramps-redis" [label="gramps-redis", shape=box, style=filled, fillcolor="#dfefff"];
"svc:grampsweb" [label="grampsweb", shape=box, style=filled, fillcolor="#dfefff"];
"svc:grampsweb_celery" [label="grampsweb_celery", shape=box, style=filled, fillcolor="#dfefff"];
"svc:influxdb" [label="influxdb", shape=box, style=filled, fillcolor="#dfefff"];
"svc:monitor-kuma" [label="monitor-kuma", shape=box, style=filled, fillcolor="#dfefff"];
"svc:mtls-bridge" [label="mtls-bridge", shape=box, style=filled, fillcolor="#dfefff"];
"svc:nextcloud-db" [label="nextcloud-db", shape=box, style=filled, fillcolor="#dfefff"];
"svc:nextcloud-redis" [label="nextcloud-redis", shape=box, style=filled, fillcolor="#dfefff"];
"svc:nextcloud-webapp" [label="nextcloud-webapp", shape=box, style=filled, fillcolor="#dfefff"];
"svc:node-exporter" [label="node-exporter", shape=box, style=filled, fillcolor="#dfefff"];
"svc:node-red" [label="node-red", shape=box, style=filled, fillcolor="#dfefff"];
"svc:passbolt-db" [label="passbolt-db", shape=box, style=filled, fillcolor="#dfefff"];
"svc:passbolt-webapp" [label="passbolt-webapp", shape=box, style=filled, fillcolor="#dfefff"];
"svc:pihole-exporter" [label="pihole-exporter", shape=box, style=filled, fillcolor="#dfefff"];
"svc:portainer" [label="portainer", shape=box, style=filled, fillcolor="#dfefff"];
"svc:prometheus" [label="prometheus", shape=box, style=filled, fillcolor="#dfefff"];
"svc:searxng-webapp" [label="searxng-webapp", shape=box, style=filled, fillcolor="#dfefff"];
"svc:telegraf" [label="telegraf", shape=box, style=filled, fillcolor="#dfefff"];
"svc:traefik" [label="traefik", shape=box, style=filled, fillcolor="#dfefff"];
"net:gramps" [label="gramps", shape=ellipse, style=filled, fillcolor="#f4f4f4"];
"net:monitor" [label="monitor", shape=ellipse, style=filled, fillcolor="#f4f4f4"];
"net:nextcloud" [label="nextcloud", shape=ellipse, style=filled, fillcolor="#f4f4f4"];
"net:passbolt" [label="passbolt", shape=ellipse, style=filled, fillcolor="#f4f4f4"];
"net:traefik" [label="traefik", shape=ellipse, style=filled, fillcolor="#f4f4f4"];
"svc:authelia" -> "net:traefik";
"svc:crowdsec" -> "net:traefik";
"svc:docker-socket-proxy" -> "net:monitor";
"svc:docker-socket-proxy" -> "net:traefik";
"svc:docker-update-exporter" -> "net:monitor";
"svc:error-pages" -> "net:traefik";
"svc:gitea" -> "net:traefik";
"svc:gitea-runner" -> "net:traefik";
"svc:gotify" -> "net:traefik";
"svc:grafana" -> "net:monitor";
"svc:grafana" -> "net:traefik";
"svc:gramps-redis" -> "net:gramps";
"svc:grampsweb" -> "net:gramps";
"svc:grampsweb" -> "net:traefik";
"svc:grampsweb_celery" -> "net:gramps";
"svc:influxdb" -> "net:monitor";
"svc:influxdb" -> "net:traefik";
"svc:monitor-kuma" -> "net:monitor";
"svc:monitor-kuma" -> "net:traefik";
"svc:mtls-bridge" -> "net:monitor";
"svc:mtls-bridge" -> "net:traefik";
"svc:nextcloud-db" -> "net:nextcloud";
"svc:nextcloud-redis" -> "net:nextcloud";
"svc:nextcloud-webapp" -> "net:nextcloud";
"svc:nextcloud-webapp" -> "net:traefik";
"svc:node-exporter" -> "net:monitor";
"svc:node-red" -> "net:monitor";
"svc:node-red" -> "net:traefik";
"svc:passbolt-db" -> "net:passbolt";
"svc:passbolt-webapp" -> "net:passbolt";
"svc:passbolt-webapp" -> "net:traefik";
"svc:pihole-exporter" -> "net:monitor";
"svc:portainer" -> "net:traefik";
"svc:prometheus" -> "net:monitor";
"svc:prometheus" -> "net:traefik";
"svc:searxng-webapp" -> "net:traefik";
"svc:telegraf" -> "net:monitor";
"svc:traefik" -> "net:traefik";
}
+439
View File
@@ -0,0 +1,439 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: Compose Pages: 1 -->
<svg width="334pt" height="1502pt"
viewBox="0.00 0.00 334.49 1502.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1498)">
<title>Compose</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1498 330.49,-1498 330.49,4 -4,4"/>
<!-- svc:authelia -->
<g id="node1" class="node">
<title>svc:authelia</title>
<polygon fill="#dfefff" stroke="black" points="126,-738 54,-738 54,-702 126,-702 126,-738"/>
<text text-anchor="middle" x="90" y="-716.3" font-family="Helvetica,sans-Serif" font-size="14.00">authelia</text>
</g>
<!-- net:traefik -->
<g id="node33" class="node">
<title>net:traefik</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-774" rx="40.09" ry="18"/>
<text text-anchor="middle" x="271.25" y="-770.3" font-family="Helvetica,sans-Serif" font-size="14.00">traefik</text>
</g>
<!-- svc:authelia&#45;&gt;net:traefik -->
<g id="edge1" class="edge">
<title>svc:authelia&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M126.41,-730.67C155.5,-739.43 196.8,-751.87 227.69,-761.18"/>
<polygon fill="black" stroke="black" points="226.74,-764.55 237.32,-764.08 228.76,-757.85 226.74,-764.55"/>
</g>
<!-- svc:crowdsec -->
<g id="node2" class="node">
<title>svc:crowdsec</title>
<polygon fill="#dfefff" stroke="black" points="130.5,-684 49.5,-684 49.5,-648 130.5,-648 130.5,-684"/>
<text text-anchor="middle" x="90" y="-662.3" font-family="Helvetica,sans-Serif" font-size="14.00">crowdsec</text>
</g>
<!-- svc:crowdsec&#45;&gt;net:traefik -->
<g id="edge2" class="edge">
<title>svc:crowdsec&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M130.61,-674.06C146.59,-678.3 164.81,-684.44 180,-693 206.25,-707.78 231.35,-731.35 248.38,-749.26"/>
<polygon fill="black" stroke="black" points="246.24,-752.1 255.62,-757.03 251.37,-747.33 246.24,-752.1"/>
</g>
<!-- svc:docker&#45;socket&#45;proxy -->
<g id="node3" class="node">
<title>svc:docker&#45;socket&#45;proxy</title>
<polygon fill="#dfefff" stroke="black" points="167.5,-954 12.5,-954 12.5,-918 167.5,-918 167.5,-954"/>
<text text-anchor="middle" x="90" y="-932.3" font-family="Helvetica,sans-Serif" font-size="14.00">docker&#45;socket&#45;proxy</text>
</g>
<!-- net:monitor -->
<g id="node30" class="node">
<title>net:monitor</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-1206" rx="46.29" ry="18"/>
<text text-anchor="middle" x="271.25" y="-1202.3" font-family="Helvetica,sans-Serif" font-size="14.00">monitor</text>
</g>
<!-- svc:docker&#45;socket&#45;proxy&#45;&gt;net:monitor -->
<g id="edge3" class="edge">
<title>svc:docker&#45;socket&#45;proxy&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M167.53,-953.71C172.02,-956.38 176.24,-959.46 180,-963 242.3,-1021.6 261.83,-1127.12 267.77,-1177.6"/>
<polygon fill="black" stroke="black" points="264.31,-1178.21 268.86,-1187.78 271.27,-1177.46 264.31,-1178.21"/>
</g>
<!-- svc:docker&#45;socket&#45;proxy&#45;&gt;net:traefik -->
<g id="edge4" class="edge">
<title>svc:docker&#45;socket&#45;proxy&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M165.31,-917.9C170.5,-915.32 175.46,-912.37 180,-909 218.01,-880.82 244.97,-831.56 259.03,-800.98"/>
<polygon fill="black" stroke="black" points="262.27,-802.31 263.14,-791.76 255.87,-799.46 262.27,-802.31"/>
</g>
<!-- svc:docker&#45;update&#45;exporter -->
<g id="node4" class="node">
<title>svc:docker&#45;update&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="180,-1494 0,-1494 0,-1458 180,-1458 180,-1494"/>
<text text-anchor="middle" x="90" y="-1472.3" font-family="Helvetica,sans-Serif" font-size="14.00">docker&#45;update&#45;exporter</text>
</g>
<!-- svc:docker&#45;update&#45;exporter&#45;&gt;net:monitor -->
<g id="edge5" class="edge">
<title>svc:docker&#45;update&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M168.37,-1457.79C172.55,-1455.23 176.47,-1452.32 180,-1449 242.3,-1390.4 261.83,-1284.88 267.77,-1234.4"/>
<polygon fill="black" stroke="black" points="271.27,-1234.54 268.86,-1224.22 264.31,-1233.79 271.27,-1234.54"/>
</g>
<!-- svc:error&#45;pages -->
<g id="node5" class="node">
<title>svc:error&#45;pages</title>
<polygon fill="#dfefff" stroke="black" points="137.5,-630 42.5,-630 42.5,-594 137.5,-594 137.5,-630"/>
<text text-anchor="middle" x="90" y="-608.3" font-family="Helvetica,sans-Serif" font-size="14.00">error&#45;pages</text>
</g>
<!-- svc:error&#45;pages&#45;&gt;net:traefik -->
<g id="edge6" class="edge">
<title>svc:error&#45;pages&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M137.71,-619.76C152.25,-623.79 167.68,-629.86 180,-639 218.01,-667.18 244.97,-716.44 259.03,-747.02"/>
<polygon fill="black" stroke="black" points="255.87,-748.54 263.14,-756.24 262.27,-745.69 255.87,-748.54"/>
</g>
<!-- svc:gitea -->
<g id="node6" class="node">
<title>svc:gitea</title>
<polygon fill="#dfefff" stroke="black" points="117,-576 63,-576 63,-540 117,-540 117,-576"/>
<text text-anchor="middle" x="90" y="-554.3" font-family="Helvetica,sans-Serif" font-size="14.00">gitea</text>
</g>
<!-- svc:gitea&#45;&gt;net:traefik -->
<g id="edge7" class="edge">
<title>svc:gitea&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M117.07,-560.28C136.41,-563.2 162.37,-569.85 180,-585 229.98,-627.95 254.42,-704.8 264.44,-746.04"/>
<polygon fill="black" stroke="black" points="261.07,-747 266.73,-755.95 267.88,-745.42 261.07,-747"/>
</g>
<!-- svc:gitea&#45;runner -->
<g id="node7" class="node">
<title>svc:gitea&#45;runner</title>
<polygon fill="#dfefff" stroke="black" points="142,-522 38,-522 38,-486 142,-486 142,-522"/>
<text text-anchor="middle" x="90" y="-500.3" font-family="Helvetica,sans-Serif" font-size="14.00">gitea&#45;runner</text>
</g>
<!-- svc:gitea&#45;runner&#45;&gt;net:traefik -->
<g id="edge8" class="edge">
<title>svc:gitea&#45;runner&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M142.31,-511.04C155.9,-515.03 169.65,-521.26 180,-531 242.3,-589.6 261.83,-695.12 267.77,-745.6"/>
<polygon fill="black" stroke="black" points="264.31,-746.21 268.86,-755.78 271.27,-745.46 264.31,-746.21"/>
</g>
<!-- svc:gotify -->
<g id="node8" class="node">
<title>svc:gotify</title>
<polygon fill="#dfefff" stroke="black" points="118,-468 62,-468 62,-432 118,-432 118,-468"/>
<text text-anchor="middle" x="90" y="-446.3" font-family="Helvetica,sans-Serif" font-size="14.00">gotify</text>
</g>
<!-- svc:gotify&#45;&gt;net:traefik -->
<g id="edge9" class="edge">
<title>svc:gotify&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M118.17,-451.6C137.81,-454.16 163.67,-460.68 180,-477 254.99,-551.96 268.02,-687.13 270.03,-745.69"/>
<polygon fill="black" stroke="black" points="266.53,-745.79 270.29,-755.69 273.53,-745.61 266.53,-745.79"/>
</g>
<!-- svc:grafana -->
<g id="node9" class="node">
<title>svc:grafana</title>
<polygon fill="#dfefff" stroke="black" points="125.5,-1278 54.5,-1278 54.5,-1242 125.5,-1242 125.5,-1278"/>
<text text-anchor="middle" x="90" y="-1256.3" font-family="Helvetica,sans-Serif" font-size="14.00">grafana</text>
</g>
<!-- svc:grafana&#45;&gt;net:monitor -->
<g id="edge10" class="edge">
<title>svc:grafana&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M125.56,-1249.59C153.74,-1241.1 193.78,-1229.04 224.6,-1219.75"/>
<polygon fill="black" stroke="black" points="225.69,-1223.08 234.26,-1216.84 223.67,-1216.38 225.69,-1223.08"/>
</g>
<!-- svc:grafana&#45;&gt;net:traefik -->
<g id="edge11" class="edge">
<title>svc:grafana&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M125.54,-1257.93C144.23,-1254.97 166.25,-1248.21 180,-1233 238.17,-1168.67 262.1,-892.17 268.43,-802.32"/>
<polygon fill="black" stroke="black" points="271.93,-802.3 269.13,-792.08 264.95,-801.82 271.93,-802.3"/>
</g>
<!-- svc:gramps&#45;redis -->
<g id="node10" class="node">
<title>svc:gramps&#45;redis</title>
<polygon fill="#dfefff" stroke="black" points="144.5,-360 35.5,-360 35.5,-324 144.5,-324 144.5,-360"/>
<text text-anchor="middle" x="90" y="-338.3" font-family="Helvetica,sans-Serif" font-size="14.00">gramps&#45;redis</text>
</g>
<!-- net:gramps -->
<g id="node29" class="node">
<title>net:gramps</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-342" rx="45.49" ry="18"/>
<text text-anchor="middle" x="271.25" y="-338.3" font-family="Helvetica,sans-Serif" font-size="14.00">gramps</text>
</g>
<!-- svc:gramps&#45;redis&#45;&gt;net:gramps -->
<g id="edge12" class="edge">
<title>svc:gramps&#45;redis&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M144.78,-342C167.14,-342 193.05,-342 215.51,-342"/>
<polygon fill="black" stroke="black" points="215.56,-345.5 225.56,-342 215.56,-338.5 215.56,-345.5"/>
</g>
<!-- svc:grampsweb -->
<g id="node11" class="node">
<title>svc:grampsweb</title>
<polygon fill="#dfefff" stroke="black" points="139,-414 41,-414 41,-378 139,-378 139,-414"/>
<text text-anchor="middle" x="90" y="-392.3" font-family="Helvetica,sans-Serif" font-size="14.00">grampsweb</text>
</g>
<!-- svc:grampsweb&#45;&gt;net:gramps -->
<g id="edge13" class="edge">
<title>svc:grampsweb&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M139.03,-381.53C165.7,-373.5 198.68,-363.56 224.9,-355.66"/>
<polygon fill="black" stroke="black" points="226.03,-358.98 234.59,-352.74 224.01,-352.27 226.03,-358.98"/>
</g>
<!-- svc:grampsweb&#45;&gt;net:traefik -->
<g id="edge14" class="edge">
<title>svc:grampsweb&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M139.27,-401.34C154.08,-405.21 169.28,-411.81 180,-423 225.12,-470.09 256.34,-671.08 266.59,-745.84"/>
<polygon fill="black" stroke="black" points="263.14,-746.48 267.95,-755.92 270.08,-745.55 263.14,-746.48"/>
</g>
<!-- svc:grampsweb_celery -->
<g id="node12" class="node">
<title>svc:grampsweb_celery</title>
<polygon fill="#dfefff" stroke="black" points="163.5,-306 16.5,-306 16.5,-270 163.5,-270 163.5,-306"/>
<text text-anchor="middle" x="90" y="-284.3" font-family="Helvetica,sans-Serif" font-size="14.00">grampsweb_celery</text>
</g>
<!-- svc:grampsweb_celery&#45;&gt;net:gramps -->
<g id="edge15" class="edge">
<title>svc:grampsweb_celery&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M151.18,-306.13C175.28,-313.39 202.57,-321.61 224.94,-328.35"/>
<polygon fill="black" stroke="black" points="223.94,-331.71 234.52,-331.24 225.96,-325 223.94,-331.71"/>
</g>
<!-- svc:influxdb -->
<g id="node13" class="node">
<title>svc:influxdb</title>
<polygon fill="#dfefff" stroke="black" points="127,-1224 53,-1224 53,-1188 127,-1188 127,-1224"/>
<text text-anchor="middle" x="90" y="-1202.3" font-family="Helvetica,sans-Serif" font-size="14.00">influxdb</text>
</g>
<!-- svc:influxdb&#45;&gt;net:monitor -->
<g id="edge16" class="edge">
<title>svc:influxdb&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M127.27,-1206C152.33,-1206 186.13,-1206 214.57,-1206"/>
<polygon fill="black" stroke="black" points="214.79,-1209.5 224.79,-1206 214.79,-1202.5 214.79,-1209.5"/>
</g>
<!-- svc:influxdb&#45;&gt;net:traefik -->
<g id="edge17" class="edge">
<title>svc:influxdb&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M127.17,-1203.5C145.42,-1200.36 166.5,-1193.56 180,-1179 231.57,-1123.4 259.36,-885.3 267.6,-802.51"/>
<polygon fill="black" stroke="black" points="271.1,-802.62 268.59,-792.33 264.14,-801.94 271.1,-802.62"/>
</g>
<!-- svc:monitor&#45;kuma -->
<g id="node14" class="node">
<title>svc:monitor&#45;kuma</title>
<polygon fill="#dfefff" stroke="black" points="146.5,-1170 33.5,-1170 33.5,-1134 146.5,-1134 146.5,-1170"/>
<text text-anchor="middle" x="90" y="-1148.3" font-family="Helvetica,sans-Serif" font-size="14.00">monitor&#45;kuma</text>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:monitor -->
<g id="edge18" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M146.73,-1168.79C171.73,-1176.32 200.85,-1185.1 224.54,-1192.23"/>
<polygon fill="black" stroke="black" points="223.75,-1195.65 234.33,-1195.18 225.77,-1188.94 223.75,-1195.65"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:traefik -->
<g id="edge19" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M146.69,-1144.49C159.02,-1140.46 171.06,-1134.33 180,-1125 225.12,-1077.91 256.34,-876.92 266.59,-802.16"/>
<polygon fill="black" stroke="black" points="270.08,-802.45 267.95,-792.08 263.14,-801.52 270.08,-802.45"/>
</g>
<!-- svc:mtls&#45;bridge -->
<g id="node15" class="node">
<title>svc:mtls&#45;bridge</title>
<polygon fill="#dfefff" stroke="black" points="138.5,-1116 41.5,-1116 41.5,-1080 138.5,-1080 138.5,-1116"/>
<text text-anchor="middle" x="90" y="-1094.3" font-family="Helvetica,sans-Serif" font-size="14.00">mtls&#45;bridge</text>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:monitor -->
<g id="edge20" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M138.76,-1108.34C152.58,-1112.41 167.34,-1117.87 180,-1125 206.25,-1139.78 231.35,-1163.35 248.38,-1181.26"/>
<polygon fill="black" stroke="black" points="246.24,-1184.1 255.62,-1189.03 251.37,-1179.33 246.24,-1184.1"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:traefik -->
<g id="edge21" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M138.53,-1092.51C153.46,-1088.63 168.92,-1082.07 180,-1071 254.99,-996.04 268.02,-860.87 270.03,-802.31"/>
<polygon fill="black" stroke="black" points="273.53,-802.39 270.29,-792.31 266.53,-802.21 273.53,-802.39"/>
</g>
<!-- svc:nextcloud&#45;db -->
<g id="node16" class="node">
<title>svc:nextcloud&#45;db</title>
<polygon fill="#dfefff" stroke="black" points="144,-90 36,-90 36,-54 144,-54 144,-90"/>
<text text-anchor="middle" x="90" y="-68.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;db</text>
</g>
<!-- net:nextcloud -->
<g id="node31" class="node">
<title>net:nextcloud</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-180" rx="55.49" ry="18"/>
<text text-anchor="middle" x="271.25" y="-176.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud</text>
</g>
<!-- svc:nextcloud&#45;db&#45;&gt;net:nextcloud -->
<g id="edge22" class="edge">
<title>svc:nextcloud&#45;db&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M144.33,-84.04C156.48,-87.9 169.03,-92.82 180,-99 206.09,-113.7 231.04,-137.06 248.07,-154.93"/>
<polygon fill="black" stroke="black" points="245.93,-157.77 255.31,-162.7 251.06,-153 245.93,-157.77"/>
</g>
<!-- svc:nextcloud&#45;redis -->
<g id="node17" class="node">
<title>svc:nextcloud&#45;redis</title>
<polygon fill="#dfefff" stroke="black" points="152,-198 28,-198 28,-162 152,-162 152,-198"/>
<text text-anchor="middle" x="90" y="-176.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;redis</text>
</g>
<!-- svc:nextcloud&#45;redis&#45;&gt;net:nextcloud -->
<g id="edge23" class="edge">
<title>svc:nextcloud&#45;redis&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M152.18,-180C169.48,-180 188.35,-180 205.83,-180"/>
<polygon fill="black" stroke="black" points="205.94,-183.5 215.94,-180 205.94,-176.5 205.94,-183.5"/>
</g>
<!-- svc:nextcloud&#45;webapp -->
<g id="node18" class="node">
<title>svc:nextcloud&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="162.5,-252 17.5,-252 17.5,-216 162.5,-216 162.5,-252"/>
<text text-anchor="middle" x="90" y="-230.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;webapp</text>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud -->
<g id="edge24" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M151.18,-215.87C173.57,-209.12 198.72,-201.55 220.11,-195.1"/>
<polygon fill="black" stroke="black" points="221.4,-198.37 229.97,-192.13 219.38,-191.67 221.4,-198.37"/>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:traefik -->
<g id="edge25" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M162.89,-247.71C169.31,-251.2 175.18,-255.56 180,-261 212.66,-297.85 255.07,-643.36 267,-745.62"/>
<polygon fill="black" stroke="black" points="263.55,-746.27 268.18,-755.8 270.51,-745.47 263.55,-746.27"/>
</g>
<!-- svc:node&#45;exporter -->
<g id="node19" class="node">
<title>svc:node&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="148,-1440 32,-1440 32,-1404 148,-1404 148,-1440"/>
<text text-anchor="middle" x="90" y="-1418.3" font-family="Helvetica,sans-Serif" font-size="14.00">node&#45;exporter</text>
</g>
<!-- svc:node&#45;exporter&#45;&gt;net:monitor -->
<g id="edge26" class="edge">
<title>svc:node&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M148.25,-1412.27C159.68,-1408.33 170.94,-1402.79 180,-1395 229.98,-1352.05 254.42,-1275.2 264.44,-1233.96"/>
<polygon fill="black" stroke="black" points="267.88,-1234.58 266.73,-1224.05 261.07,-1233 267.88,-1234.58"/>
</g>
<!-- svc:node&#45;red -->
<g id="node20" class="node">
<title>svc:node&#45;red</title>
<polygon fill="#dfefff" stroke="black" points="129.5,-1062 50.5,-1062 50.5,-1026 129.5,-1026 129.5,-1062"/>
<text text-anchor="middle" x="90" y="-1040.3" font-family="Helvetica,sans-Serif" font-size="14.00">node&#45;red</text>
</g>
<!-- svc:node&#45;red&#45;&gt;net:monitor -->
<g id="edge27" class="edge">
<title>svc:node&#45;red&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M129.57,-1049.69C146.29,-1053.6 165.34,-1060.13 180,-1071 218.01,-1099.18 244.97,-1148.44 259.03,-1179.02"/>
<polygon fill="black" stroke="black" points="255.87,-1180.54 263.14,-1188.24 262.27,-1177.69 255.87,-1180.54"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:traefik -->
<g id="edge28" class="edge">
<title>svc:node&#45;red&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M129.95,-1040.03C147.14,-1036.46 166.48,-1029.72 180,-1017 242.3,-958.4 261.83,-852.88 267.77,-802.4"/>
<polygon fill="black" stroke="black" points="271.27,-802.54 268.86,-792.22 264.31,-801.79 271.27,-802.54"/>
</g>
<!-- svc:passbolt&#45;db -->
<g id="node21" class="node">
<title>svc:passbolt&#45;db</title>
<polygon fill="#dfefff" stroke="black" points="139,-36 41,-36 41,0 139,0 139,-36"/>
<text text-anchor="middle" x="90" y="-14.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt&#45;db</text>
</g>
<!-- net:passbolt -->
<g id="node32" class="node">
<title>net:passbolt</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-72" rx="48.99" ry="18"/>
<text text-anchor="middle" x="271.25" y="-68.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt</text>
</g>
<!-- svc:passbolt&#45;db&#45;&gt;net:passbolt -->
<g id="edge29" class="edge">
<title>svc:passbolt&#45;db&#45;&gt;net:passbolt</title>
<path fill="none" stroke="black" d="M139.03,-32.47C165.15,-40.34 197.32,-50.03 223.27,-57.85"/>
<polygon fill="black" stroke="black" points="222.3,-61.21 232.88,-60.74 224.32,-54.51 222.3,-61.21"/>
</g>
<!-- svc:passbolt&#45;webapp -->
<g id="node22" class="node">
<title>svc:passbolt&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="157.5,-144 22.5,-144 22.5,-108 157.5,-108 157.5,-144"/>
<text text-anchor="middle" x="90" y="-122.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt&#45;webapp</text>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:passbolt -->
<g id="edge30" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:passbolt</title>
<path fill="none" stroke="black" d="M151.18,-107.87C174.64,-100.8 201.13,-92.82 223.16,-86.19"/>
<polygon fill="black" stroke="black" points="224.37,-89.48 232.94,-83.24 222.35,-82.77 224.37,-89.48"/>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:traefik -->
<g id="edge31" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M157.85,-136.93C166.26,-140.81 174,-146.02 180,-153 189.88,-164.49 250.79,-625.33 266.53,-745.56"/>
<polygon fill="black" stroke="black" points="263.1,-746.33 267.87,-755.8 270.04,-745.43 263.1,-746.33"/>
</g>
<!-- svc:pihole&#45;exporter -->
<g id="node23" class="node">
<title>svc:pihole&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="151.5,-1386 28.5,-1386 28.5,-1350 151.5,-1350 151.5,-1386"/>
<text text-anchor="middle" x="90" y="-1364.3" font-family="Helvetica,sans-Serif" font-size="14.00">pihole&#45;exporter</text>
</g>
<!-- svc:pihole&#45;exporter&#45;&gt;net:monitor -->
<g id="edge32" class="edge">
<title>svc:pihole&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M151.62,-1355.78C161.68,-1352.07 171.57,-1347.25 180,-1341 218.01,-1312.82 244.97,-1263.56 259.03,-1232.98"/>
<polygon fill="black" stroke="black" points="262.27,-1234.31 263.14,-1223.76 255.87,-1231.46 262.27,-1234.31"/>
</g>
<!-- svc:portainer -->
<g id="node24" class="node">
<title>svc:portainer</title>
<polygon fill="#dfefff" stroke="black" points="130,-900 50,-900 50,-864 130,-864 130,-900"/>
<text text-anchor="middle" x="90" y="-878.3" font-family="Helvetica,sans-Serif" font-size="14.00">portainer</text>
</g>
<!-- svc:portainer&#45;&gt;net:traefik -->
<g id="edge33" class="edge">
<title>svc:portainer&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M130.17,-874.06C146.25,-869.82 164.67,-863.64 180,-855 206.25,-840.22 231.35,-816.65 248.38,-798.74"/>
<polygon fill="black" stroke="black" points="251.37,-800.67 255.62,-790.97 246.24,-795.9 251.37,-800.67"/>
</g>
<!-- svc:prometheus -->
<g id="node25" class="node">
<title>svc:prometheus</title>
<polygon fill="#dfefff" stroke="black" points="140,-1008 40,-1008 40,-972 140,-972 140,-1008"/>
<text text-anchor="middle" x="90" y="-986.3" font-family="Helvetica,sans-Serif" font-size="14.00">prometheus</text>
</g>
<!-- svc:prometheus&#45;&gt;net:monitor -->
<g id="edge34" class="edge">
<title>svc:prometheus&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M140.37,-997.26C154.39,-1001.24 168.86,-1007.42 180,-1017 229.98,-1059.95 254.42,-1136.8 264.44,-1178.04"/>
<polygon fill="black" stroke="black" points="261.07,-1179 266.73,-1187.95 267.88,-1177.42 261.07,-1179"/>
</g>
<!-- svc:prometheus&#45;&gt;net:traefik -->
<g id="edge35" class="edge">
<title>svc:prometheus&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M140.37,-982.74C154.39,-978.76 168.86,-972.58 180,-963 229.98,-920.05 254.42,-843.2 264.44,-801.96"/>
<polygon fill="black" stroke="black" points="267.88,-802.58 266.73,-792.05 261.07,-801 267.88,-802.58"/>
</g>
<!-- svc:searxng&#45;webapp -->
<g id="node26" class="node">
<title>svc:searxng&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="156,-846 24,-846 24,-810 156,-810 156,-846"/>
<text text-anchor="middle" x="90" y="-824.3" font-family="Helvetica,sans-Serif" font-size="14.00">searxng&#45;webapp</text>
</g>
<!-- svc:searxng&#45;webapp&#45;&gt;net:traefik -->
<g id="edge36" class="edge">
<title>svc:searxng&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M151.18,-809.87C176.26,-802.32 204.79,-793.72 227.63,-786.84"/>
<polygon fill="black" stroke="black" points="228.81,-790.14 237.37,-783.9 226.79,-783.44 228.81,-790.14"/>
</g>
<!-- svc:telegraf -->
<g id="node27" class="node">
<title>svc:telegraf</title>
<polygon fill="#dfefff" stroke="black" points="125.5,-1332 54.5,-1332 54.5,-1296 125.5,-1296 125.5,-1332"/>
<text text-anchor="middle" x="90" y="-1310.3" font-family="Helvetica,sans-Serif" font-size="14.00">telegraf</text>
</g>
<!-- svc:telegraf&#45;&gt;net:monitor -->
<g id="edge37" class="edge">
<title>svc:telegraf&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M125.8,-1307.18C142.85,-1302.93 163.26,-1296.43 180,-1287 206.25,-1272.22 231.35,-1248.65 248.38,-1230.74"/>
<polygon fill="black" stroke="black" points="251.37,-1232.67 255.62,-1222.97 246.24,-1227.9 251.37,-1232.67"/>
</g>
<!-- svc:traefik -->
<g id="node28" class="node">
<title>svc:traefik</title>
<polygon fill="#dfefff" stroke="black" points="121,-792 59,-792 59,-756 121,-756 121,-792"/>
<text text-anchor="middle" x="90" y="-770.3" font-family="Helvetica,sans-Serif" font-size="14.00">traefik</text>
</g>
<!-- svc:traefik&#45;&gt;net:traefik -->
<g id="edge38" class="edge">
<title>svc:traefik&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M121,-774C148.16,-774 188.69,-774 220.66,-774"/>
<polygon fill="black" stroke="black" points="220.71,-777.5 230.71,-774 220.71,-770.5 220.71,-777.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

+118
View File
@@ -0,0 +1,118 @@
digraph DockerTraefikDynu {
graph [rankdir=LR, compound=true, splines=polyline, nodesep=0.9, ranksep=1.6, fontname="Helvetica", concentrate=true, newrank=true];
node [fontname="Helvetica", fontsize=11, style="rounded,filled"];
edge [fontname="Helvetica", fontsize=9, color="#334155"];
"dynu" [label="Dynu / Public DNS", shape=box, fillcolor="#fde68a"];
"svc:traefik" [label="Traefik", shape=box, fillcolor="#bfdbfe"];
"dynu" -> "svc:traefik" [penwidth=1.6];
"svc:authelia" [label="authelia
[TLS]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:authelia" [penwidth=1.4];
"dns:auth.<domain>" [label="auth.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:auth.<domain>" -> "dynu";
"svc:gitea" [label="gitea
:3000
[TLS]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:gitea" [penwidth=1.4];
"dns:gitea.<domain>" [label="gitea.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:gitea.<domain>" -> "dynu";
"svc:gotify" [label="gotify
:80", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:gotify" [penwidth=1.4];
"dns:gotify.<domain>" [label="gotify.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:gotify.<domain>" -> "dynu";
"svc:grafana" [label="grafana
:3000", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:grafana" [penwidth=1.4];
"dns:grafana.<domain>" [label="grafana.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:grafana.<domain>" -> "dynu";
"svc:grampsweb" [label="grampsweb", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:grampsweb" [penwidth=1.4];
"dns:familytree.<domain>" [label="familytree.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:familytree.<domain>" -> "dynu";
"svc:influxdb" [label="influxdb
:8086
[authelia]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:influxdb" [penwidth=1.4];
"dns:influxdb.<domain>" [label="influxdb.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:influxdb.<domain>" -> "dynu";
"svc:monitor-kuma" [label="monitor-kuma
[TLS]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:monitor-kuma" [penwidth=1.4];
"dns:monitor-kuma.<domain>" [label="monitor-kuma.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:monitor-kuma.<domain>" -> "dynu";
"svc:mtls-bridge" [label="mtls-bridge
:8080
[mTLS]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:mtls-bridge" [penwidth=1.4];
"dns:mtls-bridge.<domain>" [label="mtls-bridge.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:mtls-bridge.<domain>" -> "dynu";
"svc:nextcloud-webapp" [label="nextcloud-webapp", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:nextcloud-webapp" [penwidth=1.4];
"dns:nextcloud.<domain>" [label="nextcloud.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:nextcloud.<domain>" -> "dynu";
"svc:node-red" [label="node-red
:1880
[authelia]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:node-red" [penwidth=1.4];
"dns:node-red.<domain>" [label="node-red.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:node-red.<domain>" -> "dynu";
"svc:passbolt-webapp" [label="passbolt-webapp", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:passbolt-webapp" [penwidth=1.4];
"dns:passbolt.<domain>" [label="passbolt.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:passbolt.<domain>" -> "dynu";
"svc:portainer" [label="portainer
:9000
[TLS]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:portainer" [penwidth=1.4];
"dns:portainer.<domain>" [label="portainer.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:portainer.<domain>" -> "dynu";
"svc:prometheus" [label="prometheus
:9090
[authelia]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:prometheus" [penwidth=1.4];
"dns:prometheus.<domain>" [label="prometheus.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:prometheus.<domain>" -> "dynu";
"svc:searxng-webapp" [label="searxng-webapp", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:searxng-webapp" [penwidth=1.4];
"dns:searxng.<domain>" [label="searxng.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:searxng.<domain>" -> "dynu";
"svc:traefik" [label="traefik
[authelia]", shape=box, fillcolor="#dcfce7"];
"svc:traefik" -> "svc:traefik" [penwidth=1.4];
"dns:traefik.<domain>" [label="traefik.<domain>", shape=note, fillcolor="#fef3c7"];
"dns:traefik.<domain>" -> "dynu";
{ rank=same; "dns:auth.<domain>"; "dns:familytree.<domain>"; "dns:gitea.<domain>"; "dns:gotify.<domain>"; "dns:grafana.<domain>"; "dns:influxdb.<domain>"; "dns:monitor-kuma.<domain>"; "dns:mtls-bridge.<domain>"; "dns:nextcloud.<domain>"; "dns:node-red.<domain>"; "dns:passbolt.<domain>"; "dns:portainer.<domain>"; "dns:prometheus.<domain>"; "dns:searxng.<domain>"; "dns:traefik.<domain>"; }
subgraph "cluster_networks" {
label="Docker backend networks"; style="rounded,dashed"; color="#d1d5db";
"net:gramps" [label="gramps", shape=ellipse, fillcolor="#f8fafc"];
"net:monitor" [label="monitor", shape=ellipse, fillcolor="#f8fafc"];
"net:nextcloud" [label="nextcloud", shape=ellipse, fillcolor="#f8fafc"];
"net:passbolt" [label="passbolt", shape=ellipse, fillcolor="#f8fafc"];
"net:traefik" [label="traefik", shape=ellipse, fillcolor="#f8fafc"];
}
"svc:authelia" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:gitea" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:gotify" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:grafana" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:grafana" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:grampsweb" -> "net:gramps" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:grampsweb" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:influxdb" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:influxdb" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:monitor-kuma" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:monitor-kuma" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:mtls-bridge" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:mtls-bridge" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:nextcloud-webapp" -> "net:nextcloud" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:nextcloud-webapp" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:node-red" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:node-red" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:passbolt-webapp" -> "net:passbolt" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:passbolt-webapp" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:portainer" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:prometheus" -> "net:monitor" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:prometheus" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:searxng-webapp" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
"svc:traefik" -> "net:traefik" [style=dashed, color="#94a3b8", arrowsize=0.7];
}
+611
View File
@@ -0,0 +1,611 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: DockerTraefikDynu Pages: 1 -->
<svg width="1113pt" height="1536pt"
viewBox="0.00 0.00 1113.39 1536.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1532)">
<title>DockerTraefikDynu</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1532 1109.39,-1532 1109.39,4 -4,4"/>
<g id="clust2" class="cluster">
<title>cluster_networks</title>
<path fill="none" stroke="#d1d5db" stroke-dasharray="5,2" d="M922,-650C922,-650 1093.39,-650 1093.39,-650 1099.39,-650 1105.39,-656 1105.39,-662 1105.39,-662 1105.39,-1117 1105.39,-1117 1105.39,-1123 1099.39,-1129 1093.39,-1129 1093.39,-1129 922,-1129 922,-1129 916,-1129 910,-1123 910,-1117 910,-1117 910,-662 910,-662 910,-656 916,-650 922,-650"/>
<text text-anchor="middle" x="1007.69" y="-1113.8" font-family="Helvetica,sans-Serif" font-size="14.00">Docker backend networks</text>
</g>
<!-- dynu -->
<g id="node1" class="node">
<title>dynu</title>
<path fill="#fde68a" stroke="black" d="M374,-789C374,-789 282,-789 282,-789 276,-789 270,-783 270,-777 270,-777 270,-765 270,-765 270,-759 276,-753 282,-753 282,-753 374,-753 374,-753 380,-753 386,-759 386,-765 386,-765 386,-777 386,-777 386,-783 380,-789 374,-789"/>
<text text-anchor="middle" x="328" y="-768.2" font-family="Helvetica,sans-Serif" font-size="11.00">Dynu / Public DNS</text>
</g>
<!-- svc:traefik -->
<g id="node2" class="node">
<title>svc:traefik</title>
<path fill="#dcfce7" stroke="black" d="M559,-789C559,-789 513,-789 513,-789 507,-789 501,-783 501,-777 501,-777 501,-765 501,-765 501,-759 507,-753 513,-753 513,-753 559,-753 559,-753 565,-753 571,-759 571,-765 571,-765 571,-777 571,-777 571,-783 565,-789 559,-789"/>
<text text-anchor="middle" x="536" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik</text>
<text text-anchor="middle" x="536" y="-762.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- dynu&#45;&gt;svc:traefik -->
<g id="edge1" class="edge">
<title>dynu&#45;&gt;svc:traefik</title>
<path fill="none" stroke="#334155" stroke-width="1.6" d="M386.11,-771C419.13,-771 460.06,-771 490.64,-771"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.6" points="491,-774.5 501,-771 491,-767.5 491,-774.5"/>
</g>
<!-- svc:traefik&#45;&gt;svc:traefik -->
<g id="edge30" class="edge">
<title>svc:traefik&#45;&gt;svc:traefik</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M511.86,-789.35C485.91,-817.82 493.95,-854 536,-854 574.1,-854 584.29,-824.29 566.55,-797.52"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="569.07,-795.06 560.14,-789.35 563.55,-799.38 569.07,-795.06"/>
</g>
<!-- svc:authelia -->
<g id="node3" class="node">
<title>svc:authelia</title>
<path fill="#dcfce7" stroke="black" d="M763,-789C763,-789 726,-789 726,-789 720,-789 714,-783 714,-777 714,-777 714,-765 714,-765 714,-759 720,-753 726,-753 726,-753 763,-753 763,-753 769,-753 775,-759 775,-765 775,-765 775,-777 775,-777 775,-783 769,-789 763,-789"/>
<text text-anchor="middle" x="744.5" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">authelia</text>
<text text-anchor="middle" x="744.5" y="-762.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:authelia -->
<g id="edge2" class="edge">
<title>svc:traefik&#45;&gt;svc:authelia</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-771C607.43,-771 664.91,-771 703.39,-771"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="703.71,-774.5 713.71,-771 703.71,-767.5 703.71,-774.5"/>
</g>
<!-- svc:gitea -->
<g id="node5" class="node">
<title>svc:gitea</title>
<path fill="#dcfce7" stroke="black" d="M759.5,-688C759.5,-688 729.5,-688 729.5,-688 723.5,-688 717.5,-682 717.5,-676 717.5,-676 717.5,-656 717.5,-656 717.5,-650 723.5,-644 729.5,-644 729.5,-644 759.5,-644 759.5,-644 765.5,-644 771.5,-650 771.5,-656 771.5,-656 771.5,-676 771.5,-676 771.5,-682 765.5,-688 759.5,-688"/>
<text text-anchor="middle" x="744.5" y="-675.2" font-family="Helvetica,sans-Serif" font-size="11.00">gitea</text>
<text text-anchor="middle" x="744.5" y="-663.2" font-family="Helvetica,sans-Serif" font-size="11.00">:3000</text>
<text text-anchor="middle" x="744.5" y="-651.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:gitea -->
<g id="edge4" class="edge">
<title>svc:traefik&#45;&gt;svc:gitea</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-753.66C608.91,-734.44 669.61,-703.57 707.98,-684.06"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="709.89,-687.01 717.22,-679.36 706.72,-680.77 709.89,-687.01"/>
</g>
<!-- svc:gotify -->
<g id="node7" class="node">
<title>svc:gotify</title>
<path fill="#dcfce7" stroke="black" d="M759.5,-579C759.5,-579 729.5,-579 729.5,-579 723.5,-579 717.5,-573 717.5,-567 717.5,-567 717.5,-555 717.5,-555 717.5,-549 723.5,-543 729.5,-543 729.5,-543 759.5,-543 759.5,-543 765.5,-543 771.5,-549 771.5,-555 771.5,-555 771.5,-567 771.5,-567 771.5,-573 765.5,-579 759.5,-579"/>
<text text-anchor="middle" x="744.5" y="-564.2" font-family="Helvetica,sans-Serif" font-size="11.00">gotify</text>
<text text-anchor="middle" x="744.5" y="-552.2" font-family="Helvetica,sans-Serif" font-size="11.00">:80</text>
</g>
<!-- svc:traefik&#45;&gt;svc:gotify -->
<g id="edge6" class="edge">
<title>svc:traefik&#45;&gt;svc:gotify</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M553.8,-752.96C592.77,-711.11 686,-611 686,-611 686,-611 700.43,-598.45 714.83,-585.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-588.25 722.74,-579.05 712.9,-582.97 717.49,-588.25"/>
</g>
<!-- svc:grafana -->
<g id="node9" class="node">
<title>svc:grafana</title>
<path fill="#dcfce7" stroke="black" d="M762,-999C762,-999 727,-999 727,-999 721,-999 715,-993 715,-987 715,-987 715,-975 715,-975 715,-969 721,-963 727,-963 727,-963 762,-963 762,-963 768,-963 774,-969 774,-975 774,-975 774,-987 774,-987 774,-993 768,-999 762,-999"/>
<text text-anchor="middle" x="744.5" y="-984.2" font-family="Helvetica,sans-Serif" font-size="11.00">grafana</text>
<text text-anchor="middle" x="744.5" y="-972.2" font-family="Helvetica,sans-Serif" font-size="11.00">:3000</text>
</g>
<!-- svc:traefik&#45;&gt;svc:grafana -->
<g id="edge8" class="edge">
<title>svc:traefik&#45;&gt;svc:grafana</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M553.8,-789.04C592.77,-830.89 686,-931 686,-931 686,-931 700.43,-943.55 714.83,-956.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-959.03 722.74,-962.95 717.49,-953.75 712.9,-959.03"/>
</g>
<!-- svc:grampsweb -->
<g id="node11" class="node">
<title>svc:grampsweb</title>
<path fill="#dcfce7" stroke="black" d="M772.5,-1528C772.5,-1528 716.5,-1528 716.5,-1528 710.5,-1528 704.5,-1522 704.5,-1516 704.5,-1516 704.5,-1504 704.5,-1504 704.5,-1498 710.5,-1492 716.5,-1492 716.5,-1492 772.5,-1492 772.5,-1492 778.5,-1492 784.5,-1498 784.5,-1504 784.5,-1504 784.5,-1516 784.5,-1516 784.5,-1522 778.5,-1528 772.5,-1528"/>
<text text-anchor="middle" x="744.5" y="-1507.2" font-family="Helvetica,sans-Serif" font-size="11.00">grampsweb</text>
</g>
<!-- svc:traefik&#45;&gt;svc:grampsweb -->
<g id="edge10" class="edge">
<title>svc:traefik&#45;&gt;svc:grampsweb</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M540.91,-789.07C564.42,-897.78 686,-1460 686,-1460 686,-1460 700.43,-1472.55 714.83,-1485.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-1488.03 722.74,-1491.95 717.49,-1482.75 712.9,-1488.03"/>
</g>
<!-- svc:influxdb -->
<g id="node13" class="node">
<title>svc:influxdb</title>
<path fill="#dcfce7" stroke="black" d="M767.5,-898C767.5,-898 721.5,-898 721.5,-898 715.5,-898 709.5,-892 709.5,-886 709.5,-886 709.5,-866 709.5,-866 709.5,-860 715.5,-854 721.5,-854 721.5,-854 767.5,-854 767.5,-854 773.5,-854 779.5,-860 779.5,-866 779.5,-866 779.5,-886 779.5,-886 779.5,-892 773.5,-898 767.5,-898"/>
<text text-anchor="middle" x="744.5" y="-885.2" font-family="Helvetica,sans-Serif" font-size="11.00">influxdb</text>
<text text-anchor="middle" x="744.5" y="-873.2" font-family="Helvetica,sans-Serif" font-size="11.00">:8086</text>
<text text-anchor="middle" x="744.5" y="-861.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:influxdb -->
<g id="edge12" class="edge">
<title>svc:traefik&#45;&gt;svc:influxdb</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-788.34C606.37,-806.27 661.57,-834.34 699.97,-853.87"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="698.85,-857.22 709.35,-858.64 702.02,-850.98 698.85,-857.22"/>
</g>
<!-- svc:monitor&#45;kuma -->
<g id="node15" class="node">
<title>svc:monitor&#45;kuma</title>
<path fill="#dcfce7" stroke="black" d="M778.5,-1427C778.5,-1427 710.5,-1427 710.5,-1427 704.5,-1427 698.5,-1421 698.5,-1415 698.5,-1415 698.5,-1403 698.5,-1403 698.5,-1397 704.5,-1391 710.5,-1391 710.5,-1391 778.5,-1391 778.5,-1391 784.5,-1391 790.5,-1397 790.5,-1403 790.5,-1403 790.5,-1415 790.5,-1415 790.5,-1421 784.5,-1427 778.5,-1427"/>
<text text-anchor="middle" x="744.5" y="-1412.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor&#45;kuma</text>
<text text-anchor="middle" x="744.5" y="-1400.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:monitor&#45;kuma -->
<g id="edge14" class="edge">
<title>svc:traefik&#45;&gt;svc:monitor&#45;kuma</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M541.62,-789.24C566.77,-888.49 686,-1359 686,-1359 686,-1359 700.43,-1371.55 714.83,-1384.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-1387.03 722.74,-1390.95 717.49,-1381.75 712.9,-1387.03"/>
</g>
<!-- svc:mtls&#45;bridge -->
<g id="node17" class="node">
<title>svc:mtls&#45;bridge</title>
<path fill="#dcfce7" stroke="black" d="M772,-1326C772,-1326 717,-1326 717,-1326 711,-1326 705,-1320 705,-1314 705,-1314 705,-1294 705,-1294 705,-1288 711,-1282 717,-1282 717,-1282 772,-1282 772,-1282 778,-1282 784,-1288 784,-1294 784,-1294 784,-1314 784,-1314 784,-1320 778,-1326 772,-1326"/>
<text text-anchor="middle" x="744.5" y="-1313.2" font-family="Helvetica,sans-Serif" font-size="11.00">mtls&#45;bridge</text>
<text text-anchor="middle" x="744.5" y="-1301.2" font-family="Helvetica,sans-Serif" font-size="11.00">:8080</text>
<text text-anchor="middle" x="744.5" y="-1289.2" font-family="Helvetica,sans-Serif" font-size="11.00">[mTLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:mtls&#45;bridge -->
<g id="edge16" class="edge">
<title>svc:traefik&#45;&gt;svc:mtls&#45;bridge</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M542.66,-789.19C569.88,-876.69 686,-1250 686,-1250 686,-1250 698.77,-1262 712.28,-1274.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1277.43 719.78,-1281.72 714.89,-1272.33 710.1,-1277.43"/>
</g>
<!-- svc:nextcloud&#45;webapp -->
<g id="node19" class="node">
<title>svc:nextcloud&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M791,-137C791,-137 698,-137 698,-137 692,-137 686,-131 686,-125 686,-125 686,-113 686,-113 686,-107 692,-101 698,-101 698,-101 791,-101 791,-101 797,-101 803,-107 803,-113 803,-113 803,-125 803,-125 803,-131 797,-137 791,-137"/>
<text text-anchor="middle" x="744.5" y="-116.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:nextcloud&#45;webapp -->
<g id="edge18" class="edge">
<title>svc:traefik&#45;&gt;svc:nextcloud&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M541.62,-752.85C566.77,-654.11 686,-186 686,-186 686,-186 704.96,-163.91 721.1,-145.1"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="723.93,-147.18 727.79,-137.31 718.62,-142.62 723.93,-147.18"/>
</g>
<!-- svc:node&#45;red -->
<g id="node21" class="node">
<title>svc:node&#45;red</title>
<path fill="#dcfce7" stroke="black" d="M767.5,-1217C767.5,-1217 721.5,-1217 721.5,-1217 715.5,-1217 709.5,-1211 709.5,-1205 709.5,-1205 709.5,-1185 709.5,-1185 709.5,-1179 715.5,-1173 721.5,-1173 721.5,-1173 767.5,-1173 767.5,-1173 773.5,-1173 779.5,-1179 779.5,-1185 779.5,-1185 779.5,-1205 779.5,-1205 779.5,-1211 773.5,-1217 767.5,-1217"/>
<text text-anchor="middle" x="744.5" y="-1204.2" font-family="Helvetica,sans-Serif" font-size="11.00">node&#45;red</text>
<text text-anchor="middle" x="744.5" y="-1192.2" font-family="Helvetica,sans-Serif" font-size="11.00">:1880</text>
<text text-anchor="middle" x="744.5" y="-1180.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:node&#45;red -->
<g id="edge20" class="edge">
<title>svc:traefik&#45;&gt;svc:node&#45;red</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M544.29,-789.1C574.2,-863.38 686,-1141 686,-1141 686,-1141 698.77,-1153 712.28,-1165.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1168.43 719.78,-1172.72 714.89,-1163.33 710.1,-1168.43"/>
</g>
<!-- svc:passbolt&#45;webapp -->
<g id="node23" class="node">
<title>svc:passbolt&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M787.5,-36C787.5,-36 701.5,-36 701.5,-36 695.5,-36 689.5,-30 689.5,-24 689.5,-24 689.5,-12 689.5,-12 689.5,-6 695.5,0 701.5,0 701.5,0 787.5,0 787.5,0 793.5,0 799.5,-6 799.5,-12 799.5,-12 799.5,-24 799.5,-24 799.5,-30 793.5,-36 787.5,-36"/>
<text text-anchor="middle" x="744.5" y="-15.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:passbolt&#45;webapp -->
<g id="edge22" class="edge">
<title>svc:traefik&#45;&gt;svc:passbolt&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M540.83,-752.92C564.15,-642.88 686,-68 686,-68 686,-68 700.43,-55.45 714.83,-42.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-45.25 722.74,-36.05 712.9,-39.97 717.49,-45.25"/>
</g>
<!-- svc:portainer -->
<g id="node25" class="node">
<title>svc:portainer</title>
<path fill="#dcfce7" stroke="black" d="M766,-478C766,-478 723,-478 723,-478 717,-478 711,-472 711,-466 711,-466 711,-446 711,-446 711,-440 717,-434 723,-434 723,-434 766,-434 766,-434 772,-434 778,-440 778,-446 778,-446 778,-466 778,-466 778,-472 772,-478 766,-478"/>
<text text-anchor="middle" x="744.5" y="-465.2" font-family="Helvetica,sans-Serif" font-size="11.00">portainer</text>
<text text-anchor="middle" x="744.5" y="-453.2" font-family="Helvetica,sans-Serif" font-size="11.00">:9000</text>
<text text-anchor="middle" x="744.5" y="-441.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:portainer -->
<g id="edge24" class="edge">
<title>svc:traefik&#45;&gt;svc:portainer</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M547.48,-752.65C581.39,-693.24 686,-510 686,-510 686,-510 698.77,-498 712.28,-485.32"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="714.89,-487.67 719.78,-478.28 710.1,-482.57 714.89,-487.67"/>
</g>
<!-- svc:prometheus -->
<g id="node27" class="node">
<title>svc:prometheus</title>
<path fill="#dcfce7" stroke="black" d="M774,-1108C774,-1108 715,-1108 715,-1108 709,-1108 703,-1102 703,-1096 703,-1096 703,-1076 703,-1076 703,-1070 709,-1064 715,-1064 715,-1064 774,-1064 774,-1064 780,-1064 786,-1070 786,-1076 786,-1076 786,-1096 786,-1096 786,-1102 780,-1108 774,-1108"/>
<text text-anchor="middle" x="744.5" y="-1095.2" font-family="Helvetica,sans-Serif" font-size="11.00">prometheus</text>
<text text-anchor="middle" x="744.5" y="-1083.2" font-family="Helvetica,sans-Serif" font-size="11.00">:9090</text>
<text text-anchor="middle" x="744.5" y="-1071.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:prometheus -->
<g id="edge26" class="edge">
<title>svc:traefik&#45;&gt;svc:prometheus</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M547.48,-789.35C581.39,-848.76 686,-1032 686,-1032 686,-1032 698.77,-1044 712.28,-1056.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1059.43 719.78,-1063.72 714.89,-1054.33 710.1,-1059.43"/>
</g>
<!-- svc:searxng&#45;webapp -->
<g id="node29" class="node">
<title>svc:searxng&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M786,-369C786,-369 703,-369 703,-369 697,-369 691,-363 691,-357 691,-357 691,-345 691,-345 691,-339 697,-333 703,-333 703,-333 786,-333 786,-333 792,-333 798,-339 798,-345 798,-345 798,-357 798,-357 798,-363 792,-369 786,-369"/>
<text text-anchor="middle" x="744.5" y="-348.2" font-family="Helvetica,sans-Serif" font-size="11.00">searxng&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:searxng&#45;webapp -->
<g id="edge28" class="edge">
<title>svc:traefik&#45;&gt;svc:searxng&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M544.29,-752.9C574.2,-678.62 686,-401 686,-401 686,-401 700.43,-388.45 714.83,-375.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-378.25 722.74,-369.05 712.9,-372.97 717.49,-378.25"/>
</g>
<!-- net:traefik -->
<g id="node36" class="node">
<title>net:traefik</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-878" rx="31.04" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-875.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik</text>
</g>
<!-- svc:traefik&#45;&gt;net:traefik -->
<g id="edge55" class="edge">
<title>svc:traefik&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M542.75,-752.82C570.13,-666.27 686,-300 686,-300 686,-300 803,-300 803,-300 803,-300 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- svc:authelia&#45;&gt;net:traefik -->
<g id="edge32" class="edge">
<title>svc:authelia&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M775.21,-783.17C824.86,-803.51 924.25,-844.23 975.12,-865.06"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="974.26,-867.36 981.67,-867.75 976.12,-862.83 974.26,-867.36"/>
</g>
<!-- dns:auth.&lt;domain&gt; -->
<g id="node4" class="node">
<title>dns:auth.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="123.5,-1496 25.5,-1496 25.5,-1460 129.5,-1460 129.5,-1490 123.5,-1496"/>
<polyline fill="none" stroke="black" points="123.5,-1496 123.5,-1490 "/>
<polyline fill="none" stroke="black" points="129.5,-1490 123.5,-1490 "/>
<text text-anchor="middle" x="77.5" y="-1475.2" font-family="Helvetica,sans-Serif" font-size="11.00">auth.&lt;domain&gt;</text>
</g>
<!-- dns:auth.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge3" class="edge">
<title>dns:auth.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1459.95C128.03,-1445.63 155,-1428 155,-1428 155,-1428 286.65,-925.11 319.6,-799.28"/>
<polygon fill="#334155" stroke="#334155" points="323.07,-799.82 322.22,-789.26 316.3,-798.05 323.07,-799.82"/>
</g>
<!-- svc:gitea&#45;&gt;net:traefik -->
<g id="edge33" class="edge">
<title>svc:gitea&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M768.56,-688.05C784.55,-703.36 803,-721 803,-721 803,-721 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:gitea.&lt;domain&gt; -->
<g id="node6" class="node">
<title>dns:gitea.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="125,-1395 24,-1395 24,-1359 131,-1359 131,-1389 125,-1395"/>
<polyline fill="none" stroke="black" points="125,-1395 125,-1389 "/>
<polyline fill="none" stroke="black" points="131,-1389 125,-1389 "/>
<text text-anchor="middle" x="77.5" y="-1374.2" font-family="Helvetica,sans-Serif" font-size="11.00">gitea.&lt;domain&gt;</text>
</g>
<!-- dns:gitea.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge5" class="edge">
<title>dns:gitea.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1358.95C128.03,-1344.63 155,-1327 155,-1327 155,-1327 283.58,-911.36 318.4,-798.82"/>
<polygon fill="#334155" stroke="#334155" points="321.76,-799.77 321.38,-789.18 315.08,-797.7 321.76,-799.77"/>
</g>
<!-- svc:gotify&#45;&gt;net:traefik -->
<g id="edge34" class="edge">
<title>svc:gotify&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-579.05C782.73,-593.37 803,-611 803,-611 803,-611 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:gotify.&lt;domain&gt; -->
<g id="node8" class="node">
<title>dns:gotify.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="126,-1294 23,-1294 23,-1258 132,-1258 132,-1288 126,-1294"/>
<polyline fill="none" stroke="black" points="126,-1294 126,-1288 "/>
<polyline fill="none" stroke="black" points="132,-1288 126,-1288 "/>
<text text-anchor="middle" x="77.5" y="-1273.2" font-family="Helvetica,sans-Serif" font-size="11.00">gotify.&lt;domain&gt;</text>
</g>
<!-- dns:gotify.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge7" class="edge">
<title>dns:gotify.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1257.95C128.03,-1243.63 155,-1226 155,-1226 155,-1226 279.21,-897.41 316.53,-798.71"/>
<polygon fill="#334155" stroke="#334155" points="319.89,-799.71 320.15,-789.12 313.34,-797.23 319.89,-799.71"/>
</g>
<!-- net:monitor -->
<g id="node33" class="node">
<title>net:monitor</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-979" rx="35.46" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-976.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor</text>
</g>
<!-- svc:grafana&#45;&gt;net:monitor -->
<g id="edge35" class="edge">
<title>svc:grafana&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M774.2,-980.78C820.4,-980.43 911.47,-979.73 964.89,-979.32"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="965.19,-981.77 972.17,-979.26 965.15,-976.87 965.19,-981.77"/>
</g>
<!-- svc:grafana&#45;&gt;net:traefik -->
<g id="edge36" class="edge">
<title>svc:grafana&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M774.2,-969.68C823.41,-950.28 923.53,-910.8 974.83,-890.57"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="975.82,-892.81 981.44,-887.96 974.03,-888.25 975.82,-892.81"/>
</g>
<!-- dns:grafana.&lt;domain&gt; -->
<g id="node10" class="node">
<title>dns:grafana.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="132,-1193 17,-1193 17,-1157 138,-1157 138,-1187 132,-1193"/>
<polyline fill="none" stroke="black" points="132,-1193 132,-1187 "/>
<polyline fill="none" stroke="black" points="138,-1187 132,-1187 "/>
<text text-anchor="middle" x="77.5" y="-1172.2" font-family="Helvetica,sans-Serif" font-size="11.00">grafana.&lt;domain&gt;</text>
</g>
<!-- dns:grafana.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge9" class="edge">
<title>dns:grafana.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1156.95C128.03,-1142.63 155,-1125 155,-1125 155,-1125 273.61,-880.89 313.84,-798.09"/>
<polygon fill="#334155" stroke="#334155" points="317.01,-799.57 318.23,-789.04 310.72,-796.51 317.01,-799.57"/>
</g>
<!-- net:gramps -->
<g id="node32" class="node">
<title>net:gramps</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-1080" rx="34.76" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-1077.2" font-family="Helvetica,sans-Serif" font-size="11.00">gramps</text>
</g>
<!-- svc:grampsweb&#45;&gt;net:gramps -->
<g id="edge37" class="edge">
<title>svc:grampsweb&#45;&gt;net:gramps</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1491.95C782.73,-1477.63 803,-1460 803,-1460 803,-1460 949.23,-1187.21 993.89,-1103.88"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="996.27,-1104.64 997.41,-1097.32 991.95,-1102.33 996.27,-1104.64"/>
</g>
<!-- svc:grampsweb&#45;&gt;net:traefik -->
<g id="edge38" class="edge">
<title>svc:grampsweb&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1491.95C782.73,-1477.63 803,-1460 803,-1460 803,-1460 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:familytree.&lt;domain&gt; -->
<g id="node12" class="node">
<title>dns:familytree.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="139,-1092 10,-1092 10,-1056 145,-1056 145,-1086 139,-1092"/>
<polyline fill="none" stroke="black" points="139,-1092 139,-1086 "/>
<polyline fill="none" stroke="black" points="145,-1086 139,-1086 "/>
<text text-anchor="middle" x="77.5" y="-1071.2" font-family="Helvetica,sans-Serif" font-size="11.00">familytree.&lt;domain&gt;</text>
</g>
<!-- dns:familytree.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge11" class="edge">
<title>dns:familytree.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1055.95C128.03,-1041.63 155,-1024 155,-1024 155,-1024 264.64,-862.73 308.84,-797.71"/>
<polygon fill="#334155" stroke="#334155" points="311.9,-799.43 314.63,-789.2 306.11,-795.5 311.9,-799.43"/>
</g>
<!-- svc:influxdb&#45;&gt;net:monitor -->
<g id="edge39" class="edge">
<title>svc:influxdb&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M779.65,-889.47C829.7,-909.2 922.46,-945.78 972.53,-965.53"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="971.89,-967.91 979.3,-968.2 973.69,-963.35 971.89,-967.91"/>
</g>
<!-- svc:influxdb&#45;&gt;net:traefik -->
<g id="edge40" class="edge">
<title>svc:influxdb&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M779.65,-876.26C828.54,-876.64 918.2,-877.32 968.99,-877.71"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="969.17,-880.16 976.19,-877.77 969.21,-875.26 969.17,-880.16"/>
</g>
<!-- dns:influxdb.&lt;domain&gt; -->
<g id="node14" class="node">
<title>dns:influxdb.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="132.5,-991 16.5,-991 16.5,-955 138.5,-955 138.5,-985 132.5,-991"/>
<polyline fill="none" stroke="black" points="132.5,-991 132.5,-985 "/>
<polyline fill="none" stroke="black" points="138.5,-985 132.5,-985 "/>
<text text-anchor="middle" x="77.5" y="-970.2" font-family="Helvetica,sans-Serif" font-size="11.00">influxdb.&lt;domain&gt;</text>
</g>
<!-- dns:influxdb.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge13" class="edge">
<title>dns:influxdb.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-954.95C128.03,-940.63 155,-923 155,-923 155,-923 250.14,-838.92 298.91,-795.83"/>
<polygon fill="#334155" stroke="#334155" points="301.42,-798.28 306.59,-789.03 296.78,-793.03 301.42,-798.28"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:monitor -->
<g id="edge41" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1390.95C782.73,-1376.63 803,-1359 803,-1359 803,-1359 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:traefik -->
<g id="edge42" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1390.95C782.73,-1376.63 803,-1359 803,-1359 803,-1359 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:monitor&#45;kuma.&lt;domain&gt; -->
<g id="node16" class="node">
<title>dns:monitor&#45;kuma.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="149,-890 0,-890 0,-854 155,-854 155,-884 149,-890"/>
<polyline fill="none" stroke="black" points="149,-890 149,-884 "/>
<polyline fill="none" stroke="black" points="155,-884 149,-884 "/>
<text text-anchor="middle" x="77.5" y="-869.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor&#45;kuma.&lt;domain&gt;</text>
</g>
<!-- dns:monitor&#45;kuma.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge15" class="edge">
<title>dns:monitor&#45;kuma.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M122.93,-853.94C165.02,-836.83 228.32,-811.11 273.25,-792.85"/>
<polygon fill="#334155" stroke="#334155" points="274.59,-796.08 282.54,-789.07 271.96,-789.59 274.59,-796.08"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:monitor -->
<g id="edge43" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1281.72C785.06,-1266.85 803,-1250 803,-1250 803,-1250 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:traefik -->
<g id="edge44" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1281.72C785.06,-1266.85 803,-1250 803,-1250 803,-1250 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:mtls&#45;bridge.&lt;domain&gt; -->
<g id="node18" class="node">
<title>dns:mtls&#45;bridge.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="142,-789 7,-789 7,-753 148,-753 148,-783 142,-789"/>
<polyline fill="none" stroke="black" points="142,-789 142,-783 "/>
<polyline fill="none" stroke="black" points="148,-783 142,-783 "/>
<text text-anchor="middle" x="77.5" y="-768.2" font-family="Helvetica,sans-Serif" font-size="11.00">mtls&#45;bridge.&lt;domain&gt;</text>
</g>
<!-- dns:mtls&#45;bridge.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge17" class="edge">
<title>dns:mtls&#45;bridge.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M148.05,-771C182.94,-771 225.03,-771 259.61,-771"/>
<polygon fill="#334155" stroke="#334155" points="259.62,-774.5 269.62,-771 259.62,-767.5 259.62,-774.5"/>
</g>
<!-- net:nextcloud -->
<g id="node34" class="node">
<title>net:nextcloud</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-777" rx="42.89" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud</text>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud -->
<g id="edge45" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M761.21,-137.31C778.24,-157.15 803,-186 803,-186 803,-186 910,-727 910,-727 910,-727 945.48,-745.35 973.46,-759.81"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="972.67,-762.17 980.02,-763.2 974.92,-757.81 972.67,-762.17"/>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:traefik -->
<g id="edge46" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M761.21,-137.31C778.24,-157.15 803,-186 803,-186 803,-186 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:nextcloud.&lt;domain&gt; -->
<g id="node20" class="node">
<title>dns:nextcloud.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="138,-688 11,-688 11,-652 144,-652 144,-682 138,-688"/>
<polyline fill="none" stroke="black" points="138,-688 138,-682 "/>
<polyline fill="none" stroke="black" points="144,-682 138,-682 "/>
<text text-anchor="middle" x="77.5" y="-667.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud.&lt;domain&gt;</text>
</g>
<!-- dns:nextcloud.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge19" class="edge">
<title>dns:nextcloud.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M122.93,-688.06C165.02,-705.17 228.32,-730.89 273.25,-749.15"/>
<polygon fill="#334155" stroke="#334155" points="271.96,-752.41 282.54,-752.93 274.59,-745.92 271.96,-752.41"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:monitor -->
<g id="edge47" class="edge">
<title>svc:node&#45;red&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1172.72C785.06,-1157.85 803,-1141 803,-1141 803,-1141 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:traefik -->
<g id="edge48" class="edge">
<title>svc:node&#45;red&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1172.72C785.06,-1157.85 803,-1141 803,-1141 803,-1141 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:node&#45;red.&lt;domain&gt; -->
<g id="node22" class="node">
<title>dns:node&#45;red.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="135.5,-587 13.5,-587 13.5,-551 141.5,-551 141.5,-581 135.5,-587"/>
<polyline fill="none" stroke="black" points="135.5,-587 135.5,-581 "/>
<polyline fill="none" stroke="black" points="141.5,-581 135.5,-581 "/>
<text text-anchor="middle" x="77.5" y="-566.2" font-family="Helvetica,sans-Serif" font-size="11.00">node&#45;red.&lt;domain&gt;</text>
</g>
<!-- dns:node&#45;red.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge21" class="edge">
<title>dns:node&#45;red.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-587.05C128.03,-601.37 155,-619 155,-619 155,-619 250.14,-703.08 298.91,-746.17"/>
<polygon fill="#334155" stroke="#334155" points="296.78,-748.97 306.59,-752.97 301.42,-743.72 296.78,-748.97"/>
</g>
<!-- net:passbolt -->
<g id="node35" class="node">
<title>net:passbolt</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-676" rx="37.77" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-673.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt</text>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:passbolt -->
<g id="edge49" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:passbolt</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-36.05C782.73,-50.37 803,-68 803,-68 803,-68 960.4,-537.83 998.48,-651.47"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="996.16,-652.27 1000.71,-658.13 1000.81,-650.72 996.16,-652.27"/>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:traefik -->
<g id="edge50" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-36.05C782.73,-50.37 803,-68 803,-68 803,-68 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:passbolt.&lt;domain&gt; -->
<g id="node24" class="node">
<title>dns:passbolt.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="134,-486 15,-486 15,-450 140,-450 140,-480 134,-486"/>
<polyline fill="none" stroke="black" points="134,-486 134,-480 "/>
<polyline fill="none" stroke="black" points="140,-480 134,-480 "/>
<text text-anchor="middle" x="77.5" y="-465.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt.&lt;domain&gt;</text>
</g>
<!-- dns:passbolt.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge23" class="edge">
<title>dns:passbolt.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-486.05C128.03,-500.37 155,-518 155,-518 155,-518 264.64,-679.27 308.84,-744.29"/>
<polygon fill="#334155" stroke="#334155" points="306.11,-746.5 314.63,-752.8 311.9,-742.57 306.11,-746.5"/>
</g>
<!-- svc:portainer&#45;&gt;net:traefik -->
<g id="edge51" class="edge">
<title>svc:portainer&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-478.28C785.06,-493.15 803,-510 803,-510 803,-510 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:portainer.&lt;domain&gt; -->
<g id="node26" class="node">
<title>dns:portainer.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="135.5,-385 13.5,-385 13.5,-349 141.5,-349 141.5,-379 135.5,-385"/>
<polyline fill="none" stroke="black" points="135.5,-385 135.5,-379 "/>
<polyline fill="none" stroke="black" points="141.5,-379 135.5,-379 "/>
<text text-anchor="middle" x="77.5" y="-364.2" font-family="Helvetica,sans-Serif" font-size="11.00">portainer.&lt;domain&gt;</text>
</g>
<!-- dns:portainer.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge25" class="edge">
<title>dns:portainer.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-385.05C128.03,-399.37 155,-417 155,-417 155,-417 273.61,-661.11 313.84,-743.91"/>
<polygon fill="#334155" stroke="#334155" points="310.72,-745.49 318.23,-752.96 317.01,-742.43 310.72,-745.49"/>
</g>
<!-- svc:prometheus&#45;&gt;net:monitor -->
<g id="edge52" class="edge">
<title>svc:prometheus&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M786.03,-1069.4C837.48,-1048.32 925.31,-1012.34 973.18,-992.73"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="974.11,-995 979.66,-990.07 972.26,-990.46 974.11,-995"/>
</g>
<!-- svc:prometheus&#45;&gt;net:traefik -->
<g id="edge53" class="edge">
<title>svc:prometheus&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1063.72C785.06,-1048.85 803,-1032 803,-1032 803,-1032 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:prometheus.&lt;domain&gt; -->
<g id="node28" class="node">
<title>dns:prometheus.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="144,-284 5,-284 5,-248 150,-248 150,-278 144,-284"/>
<polyline fill="none" stroke="black" points="144,-284 144,-278 "/>
<polyline fill="none" stroke="black" points="150,-278 144,-278 "/>
<text text-anchor="middle" x="77.5" y="-263.2" font-family="Helvetica,sans-Serif" font-size="11.00">prometheus.&lt;domain&gt;</text>
</g>
<!-- dns:prometheus.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge27" class="edge">
<title>dns:prometheus.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-284.05C128.03,-298.37 155,-316 155,-316 155,-316 279.21,-644.59 316.53,-743.29"/>
<polygon fill="#334155" stroke="#334155" points="313.34,-744.77 320.15,-752.88 319.89,-742.29 313.34,-744.77"/>
</g>
<!-- svc:searxng&#45;webapp&#45;&gt;net:traefik -->
<g id="edge54" class="edge">
<title>svc:searxng&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-369.05C782.73,-383.37 803,-401 803,-401 803,-401 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:searxng.&lt;domain&gt; -->
<g id="node30" class="node">
<title>dns:searxng.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="133,-183 16,-183 16,-147 139,-147 139,-177 133,-183"/>
<polyline fill="none" stroke="black" points="133,-183 133,-177 "/>
<polyline fill="none" stroke="black" points="139,-177 133,-177 "/>
<text text-anchor="middle" x="77.5" y="-162.2" font-family="Helvetica,sans-Serif" font-size="11.00">searxng.&lt;domain&gt;</text>
</g>
<!-- dns:searxng.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge29" class="edge">
<title>dns:searxng.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-183.05C128.03,-197.37 155,-215 155,-215 155,-215 283.58,-630.64 318.4,-743.18"/>
<polygon fill="#334155" stroke="#334155" points="315.08,-744.3 321.38,-752.82 321.76,-742.23 315.08,-744.3"/>
</g>
<!-- dns:traefik.&lt;domain&gt; -->
<g id="node31" class="node">
<title>dns:traefik.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="128.5,-82 20.5,-82 20.5,-46 134.5,-46 134.5,-76 128.5,-82"/>
<polyline fill="none" stroke="black" points="128.5,-82 128.5,-76 "/>
<polyline fill="none" stroke="black" points="134.5,-76 128.5,-76 "/>
<text text-anchor="middle" x="77.5" y="-61.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik.&lt;domain&gt;</text>
</g>
<!-- dns:traefik.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge31" class="edge">
<title>dns:traefik.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-82.05C128.03,-96.37 155,-114 155,-114 155,-114 286.65,-616.89 319.6,-742.72"/>
<polygon fill="#334155" stroke="#334155" points="316.3,-743.95 322.22,-752.74 323.07,-742.18 316.3,-743.95"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 41 KiB

+4
View File
@@ -0,0 +1,4 @@
digraph PhysicalTopology {
graph [rankdir=LR, fontname="Helvetica", nodesep=1.0, ranksep=1.5];
"placeholder:inventory" [shape=note, style="filled", fillcolor="#fef3c7", label="Host inventory JSON not found.\nGenerate terraform inventory and rerun scripts/docs/generate-all.sh\n(--host-inventory <path>)."];
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: PhysicalTopology Pages: 1 -->
<svg width="514pt" height="61pt"
viewBox="0.00 0.00 514.00 61.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 57)">
<title>PhysicalTopology</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-57 510,-57 510,4 -4,4"/>
<!-- placeholder:inventory -->
<g id="node1" class="node">
<title>placeholder:inventory</title>
<polygon fill="#fef3c7" stroke="black" points="500,-53 0,-53 0,0 506,0 506,-47 500,-53"/>
<polyline fill="none" stroke="black" points="500,-53 500,-47 "/>
<polyline fill="none" stroke="black" points="506,-47 500,-47 "/>
<text text-anchor="middle" x="253" y="-37.8" font-family="Times,serif" font-size="14.00">Host inventory JSON not found.</text>
<text text-anchor="middle" x="253" y="-22.8" font-family="Times,serif" font-size="14.00">Generate terraform inventory and rerun scripts/docs/generate&#45;all.sh</text>
<text text-anchor="middle" x="253" y="-7.8" font-family="Times,serif" font-size="14.00">(&#45;&#45;host&#45;inventory &lt;path&gt;).</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+13
View File
@@ -0,0 +1,13 @@
# Docker Environment
This environment is orchestrated from central Docker Compose definitions committed in this repository.
Compose source files are rendered into a resolved configuration during docs generation, then summarized into generated markdown and diagrams.
Generated outputs:
- [Compose Inventory](generated/compose-inventory.md)
- [Resolved Compose Config](generated/docker-compose.resolved.yml)
- [Docker Compose Diagram](diagrams/docker-compose.svg)
Generated documentation is produced by CI from repository files only. Documentation generation does not start containers.
+139
View File
@@ -0,0 +1,139 @@
# Dynu DNS Read-Only Inventory
This repository includes a **read-only** Dynu DNS inventory workflow for `lan.ddnsgeek.com`.
> This integration is intentionally read-only. No Dynu mutations are permitted in this repo at this stage.
## Scope
- Fetch live DNS/domain data from Dynu using **GET requests only**.
- Correlate Dynu hostnames with Traefik `Host(...)` rules found in compose files.
- Generate local inventory artifacts for documentation.
## Safety Guard Rails
- Scripts fail unless `DYNU_READ_ONLY=true`.
- No Dynu write methods (`POST`, `PUT`, `PATCH`, `DELETE`) are implemented.
- No Terraform Dynu provider/resources/modules are introduced.
- No Ansible Dynu mutation tasks are introduced.
- API secrets are read from environment variables and are never logged.
## Correlation logic
`scripts/dynu/correlate_dynu_with_traefik.py` uses compose files as the source of truth and parses them as YAML.
It supports both common label formats:
- list style:
```yaml
labels:
- "traefik.http.routers.app.rule=Host(`app.lan.ddnsgeek.com`)"
```
- map style:
```yaml
labels:
traefik.http.routers.app.rule: "Host(`app.lan.ddnsgeek.com`)"
```
The parser extracts hostnames from router rules such as:
- `Host(`a`)`
- `Host("a")`
- `Host('a')`
- multi-host rules (comma-delimited)
- combined expressions such as `Host(...) && PathPrefix(...)`
## Route metadata in inventory
Each discovered hostname mapping includes:
- fqdn
- compose service name
- compose file path
- stack area (`apps`, `monitoring`, `core`)
- router label key(s)
- raw router rule
- `uses_tls`
- `tls_options`
- `middlewares`
- `uses_mtls`
- `uses_authelia`
mTLS is metadata only and **never blocks mapping**.
## Validation model
The generated JSON/Markdown include a top-level `validation` section with:
- `allowed_unmapped_hostnames`
- `unexpected_unmapped_hostnames`
- `duplicate_hostnames`
- `ambiguous_hostnames`
- `validation_ok`
Current policy:
- `edge.lan.ddnsgeek.com` is the only allowed unmapped DNS hostname.
- every other `*.lan.ddnsgeek.com` DNS hostname should map to a compose/Traefik-discovered service.
Optional strict mode:
- Set `DYNU_ENFORCE_VALIDATION=true` to make the correlate script exit non-zero when unexpected unmapped hostnames exist.
## Required Environment Variables
- `DYNU_API_KEY` (required for fetch)
- `DYNU_BASE_URL` (optional, defaults to `https://api.dynu.com`)
- `DYNU_READ_ONLY` (**must** be `true`)
Recommended local secrets file (not committed): `secrets/dynu.env`
```bash
DYNU_API_KEY=replace-with-real-api-key
DYNU_READ_ONLY=true
DYNU_BASE_URL=https://api.dynu.com
```
Notes:
- Keep values unquoted unless required by your shell.
- `scripts/dynu/build_dns_inventory.sh` will auto-load `secrets/dynu.env` when present.
## Commands
Run directly:
```bash
DYNU_READ_ONLY=true DYNU_API_KEY=... python3 scripts/dynu/fetch_dynu_dns.py
DYNU_READ_ONLY=true python3 scripts/dynu/correlate_dynu_with_traefik.py
```
Or run the wrapper:
```bash
scripts/dynu/build_dns_inventory.sh
```
## Artifacts
- `data/dns/dynu_live.json` (generated, untracked by default due to repo `data/` ignore)
- `data/dns/dynu_traefik_inventory.json` (generated, untracked by default)
- `docs/generated/dns-inventory.md` (generated documentation artifact)
Because `data/` is gitignored in this repository, JSON outputs are intentionally local-only unless ignore behavior changes in the future.
## Ansible Wrapper (Read-Only)
A syntax-safe wrapper playbook is provided at:
- `infrastructure/ansible/playbooks/dns-inventory.yml`
It only executes the local read-only scripts and does not call write-capable Dynu APIs.
## Not Managed Yet
Dynu DNS records are **not** managed by Terraform or Ansible in this repository at this stage.
No configuration in this repository sends Dynu mutation requests.
View File
+26
View File
@@ -0,0 +1,26 @@
default-network.yml
apps/gitea/docker-compose.yml
apps/gramps/docker-compose.yml
apps/nextcloud/docker-compose.yml
apps/passbolt/docker-compose.yml
apps/searxng/docker-compose.yml
apps/shift-recorder/docker-compose.yml
apps/stockfill/docker-compose.yml
core/authelia/docker-compose.yml
core/crowdsec/docker-compose.yml
core/error-pages/docker-compose.yml
core/test/docker-compose.yml
core/traefik/docker-compose.yml
monitoring/docker-exporter/docker-compose.yml
monitoring/docker-socket-proxy/docker-compose.yml
monitoring/gotify/docker-compose.yml
monitoring/grafana/docker-compose.yml
monitoring/influxdb/docker-compose.yml
monitoring/mtls-bridge/docker-compose.yml
monitoring/node-exporter/docker-compose.yml
monitoring/node-red/docker-compose.yml
monitoring/pihole-exporter/docker-compose.yml
monitoring/portainer/docker-compose.yml
monitoring/prometheus/docker-compose.yml
monitoring/telegraf/docker-compose.yml
monitoring/uptime-kuma/docker-compose.yml
+61
View File
@@ -0,0 +1,61 @@
# Docker Compose Inventory
Source fingerprint: `0fad36c3fed6`
## Summary
| Item | Count |
|---|---:|
| Services | 30 |
| Networks | 5 |
| Volumes | 0 |
## Services
| Service | Container | Image | Build | Profiles | Networks | Ports | Restart |
|---|---|---|---|---|---|---|---|
| authelia | authelia | authelia/authelia | /home/nixos/docker/core/authelia | core, all, authelia, traefik | traefik | | always |
| crowdsec | crowdsec | | /home/nixos/docker/core/crowdsec | core, all, crowdsec, traefik | traefik | | always |
| docker-socket-proxy | docker-socket-proxy | tecnativa/docker-socket-proxy:latest | | monitoring, all, docker-socket-proxy, core, traefik, prometheus | monitor, traefik | | unless-stopped |
| docker-update-exporter | docker-update-exporter | | /home/nixos/docker/monitoring/docker-exporter | monitoring, all, docker-exporter, prometheus | monitor | | unless-stopped |
| error-pages | error-pages | tarampampam/error-pages:3 | | core, all, error-pages, traefik | traefik | | always |
| gitea | gitea | gitea/gitea:latest | | apps, all, gitea | traefik | | always |
| gitea-runner | gitea-runner | gitea/act_runner:latest | | apps, all, gitea, ci | traefik | | always |
| gotify | gotify | gotify/server:latest | | monitoring, all, gotify | traefik | | always |
| grafana | grafana | grafana/grafana:latest | | monitoring, all, grafana | monitor, traefik | | unless-stopped |
| gramps-redis | gramps-redis | valkey/valkey:8-alpine | | apps, all, gramps | gramps | | always |
| grampsweb | gramps-web | ghcr.io/gramps-project/grampsweb:latest | | apps, all, gramps | gramps, traefik | | always |
| grampsweb_celery | gramps-web-celery | ghcr.io/gramps-project/grampsweb:latest | | apps, all, gramps | gramps | | always |
| influxdb | influxdb | influxdb:2.7 | | monitoring, all, influxdb, prometheus | monitor, traefik | | unless-stopped |
| monitor-kuma | monitor-kuma | louislam/uptime-kuma:2.1.1 | | monitoring, all, uptime-kuma | monitor, traefik | | always |
| mtls-bridge | mtls-bridge | | /home/nixos/docker/monitoring/mtls-bridge | monitoring, all, mtls-bridge | monitor, traefik | | unless-stopped |
| nextcloud-db | nextcloud-db | mariadb:11.4 | | apps, all, nextcloud | nextcloud | | always |
| nextcloud-redis | nextcloud-redis | redis | | apps, all, nextcloud | nextcloud | | always |
| nextcloud-webapp | nextcloud-webapp | | /home/nixos/docker/apps/nextcloud | apps, all, nextcloud | nextcloud, traefik | | always |
| node-exporter | node-exporter | prom/node-exporter:latest | | monitoring, all, node-exporter, prometheus | monitor | | unless-stopped |
| node-red | node-red | | /home/nixos/docker/monitoring/node-red | monitoring, all, node-red | monitor, traefik | | unless-stopped |
| passbolt-db | passbolt-db | mariadb:12 | | apps, all, passbolt | passbolt | | always |
| passbolt-webapp | passbolt-webapp | passbolt/passbolt:latest-ce | | apps, all, passbolt | passbolt, traefik | | always |
| pihole-exporter | pihole-exporter | ekofr/pihole-exporter:latest | | monitoring, all, pihole-exporter, prometheus | monitor | {'mode': 'ingress', 'target': 9617, 'published': '9617', 'protocol': 'tcp'} | unless-stopped |
| portainer | portainer | portainer/portainer-ce:latest | | monitoring, all, portainer | traefik | | unless-stopped |
| prometheus | prometheus | prom/prometheus:latest | | monitoring, all, prometheus | monitor, traefik | | unless-stopped |
| searxng-webapp | searxng-webapp | searxng/searxng | | apps, all, searxng | traefik | | always |
| shift-recorder-web | shift-recorder | | /home/nixos/docker/apps/shift-recorder | apps, all, shift-recorder | traefik | | unless-stopped |
| stockfill | stockfill | | /home/nixos/docker/apps/stockfill | apps, all, stockfill | traefik | | unless-stopped |
| telegraf | telegraf | telegraf:latest | | monitoring, all, telegraf, prometheus | monitor | | unless-stopped |
| traefik | traefik | traefik:3 | /home/nixos/docker/core | core, all, traefik | traefik | {'mode': 'ingress', 'target': 80, 'published': '80', 'protocol': 'tcp'}, {'mode': 'ingress', 'target': 443, 'published': '443', 'protocol': 'tcp'} | always |
## Networks
| Network | Driver | External |
|---|---|---|
| gramps | | False |
| monitor | | False |
| nextcloud | | False |
| passbolt | | False |
| traefik | bridge | False |
## Volumes
| Volume | External |
|---|---|
+65
View File
@@ -0,0 +1,65 @@
# DNS Inventory (Dynu + Traefik)
> This integration is intentionally read-only. No Dynu mutations are permitted in this repo at this stage.
- Base domain: `lan.ddnsgeek.com`
- Dynu fetched at: `2026-04-21T04:18:38+00:00`
- Inventory generated at: `2026-04-21T04:18:39+00:00`
## Summary
- Traefik hostnames discovered: **17**
- Dynu hostnames discovered: **20**
- Mapped hostnames: **17**
- DNS-only hostnames: **3**
- Traefik-only hostnames: **0**
- Ambiguous hostnames: **0**
## Validation
- Validation ok: **false**
- Allowed unmapped hostnames: `edge.lan.ddnsgeek.com`
- Unexpected unmapped hostnames: **1**
- Duplicate hostnames: **1**
- Ambiguous hostnames: **0**
### Allowed unmapped hostnames
- `edge.lan.ddnsgeek.com`
### Unexpected unmapped hostnames
- `kuma.lan.ddnsgeek.com`
### Duplicate hostnames
- `mtls-bridge.lan.ddnsgeek.com`
### Ambiguous hostnames
_None._
## Correlation
| Hostname | Status | Reasons | Service(s) | Route metadata | DNS records |
|---|---|---|---|---|---|
| `auth.lan.ddnsgeek.com` | `mapped` | `mapped` | core/authelia | authelia [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `edge.lan.ddnsgeek.com` | `allowed_unmapped` | `allowed_unmapped, dns_only` | - | - | A: |
| `familytree.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/grampsweb | gramps [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `gitea.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/gitea | gitea [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `gotify.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/gotify | gotify [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
| `grafana.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/grafana | grafana [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
| `influxdb.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/influxdb | influxdb [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
| `kuma.lan.ddnsgeek.com` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | A:120.155.63.223 |
| `lan.ddnsgeek.com` | `dns_only` | `dns_only` | - | - | SOA: |
| `monitor-kuma.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/monitor-kuma | monitor [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
| `mtls-bridge.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/mtls-bridge | mtls-bridge [tls=true, mtls=true, authelia=false, tls_options=-, middlewares=mtls-bridge-auth,mtls-bridge-cors]<br>mtls-bridge-preflight [tls=true, mtls=true, authelia=false, tls_options=-, middlewares=mtls-bridge-cors] | A: |
| `nextcloud.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/nextcloud-webapp | nextcloud [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=nextcloud-dav,nextcloud-webfinger] | A: |
| `node-red.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/node-red | node-red [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
| `passbolt.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/passbolt-webapp | passbolt [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `portainer.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/portainer | portainer [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
| `prometheus.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/prometheus | prometheus [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
| `searxng.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/searxng-webapp | searxng [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `shifts.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/shift-recorder-web | shifts [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `stockfill.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/stockfill | stockfill [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `traefik.lan.ddnsgeek.com` | `mapped` | `mapped` | core/traefik | traefik [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
# Host Topology
> Generated by `scripts/docs/generate_host_topology.py` on 2026-05-12T18:32:09+00:00.
## Topology Diagram
```mermaid
flowchart TD
phys_pve["pve\nphysical"]
phys_raspberrypi["raspberrypi\nphysical"]
virt_docker["docker\nvirtual"]
phys_pve --> virt_docker
virt_nix_cache["nix-cache\nvirtual"]
phys_pve --> virt_nix_cache
virt_pbs["pbs\nvirtual"]
phys_pve --> virt_pbs
virt_pihole["pihole\nvirtual"]
phys_pve --> virt_pihole
virt_server_nixos["server-nixos\nvirtual"]
phys_pve --> virt_server_nixos
```
## Physical Hosts
| Name | Type | Role | Management | OS | Hypervisor | Location | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| pve | physical | proxmox | pve.sweet.home | debian | proxmox | home | Primary Proxmox VE host |
| raspberrypi | physical | edge | raspberrypi.tail13f623.ts.net | debian | | riverglades | Raspberry Pi host |
## Virtual Hosts
| Name | Type | Role | Parent/Node | Management | OS | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| docker | virtual | docker-host | pve | | linux | Primary Docker VM |
| nix-cache | virtual | cache | pve | | linux | Nix binary cache VM |
| pbs | virtual | backup | pve | | linux | Proxmox Backup Server VM |
| pihole | virtual | dns | pve | | linux | DNS filtering VM |
| server-nixos | virtual | nixos-server | pve | | nixos | General-purpose NixOS VM |
+11
View File
@@ -0,0 +1,11 @@
# Generated Documentation
This directory contains documentation generated automatically from repository configuration.
## Files
- [Compose file list](compose-files.txt)
- [Resolved Docker Compose config](docker-compose.resolved.yml)
- [Compose inventory](compose-inventory.md)
- [Traefik routes](traefik-routes.md)
- [Docker Compose diagram](../diagrams/docker-compose.svg)
+23
View File
@@ -0,0 +1,23 @@
# Traefik Routes
| Service | Router | Rule | Entrypoints | TLS | Middlewares | Target Port |
|---|---|---|---|---|---|---|
| authelia | authelia | Host(`auth.lan.ddnsgeek.com`) | websecure | true | | |
| error-pages | error-pages-router | HostRegexp(`{host:.+}`) | web | | error-pages-middleware | |
| gitea | gitea | Host(`gitea.lan.ddnsgeek.com`) | websecure | true | | 3000 |
| gotify | gotify | Host(`gotify.lan.ddnsgeek.com`) | websecure | | | 80 |
| grafana | grafana | Host(`grafana.lan.ddnsgeek.com`) | websecure | | | 3000 |
| grampsweb | gramps | Host(`familytree.lan.ddnsgeek.com`) | websecure | | | 5000 |
| influxdb | influxdb | Host(`influxdb.lan.ddnsgeek.com`) | websecure | | authelia | 8086 |
| monitor-kuma | monitor | Host(`monitor-kuma.lan.ddnsgeek.com`) | websecure | true | | 3001 |
| mtls-bridge | mtls-bridge | Host(`mtls-bridge.lan.ddnsgeek.com`) | websecure | | mtls-bridge-auth,mtls-bridge-cors | 8080 |
| mtls-bridge | mtls-bridge-preflight | Host(`mtls-bridge.lan.ddnsgeek.com`) && Method(`OPTIONS`) | websecure | | mtls-bridge-cors | |
| nextcloud-webapp | nextcloud | Host(`nextcloud.lan.ddnsgeek.com`) | websecure | | nextcloud-dav, nextcloud-webfinger | |
| node-red | node-red | Host(`node-red.lan.ddnsgeek.com`) | websecure | | authelia | 1880 |
| passbolt-webapp | passbolt | Host(`passbolt.lan.ddnsgeek.com`) | websecure | | | |
| portainer | portainer | Host(`portainer.lan.ddnsgeek.com`) | websecure | true | | 9000 |
| prometheus | prometheus | Host(`prometheus.lan.ddnsgeek.com`) | websecure | | authelia | 9090 |
| searxng-webapp | searxng | Host(`searxng.lan.ddnsgeek.com`) | websecure | | | 8080 |
| shift-recorder-web | shifts | Host(`shifts.lan.ddnsgeek.com`) | websecure | true | | 80 |
| stockfill | stockfill | Host(`stockfill.lan.ddnsgeek.com`) | websecure | true | | 80 |
| traefik | traefik | Host(`traefik.lan.ddnsgeek.com`) | websecure | | authelia | |
+29
View File
@@ -0,0 +1,29 @@
# Infrastructure Documentation
This documentation describes the Docker-based infrastructure, reverse proxy configuration, monitoring stack, automation services, and generated inventory for this environment.
Some sections are manually written. Other sections are generated automatically by GitHub Actions from the repository configuration.
## Sections
- [Docker Environment](docker.md)
- [Networking and Reverse Proxy](networking.md)
- [Monitoring and Alerting](monitoring.md)
- [Automation](automation.md)
- [Operations](operations.md)
- [Public Showcase](showcase.md)
## Generated Documentation
- [Compose Inventory](generated/compose-inventory.md)
- [Traefik Routes](generated/traefik-routes.md)
- [Resolved Compose Config](generated/docker-compose.resolved.yml)
- [Docker Compose Diagram](diagrams/docker-compose.svg)
## Public-safe Output
Sanitized documentation intended for public sharing is generated under:
```text
docs/public/
```
+28 -3
View File
@@ -25,14 +25,37 @@ This is currently the most structured host/VM inventory in the repo.
These resources should match existing running containers, not redefine runtime composition strategy.
### 3) Compose runtime definitions
### 3) Ansible bootstrap layer
`infrastructure/ansible/` provides an emerging inventory/configuration scaffold for hosts and devices.
Current scope is intentionally limited to structure, variables scaffolding, and safe validation workflows.
### 4) Compose runtime definitions
Compose files define intended service runtime composition, networking, labels, and integration.
### 4) Architecture docs
### 5) Architecture docs
`docs/architecture.md` provides a human-readable topology view based on repository configuration and observed runtime signals.
### 6) Dynu DNS read-only inventory
`scripts/dynu/` and `docs/dynu-dns-inventory.md` provide a strictly read-only DNS inventory workflow:
- fetch Dynu DNS data with GET-only API usage,
- correlate Dynu hostnames with Traefik `Host(...)` labels in Compose sources,
- generate local JSON and markdown artifacts for documentation pipelines.
Dynu write operations are intentionally blocked in this repository stage.
### 7) Terraform Dynu DNS layer
`infrastructure/terraform/dynu/` is the brownfield Terraform DNS mirror/reconciliation root for Dynu domain/record inventory outputs.
At this stage it is primarily documentation-oriented catalog output, ready for one-object-at-a-time imports.
## Output shaping expectations
When adding Terraform outputs for documentation/tooling:
@@ -44,8 +67,10 @@ When adding Terraform outputs for documentation/tooling:
## Limitations today
- Generated host topology document: `docs/generated/host-topology.md` (via `scripts/docs/build_host_topology.sh`).
- No full generated inventory document pipeline is present yet.
- Some Terraform files still include generated boilerplate comments requiring ongoing cleanup.
- Ansible/NixOS operational layers are not yet implemented in a way that provides authoritative inventory in this repo.
- Ansible is currently a bootstrap inventory/configuration layer and is not authoritative for full operations yet.
- NixOS operational management is not yet implemented as an Ansible authority in this repo.
These limitations are expected for the current adoption stage.
+10
View File
@@ -0,0 +1,10 @@
# Monitoring and Alerting
Monitoring documentation is generated from static rule files committed in this repository.
- Prometheus rule files are parsed statically.
- Live Prometheus APIs are not queried.
- Future improvements can include Grafana dashboard and alert contact-point summaries.
Generated asset:
+12
View File
@@ -0,0 +1,12 @@
# Networking and Reverse Proxy
Networking and reverse-proxy documentation is generated from Docker Compose metadata.
- Traefik labels are parsed statically from Compose definitions.
- Service-to-network relationships are represented in generated diagrams.
- Sensitive internal values are redacted in public-facing output.
Related generated assets:
- [Traefik Routes](generated/traefik-routes.md)
- [Docker Compose Diagram](diagrams/docker-compose.svg)
+30
View File
@@ -0,0 +1,30 @@
# Operations
## Local docs generation
```bash
chmod +x scripts/docs/*.sh
scripts/docs/generate-all.sh
```
## Inspect generated changes
```bash
git status -- docs/generated docs/diagrams docs/public
```
## GitHub Actions artifacts
Use the **Generate documentation** workflow run and download the `generated-documentation` artifact from the run summary.
## Commit behavior
- Pull requests generate docs and upload artifacts only.
- Pushes to `main` generate docs, upload artifacts, and commit generated docs when changes exist.
- Manual workflow runs can commit generated docs when enabled through workflow input.
## Troubleshooting
- Confirm required tooling (`python3`, `jq`, `graphviz`, `docker compose`) is available.
- Re-run generation after documentation script changes.
- Review `docs/public` output for redaction coverage before sharing.
View File
+59
View File
@@ -0,0 +1,59 @@
# Docker Compose Inventory
Source fingerprint: `232be78ef441`
## Summary
| Item | Count |
|---|---:|
| Services | 28 |
| Networks | 5 |
| Volumes | 0 |
## Services
| Service | Container | Image | Build | Profiles | Networks | Ports | Restart |
|---|---|---|---|---|---|---|---|
| authelia | authelia | authelia/authelia | /home/nixos/docker/core/authelia | core, all, authelia, traefik | traefik | | always |
| crowdsec | crowdsec | | /home/nixos/docker/core/crowdsec | core, all, crowdsec, traefik | traefik | | always |
| docker-socket-proxy | docker-socket-proxy | tecnativa/docker-socket-proxy:latest | | monitoring, all, docker-socket-proxy, core, traefik, prometheus | monitor, traefik | | unless-stopped |
| docker-update-exporter | docker-update-exporter | | /home/nixos/docker/monitoring/docker-exporter | monitoring, all, docker-exporter, prometheus | monitor | | unless-stopped |
| error-pages | error-pages | tarampampam/error-pages:3 | | core, all, error-pages, traefik | traefik | | always |
| gitea | gitea | gitea/gitea:latest | | apps, all, gitea | traefik | | always |
| gitea-runner | gitea-runner | gitea/act_runner:latest | | apps, all, gitea, ci | traefik | | always |
| gotify | gotify | gotify/server:latest | | monitoring, all, gotify | traefik | | always |
| grafana | grafana | grafana/grafana:latest | | monitoring, all, grafana | monitor, traefik | | unless-stopped |
| gramps-redis | gramps-redis | valkey/valkey:8-alpine | | apps, all, gramps | gramps | | always |
| grampsweb | gramps-web | ghcr.io/gramps-project/grampsweb:latest | | apps, all, gramps | gramps, traefik | | always |
| grampsweb_celery | gramps-web-celery | ghcr.io/gramps-project/grampsweb:latest | | apps, all, gramps | gramps | | always |
| influxdb | influxdb | influxdb:2.7 | | monitoring, all, influxdb, prometheus | monitor, traefik | | unless-stopped |
| monitor-kuma | monitor-kuma | louislam/uptime-kuma:2.1.1 | | monitoring, all, uptime-kuma | monitor, traefik | | always |
| mtls-bridge | mtls-bridge | | /home/nixos/docker/monitoring/mtls-bridge | monitoring, all, mtls-bridge | monitor, traefik | | unless-stopped |
| nextcloud-db | nextcloud-db | mariadb:11.4 | | apps, all, nextcloud | nextcloud | | always |
| nextcloud-redis | nextcloud-redis | redis | | apps, all, nextcloud | nextcloud | | always |
| nextcloud-webapp | nextcloud-webapp | | /home/nixos/docker/apps/nextcloud | apps, all, nextcloud | nextcloud, traefik | | always |
| node-exporter | node-exporter | prom/node-exporter:latest | | monitoring, all, node-exporter, prometheus | monitor | | unless-stopped |
| node-red | node-red | | /home/nixos/docker/monitoring/node-red | monitoring, all, node-red | monitor, traefik | | unless-stopped |
| passbolt-db | passbolt-db | mariadb:12 | | apps, all, passbolt | passbolt | | always |
| passbolt-webapp | passbolt-webapp | passbolt/passbolt:latest-ce | | apps, all, passbolt | passbolt, traefik | | always |
| pihole-exporter | pihole-exporter | ekofr/pihole-exporter:latest | | monitoring, all, pihole-exporter, prometheus | monitor | {'mode': 'ingress', 'target': 9617, 'published': '9617', 'protocol': 'tcp'} | unless-stopped |
| portainer | portainer | portainer/portainer-ce:latest | | monitoring, all, portainer | traefik | | unless-stopped |
| prometheus | prometheus | prom/prometheus:latest | | monitoring, all, prometheus | monitor, traefik | | unless-stopped |
| searxng-webapp | searxng-webapp | searxng/searxng | | apps, all, searxng | traefik | | always |
| telegraf | telegraf | telegraf:latest | | monitoring, all, telegraf, prometheus | monitor | | unless-stopped |
| traefik | traefik | traefik:3 | /home/nixos/docker/core | core, all, traefik | traefik | {'mode': 'ingress', 'target': 80, 'published': '80', 'protocol': 'tcp'}, {'mode': 'ingress', 'target': 443, 'published': '443', 'protocol': 'tcp'} | always |
## Networks
| Network | Driver | External |
|---|---|---|
| gramps | | False |
| monitor | | False |
| nextcloud | | False |
| passbolt | | False |
| traefik | bridge | False |
## Volumes
| Volume | External |
|---|---|
+19
View File
@@ -0,0 +1,19 @@
# Infrastructure diagrams
## Physical / virtual topology
This view groups containers by inferred host and service role (edge/proxy/auth, monitoring, automation, apps, and supporting storage/services).
<div class="diagram-wrap">
<img src="physical-topology.svg" alt="Physical topology">
</div>
## Docker, Traefik and Dynu routing
This view shows sanitised public DNS names flowing to Traefik, then to exposed Docker services, with backend Docker network membership shown as secondary context.
_Diagrams are generated from Compose data and Traefik labels._
<div class="diagram-wrap">
<img src="docker-traefik-dynu.svg" alt="Docker Traefik Dynu">
</div>
+439
View File
@@ -0,0 +1,439 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: Compose Pages: 1 -->
<svg width="334pt" height="1502pt"
viewBox="0.00 0.00 334.49 1502.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1498)">
<title>Compose</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1498 330.49,-1498 330.49,4 -4,4"/>
<!-- svc:authelia -->
<g id="node1" class="node">
<title>svc:authelia</title>
<polygon fill="#dfefff" stroke="black" points="126,-738 54,-738 54,-702 126,-702 126,-738"/>
<text text-anchor="middle" x="90" y="-716.3" font-family="Helvetica,sans-Serif" font-size="14.00">authelia</text>
</g>
<!-- net:traefik -->
<g id="node33" class="node">
<title>net:traefik</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-774" rx="40.09" ry="18"/>
<text text-anchor="middle" x="271.25" y="-770.3" font-family="Helvetica,sans-Serif" font-size="14.00">traefik</text>
</g>
<!-- svc:authelia&#45;&gt;net:traefik -->
<g id="edge1" class="edge">
<title>svc:authelia&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M126.41,-730.67C155.5,-739.43 196.8,-751.87 227.69,-761.18"/>
<polygon fill="black" stroke="black" points="226.74,-764.55 237.32,-764.08 228.76,-757.85 226.74,-764.55"/>
</g>
<!-- svc:crowdsec -->
<g id="node2" class="node">
<title>svc:crowdsec</title>
<polygon fill="#dfefff" stroke="black" points="130.5,-684 49.5,-684 49.5,-648 130.5,-648 130.5,-684"/>
<text text-anchor="middle" x="90" y="-662.3" font-family="Helvetica,sans-Serif" font-size="14.00">crowdsec</text>
</g>
<!-- svc:crowdsec&#45;&gt;net:traefik -->
<g id="edge2" class="edge">
<title>svc:crowdsec&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M130.61,-674.06C146.59,-678.3 164.81,-684.44 180,-693 206.25,-707.78 231.35,-731.35 248.38,-749.26"/>
<polygon fill="black" stroke="black" points="246.24,-752.1 255.62,-757.03 251.37,-747.33 246.24,-752.1"/>
</g>
<!-- svc:docker&#45;socket&#45;proxy -->
<g id="node3" class="node">
<title>svc:docker&#45;socket&#45;proxy</title>
<polygon fill="#dfefff" stroke="black" points="167.5,-954 12.5,-954 12.5,-918 167.5,-918 167.5,-954"/>
<text text-anchor="middle" x="90" y="-932.3" font-family="Helvetica,sans-Serif" font-size="14.00">docker&#45;socket&#45;proxy</text>
</g>
<!-- net:monitor -->
<g id="node30" class="node">
<title>net:monitor</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-1206" rx="46.29" ry="18"/>
<text text-anchor="middle" x="271.25" y="-1202.3" font-family="Helvetica,sans-Serif" font-size="14.00">monitor</text>
</g>
<!-- svc:docker&#45;socket&#45;proxy&#45;&gt;net:monitor -->
<g id="edge3" class="edge">
<title>svc:docker&#45;socket&#45;proxy&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M167.53,-953.71C172.02,-956.38 176.24,-959.46 180,-963 242.3,-1021.6 261.83,-1127.12 267.77,-1177.6"/>
<polygon fill="black" stroke="black" points="264.31,-1178.21 268.86,-1187.78 271.27,-1177.46 264.31,-1178.21"/>
</g>
<!-- svc:docker&#45;socket&#45;proxy&#45;&gt;net:traefik -->
<g id="edge4" class="edge">
<title>svc:docker&#45;socket&#45;proxy&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M165.31,-917.9C170.5,-915.32 175.46,-912.37 180,-909 218.01,-880.82 244.97,-831.56 259.03,-800.98"/>
<polygon fill="black" stroke="black" points="262.27,-802.31 263.14,-791.76 255.87,-799.46 262.27,-802.31"/>
</g>
<!-- svc:docker&#45;update&#45;exporter -->
<g id="node4" class="node">
<title>svc:docker&#45;update&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="180,-1494 0,-1494 0,-1458 180,-1458 180,-1494"/>
<text text-anchor="middle" x="90" y="-1472.3" font-family="Helvetica,sans-Serif" font-size="14.00">docker&#45;update&#45;exporter</text>
</g>
<!-- svc:docker&#45;update&#45;exporter&#45;&gt;net:monitor -->
<g id="edge5" class="edge">
<title>svc:docker&#45;update&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M168.37,-1457.79C172.55,-1455.23 176.47,-1452.32 180,-1449 242.3,-1390.4 261.83,-1284.88 267.77,-1234.4"/>
<polygon fill="black" stroke="black" points="271.27,-1234.54 268.86,-1224.22 264.31,-1233.79 271.27,-1234.54"/>
</g>
<!-- svc:error&#45;pages -->
<g id="node5" class="node">
<title>svc:error&#45;pages</title>
<polygon fill="#dfefff" stroke="black" points="137.5,-630 42.5,-630 42.5,-594 137.5,-594 137.5,-630"/>
<text text-anchor="middle" x="90" y="-608.3" font-family="Helvetica,sans-Serif" font-size="14.00">error&#45;pages</text>
</g>
<!-- svc:error&#45;pages&#45;&gt;net:traefik -->
<g id="edge6" class="edge">
<title>svc:error&#45;pages&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M137.71,-619.76C152.25,-623.79 167.68,-629.86 180,-639 218.01,-667.18 244.97,-716.44 259.03,-747.02"/>
<polygon fill="black" stroke="black" points="255.87,-748.54 263.14,-756.24 262.27,-745.69 255.87,-748.54"/>
</g>
<!-- svc:gitea -->
<g id="node6" class="node">
<title>svc:gitea</title>
<polygon fill="#dfefff" stroke="black" points="117,-576 63,-576 63,-540 117,-540 117,-576"/>
<text text-anchor="middle" x="90" y="-554.3" font-family="Helvetica,sans-Serif" font-size="14.00">gitea</text>
</g>
<!-- svc:gitea&#45;&gt;net:traefik -->
<g id="edge7" class="edge">
<title>svc:gitea&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M117.07,-560.28C136.41,-563.2 162.37,-569.85 180,-585 229.98,-627.95 254.42,-704.8 264.44,-746.04"/>
<polygon fill="black" stroke="black" points="261.07,-747 266.73,-755.95 267.88,-745.42 261.07,-747"/>
</g>
<!-- svc:gitea&#45;runner -->
<g id="node7" class="node">
<title>svc:gitea&#45;runner</title>
<polygon fill="#dfefff" stroke="black" points="142,-522 38,-522 38,-486 142,-486 142,-522"/>
<text text-anchor="middle" x="90" y="-500.3" font-family="Helvetica,sans-Serif" font-size="14.00">gitea&#45;runner</text>
</g>
<!-- svc:gitea&#45;runner&#45;&gt;net:traefik -->
<g id="edge8" class="edge">
<title>svc:gitea&#45;runner&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M142.31,-511.04C155.9,-515.03 169.65,-521.26 180,-531 242.3,-589.6 261.83,-695.12 267.77,-745.6"/>
<polygon fill="black" stroke="black" points="264.31,-746.21 268.86,-755.78 271.27,-745.46 264.31,-746.21"/>
</g>
<!-- svc:gotify -->
<g id="node8" class="node">
<title>svc:gotify</title>
<polygon fill="#dfefff" stroke="black" points="118,-468 62,-468 62,-432 118,-432 118,-468"/>
<text text-anchor="middle" x="90" y="-446.3" font-family="Helvetica,sans-Serif" font-size="14.00">gotify</text>
</g>
<!-- svc:gotify&#45;&gt;net:traefik -->
<g id="edge9" class="edge">
<title>svc:gotify&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M118.17,-451.6C137.81,-454.16 163.67,-460.68 180,-477 254.99,-551.96 268.02,-687.13 270.03,-745.69"/>
<polygon fill="black" stroke="black" points="266.53,-745.79 270.29,-755.69 273.53,-745.61 266.53,-745.79"/>
</g>
<!-- svc:grafana -->
<g id="node9" class="node">
<title>svc:grafana</title>
<polygon fill="#dfefff" stroke="black" points="125.5,-1278 54.5,-1278 54.5,-1242 125.5,-1242 125.5,-1278"/>
<text text-anchor="middle" x="90" y="-1256.3" font-family="Helvetica,sans-Serif" font-size="14.00">grafana</text>
</g>
<!-- svc:grafana&#45;&gt;net:monitor -->
<g id="edge10" class="edge">
<title>svc:grafana&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M125.56,-1249.59C153.74,-1241.1 193.78,-1229.04 224.6,-1219.75"/>
<polygon fill="black" stroke="black" points="225.69,-1223.08 234.26,-1216.84 223.67,-1216.38 225.69,-1223.08"/>
</g>
<!-- svc:grafana&#45;&gt;net:traefik -->
<g id="edge11" class="edge">
<title>svc:grafana&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M125.54,-1257.93C144.23,-1254.97 166.25,-1248.21 180,-1233 238.17,-1168.67 262.1,-892.17 268.43,-802.32"/>
<polygon fill="black" stroke="black" points="271.93,-802.3 269.13,-792.08 264.95,-801.82 271.93,-802.3"/>
</g>
<!-- svc:gramps&#45;redis -->
<g id="node10" class="node">
<title>svc:gramps&#45;redis</title>
<polygon fill="#dfefff" stroke="black" points="144.5,-360 35.5,-360 35.5,-324 144.5,-324 144.5,-360"/>
<text text-anchor="middle" x="90" y="-338.3" font-family="Helvetica,sans-Serif" font-size="14.00">gramps&#45;redis</text>
</g>
<!-- net:gramps -->
<g id="node29" class="node">
<title>net:gramps</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-342" rx="45.49" ry="18"/>
<text text-anchor="middle" x="271.25" y="-338.3" font-family="Helvetica,sans-Serif" font-size="14.00">gramps</text>
</g>
<!-- svc:gramps&#45;redis&#45;&gt;net:gramps -->
<g id="edge12" class="edge">
<title>svc:gramps&#45;redis&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M144.78,-342C167.14,-342 193.05,-342 215.51,-342"/>
<polygon fill="black" stroke="black" points="215.56,-345.5 225.56,-342 215.56,-338.5 215.56,-345.5"/>
</g>
<!-- svc:grampsweb -->
<g id="node11" class="node">
<title>svc:grampsweb</title>
<polygon fill="#dfefff" stroke="black" points="139,-414 41,-414 41,-378 139,-378 139,-414"/>
<text text-anchor="middle" x="90" y="-392.3" font-family="Helvetica,sans-Serif" font-size="14.00">grampsweb</text>
</g>
<!-- svc:grampsweb&#45;&gt;net:gramps -->
<g id="edge13" class="edge">
<title>svc:grampsweb&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M139.03,-381.53C165.7,-373.5 198.68,-363.56 224.9,-355.66"/>
<polygon fill="black" stroke="black" points="226.03,-358.98 234.59,-352.74 224.01,-352.27 226.03,-358.98"/>
</g>
<!-- svc:grampsweb&#45;&gt;net:traefik -->
<g id="edge14" class="edge">
<title>svc:grampsweb&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M139.27,-401.34C154.08,-405.21 169.28,-411.81 180,-423 225.12,-470.09 256.34,-671.08 266.59,-745.84"/>
<polygon fill="black" stroke="black" points="263.14,-746.48 267.95,-755.92 270.08,-745.55 263.14,-746.48"/>
</g>
<!-- svc:grampsweb_celery -->
<g id="node12" class="node">
<title>svc:grampsweb_celery</title>
<polygon fill="#dfefff" stroke="black" points="163.5,-306 16.5,-306 16.5,-270 163.5,-270 163.5,-306"/>
<text text-anchor="middle" x="90" y="-284.3" font-family="Helvetica,sans-Serif" font-size="14.00">grampsweb_celery</text>
</g>
<!-- svc:grampsweb_celery&#45;&gt;net:gramps -->
<g id="edge15" class="edge">
<title>svc:grampsweb_celery&#45;&gt;net:gramps</title>
<path fill="none" stroke="black" d="M151.18,-306.13C175.28,-313.39 202.57,-321.61 224.94,-328.35"/>
<polygon fill="black" stroke="black" points="223.94,-331.71 234.52,-331.24 225.96,-325 223.94,-331.71"/>
</g>
<!-- svc:influxdb -->
<g id="node13" class="node">
<title>svc:influxdb</title>
<polygon fill="#dfefff" stroke="black" points="127,-1224 53,-1224 53,-1188 127,-1188 127,-1224"/>
<text text-anchor="middle" x="90" y="-1202.3" font-family="Helvetica,sans-Serif" font-size="14.00">influxdb</text>
</g>
<!-- svc:influxdb&#45;&gt;net:monitor -->
<g id="edge16" class="edge">
<title>svc:influxdb&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M127.27,-1206C152.33,-1206 186.13,-1206 214.57,-1206"/>
<polygon fill="black" stroke="black" points="214.79,-1209.5 224.79,-1206 214.79,-1202.5 214.79,-1209.5"/>
</g>
<!-- svc:influxdb&#45;&gt;net:traefik -->
<g id="edge17" class="edge">
<title>svc:influxdb&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M127.17,-1203.5C145.42,-1200.36 166.5,-1193.56 180,-1179 231.57,-1123.4 259.36,-885.3 267.6,-802.51"/>
<polygon fill="black" stroke="black" points="271.1,-802.62 268.59,-792.33 264.14,-801.94 271.1,-802.62"/>
</g>
<!-- svc:monitor&#45;kuma -->
<g id="node14" class="node">
<title>svc:monitor&#45;kuma</title>
<polygon fill="#dfefff" stroke="black" points="146.5,-1170 33.5,-1170 33.5,-1134 146.5,-1134 146.5,-1170"/>
<text text-anchor="middle" x="90" y="-1148.3" font-family="Helvetica,sans-Serif" font-size="14.00">monitor&#45;kuma</text>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:monitor -->
<g id="edge18" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M146.73,-1168.79C171.73,-1176.32 200.85,-1185.1 224.54,-1192.23"/>
<polygon fill="black" stroke="black" points="223.75,-1195.65 234.33,-1195.18 225.77,-1188.94 223.75,-1195.65"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:traefik -->
<g id="edge19" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M146.69,-1144.49C159.02,-1140.46 171.06,-1134.33 180,-1125 225.12,-1077.91 256.34,-876.92 266.59,-802.16"/>
<polygon fill="black" stroke="black" points="270.08,-802.45 267.95,-792.08 263.14,-801.52 270.08,-802.45"/>
</g>
<!-- svc:mtls&#45;bridge -->
<g id="node15" class="node">
<title>svc:mtls&#45;bridge</title>
<polygon fill="#dfefff" stroke="black" points="138.5,-1116 41.5,-1116 41.5,-1080 138.5,-1080 138.5,-1116"/>
<text text-anchor="middle" x="90" y="-1094.3" font-family="Helvetica,sans-Serif" font-size="14.00">mtls&#45;bridge</text>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:monitor -->
<g id="edge20" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M138.76,-1108.34C152.58,-1112.41 167.34,-1117.87 180,-1125 206.25,-1139.78 231.35,-1163.35 248.38,-1181.26"/>
<polygon fill="black" stroke="black" points="246.24,-1184.1 255.62,-1189.03 251.37,-1179.33 246.24,-1184.1"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:traefik -->
<g id="edge21" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M138.53,-1092.51C153.46,-1088.63 168.92,-1082.07 180,-1071 254.99,-996.04 268.02,-860.87 270.03,-802.31"/>
<polygon fill="black" stroke="black" points="273.53,-802.39 270.29,-792.31 266.53,-802.21 273.53,-802.39"/>
</g>
<!-- svc:nextcloud&#45;db -->
<g id="node16" class="node">
<title>svc:nextcloud&#45;db</title>
<polygon fill="#dfefff" stroke="black" points="144,-90 36,-90 36,-54 144,-54 144,-90"/>
<text text-anchor="middle" x="90" y="-68.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;db</text>
</g>
<!-- net:nextcloud -->
<g id="node31" class="node">
<title>net:nextcloud</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-180" rx="55.49" ry="18"/>
<text text-anchor="middle" x="271.25" y="-176.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud</text>
</g>
<!-- svc:nextcloud&#45;db&#45;&gt;net:nextcloud -->
<g id="edge22" class="edge">
<title>svc:nextcloud&#45;db&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M144.33,-84.04C156.48,-87.9 169.03,-92.82 180,-99 206.09,-113.7 231.04,-137.06 248.07,-154.93"/>
<polygon fill="black" stroke="black" points="245.93,-157.77 255.31,-162.7 251.06,-153 245.93,-157.77"/>
</g>
<!-- svc:nextcloud&#45;redis -->
<g id="node17" class="node">
<title>svc:nextcloud&#45;redis</title>
<polygon fill="#dfefff" stroke="black" points="152,-198 28,-198 28,-162 152,-162 152,-198"/>
<text text-anchor="middle" x="90" y="-176.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;redis</text>
</g>
<!-- svc:nextcloud&#45;redis&#45;&gt;net:nextcloud -->
<g id="edge23" class="edge">
<title>svc:nextcloud&#45;redis&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M152.18,-180C169.48,-180 188.35,-180 205.83,-180"/>
<polygon fill="black" stroke="black" points="205.94,-183.5 215.94,-180 205.94,-176.5 205.94,-183.5"/>
</g>
<!-- svc:nextcloud&#45;webapp -->
<g id="node18" class="node">
<title>svc:nextcloud&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="162.5,-252 17.5,-252 17.5,-216 162.5,-216 162.5,-252"/>
<text text-anchor="middle" x="90" y="-230.3" font-family="Helvetica,sans-Serif" font-size="14.00">nextcloud&#45;webapp</text>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud -->
<g id="edge24" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="black" d="M151.18,-215.87C173.57,-209.12 198.72,-201.55 220.11,-195.1"/>
<polygon fill="black" stroke="black" points="221.4,-198.37 229.97,-192.13 219.38,-191.67 221.4,-198.37"/>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:traefik -->
<g id="edge25" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M162.89,-247.71C169.31,-251.2 175.18,-255.56 180,-261 212.66,-297.85 255.07,-643.36 267,-745.62"/>
<polygon fill="black" stroke="black" points="263.55,-746.27 268.18,-755.8 270.51,-745.47 263.55,-746.27"/>
</g>
<!-- svc:node&#45;exporter -->
<g id="node19" class="node">
<title>svc:node&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="148,-1440 32,-1440 32,-1404 148,-1404 148,-1440"/>
<text text-anchor="middle" x="90" y="-1418.3" font-family="Helvetica,sans-Serif" font-size="14.00">node&#45;exporter</text>
</g>
<!-- svc:node&#45;exporter&#45;&gt;net:monitor -->
<g id="edge26" class="edge">
<title>svc:node&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M148.25,-1412.27C159.68,-1408.33 170.94,-1402.79 180,-1395 229.98,-1352.05 254.42,-1275.2 264.44,-1233.96"/>
<polygon fill="black" stroke="black" points="267.88,-1234.58 266.73,-1224.05 261.07,-1233 267.88,-1234.58"/>
</g>
<!-- svc:node&#45;red -->
<g id="node20" class="node">
<title>svc:node&#45;red</title>
<polygon fill="#dfefff" stroke="black" points="129.5,-1062 50.5,-1062 50.5,-1026 129.5,-1026 129.5,-1062"/>
<text text-anchor="middle" x="90" y="-1040.3" font-family="Helvetica,sans-Serif" font-size="14.00">node&#45;red</text>
</g>
<!-- svc:node&#45;red&#45;&gt;net:monitor -->
<g id="edge27" class="edge">
<title>svc:node&#45;red&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M129.57,-1049.69C146.29,-1053.6 165.34,-1060.13 180,-1071 218.01,-1099.18 244.97,-1148.44 259.03,-1179.02"/>
<polygon fill="black" stroke="black" points="255.87,-1180.54 263.14,-1188.24 262.27,-1177.69 255.87,-1180.54"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:traefik -->
<g id="edge28" class="edge">
<title>svc:node&#45;red&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M129.95,-1040.03C147.14,-1036.46 166.48,-1029.72 180,-1017 242.3,-958.4 261.83,-852.88 267.77,-802.4"/>
<polygon fill="black" stroke="black" points="271.27,-802.54 268.86,-792.22 264.31,-801.79 271.27,-802.54"/>
</g>
<!-- svc:passbolt&#45;db -->
<g id="node21" class="node">
<title>svc:passbolt&#45;db</title>
<polygon fill="#dfefff" stroke="black" points="139,-36 41,-36 41,0 139,0 139,-36"/>
<text text-anchor="middle" x="90" y="-14.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt&#45;db</text>
</g>
<!-- net:passbolt -->
<g id="node32" class="node">
<title>net:passbolt</title>
<ellipse fill="#f4f4f4" stroke="black" cx="271.25" cy="-72" rx="48.99" ry="18"/>
<text text-anchor="middle" x="271.25" y="-68.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt</text>
</g>
<!-- svc:passbolt&#45;db&#45;&gt;net:passbolt -->
<g id="edge29" class="edge">
<title>svc:passbolt&#45;db&#45;&gt;net:passbolt</title>
<path fill="none" stroke="black" d="M139.03,-32.47C165.15,-40.34 197.32,-50.03 223.27,-57.85"/>
<polygon fill="black" stroke="black" points="222.3,-61.21 232.88,-60.74 224.32,-54.51 222.3,-61.21"/>
</g>
<!-- svc:passbolt&#45;webapp -->
<g id="node22" class="node">
<title>svc:passbolt&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="157.5,-144 22.5,-144 22.5,-108 157.5,-108 157.5,-144"/>
<text text-anchor="middle" x="90" y="-122.3" font-family="Helvetica,sans-Serif" font-size="14.00">passbolt&#45;webapp</text>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:passbolt -->
<g id="edge30" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:passbolt</title>
<path fill="none" stroke="black" d="M151.18,-107.87C174.64,-100.8 201.13,-92.82 223.16,-86.19"/>
<polygon fill="black" stroke="black" points="224.37,-89.48 232.94,-83.24 222.35,-82.77 224.37,-89.48"/>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:traefik -->
<g id="edge31" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M157.85,-136.93C166.26,-140.81 174,-146.02 180,-153 189.88,-164.49 250.79,-625.33 266.53,-745.56"/>
<polygon fill="black" stroke="black" points="263.1,-746.33 267.87,-755.8 270.04,-745.43 263.1,-746.33"/>
</g>
<!-- svc:pihole&#45;exporter -->
<g id="node23" class="node">
<title>svc:pihole&#45;exporter</title>
<polygon fill="#dfefff" stroke="black" points="151.5,-1386 28.5,-1386 28.5,-1350 151.5,-1350 151.5,-1386"/>
<text text-anchor="middle" x="90" y="-1364.3" font-family="Helvetica,sans-Serif" font-size="14.00">pihole&#45;exporter</text>
</g>
<!-- svc:pihole&#45;exporter&#45;&gt;net:monitor -->
<g id="edge32" class="edge">
<title>svc:pihole&#45;exporter&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M151.62,-1355.78C161.68,-1352.07 171.57,-1347.25 180,-1341 218.01,-1312.82 244.97,-1263.56 259.03,-1232.98"/>
<polygon fill="black" stroke="black" points="262.27,-1234.31 263.14,-1223.76 255.87,-1231.46 262.27,-1234.31"/>
</g>
<!-- svc:portainer -->
<g id="node24" class="node">
<title>svc:portainer</title>
<polygon fill="#dfefff" stroke="black" points="130,-900 50,-900 50,-864 130,-864 130,-900"/>
<text text-anchor="middle" x="90" y="-878.3" font-family="Helvetica,sans-Serif" font-size="14.00">portainer</text>
</g>
<!-- svc:portainer&#45;&gt;net:traefik -->
<g id="edge33" class="edge">
<title>svc:portainer&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M130.17,-874.06C146.25,-869.82 164.67,-863.64 180,-855 206.25,-840.22 231.35,-816.65 248.38,-798.74"/>
<polygon fill="black" stroke="black" points="251.37,-800.67 255.62,-790.97 246.24,-795.9 251.37,-800.67"/>
</g>
<!-- svc:prometheus -->
<g id="node25" class="node">
<title>svc:prometheus</title>
<polygon fill="#dfefff" stroke="black" points="140,-1008 40,-1008 40,-972 140,-972 140,-1008"/>
<text text-anchor="middle" x="90" y="-986.3" font-family="Helvetica,sans-Serif" font-size="14.00">prometheus</text>
</g>
<!-- svc:prometheus&#45;&gt;net:monitor -->
<g id="edge34" class="edge">
<title>svc:prometheus&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M140.37,-997.26C154.39,-1001.24 168.86,-1007.42 180,-1017 229.98,-1059.95 254.42,-1136.8 264.44,-1178.04"/>
<polygon fill="black" stroke="black" points="261.07,-1179 266.73,-1187.95 267.88,-1177.42 261.07,-1179"/>
</g>
<!-- svc:prometheus&#45;&gt;net:traefik -->
<g id="edge35" class="edge">
<title>svc:prometheus&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M140.37,-982.74C154.39,-978.76 168.86,-972.58 180,-963 229.98,-920.05 254.42,-843.2 264.44,-801.96"/>
<polygon fill="black" stroke="black" points="267.88,-802.58 266.73,-792.05 261.07,-801 267.88,-802.58"/>
</g>
<!-- svc:searxng&#45;webapp -->
<g id="node26" class="node">
<title>svc:searxng&#45;webapp</title>
<polygon fill="#dfefff" stroke="black" points="156,-846 24,-846 24,-810 156,-810 156,-846"/>
<text text-anchor="middle" x="90" y="-824.3" font-family="Helvetica,sans-Serif" font-size="14.00">searxng&#45;webapp</text>
</g>
<!-- svc:searxng&#45;webapp&#45;&gt;net:traefik -->
<g id="edge36" class="edge">
<title>svc:searxng&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M151.18,-809.87C176.26,-802.32 204.79,-793.72 227.63,-786.84"/>
<polygon fill="black" stroke="black" points="228.81,-790.14 237.37,-783.9 226.79,-783.44 228.81,-790.14"/>
</g>
<!-- svc:telegraf -->
<g id="node27" class="node">
<title>svc:telegraf</title>
<polygon fill="#dfefff" stroke="black" points="125.5,-1332 54.5,-1332 54.5,-1296 125.5,-1296 125.5,-1332"/>
<text text-anchor="middle" x="90" y="-1310.3" font-family="Helvetica,sans-Serif" font-size="14.00">telegraf</text>
</g>
<!-- svc:telegraf&#45;&gt;net:monitor -->
<g id="edge37" class="edge">
<title>svc:telegraf&#45;&gt;net:monitor</title>
<path fill="none" stroke="black" d="M125.8,-1307.18C142.85,-1302.93 163.26,-1296.43 180,-1287 206.25,-1272.22 231.35,-1248.65 248.38,-1230.74"/>
<polygon fill="black" stroke="black" points="251.37,-1232.67 255.62,-1222.97 246.24,-1227.9 251.37,-1232.67"/>
</g>
<!-- svc:traefik -->
<g id="node28" class="node">
<title>svc:traefik</title>
<polygon fill="#dfefff" stroke="black" points="121,-792 59,-792 59,-756 121,-756 121,-792"/>
<text text-anchor="middle" x="90" y="-770.3" font-family="Helvetica,sans-Serif" font-size="14.00">traefik</text>
</g>
<!-- svc:traefik&#45;&gt;net:traefik -->
<g id="edge38" class="edge">
<title>svc:traefik&#45;&gt;net:traefik</title>
<path fill="none" stroke="black" d="M121,-774C148.16,-774 188.69,-774 220.66,-774"/>
<polygon fill="black" stroke="black" points="220.71,-777.5 230.71,-774 220.71,-770.5 220.71,-777.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

+611
View File
@@ -0,0 +1,611 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: DockerTraefikDynu Pages: 1 -->
<svg width="1113pt" height="1536pt"
viewBox="0.00 0.00 1113.39 1536.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1532)">
<title>DockerTraefikDynu</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1532 1109.39,-1532 1109.39,4 -4,4"/>
<g id="clust2" class="cluster">
<title>cluster_networks</title>
<path fill="none" stroke="#d1d5db" stroke-dasharray="5,2" d="M922,-650C922,-650 1093.39,-650 1093.39,-650 1099.39,-650 1105.39,-656 1105.39,-662 1105.39,-662 1105.39,-1117 1105.39,-1117 1105.39,-1123 1099.39,-1129 1093.39,-1129 1093.39,-1129 922,-1129 922,-1129 916,-1129 910,-1123 910,-1117 910,-1117 910,-662 910,-662 910,-656 916,-650 922,-650"/>
<text text-anchor="middle" x="1007.69" y="-1113.8" font-family="Helvetica,sans-Serif" font-size="14.00">Docker backend networks</text>
</g>
<!-- dynu -->
<g id="node1" class="node">
<title>dynu</title>
<path fill="#fde68a" stroke="black" d="M374,-789C374,-789 282,-789 282,-789 276,-789 270,-783 270,-777 270,-777 270,-765 270,-765 270,-759 276,-753 282,-753 282,-753 374,-753 374,-753 380,-753 386,-759 386,-765 386,-765 386,-777 386,-777 386,-783 380,-789 374,-789"/>
<text text-anchor="middle" x="328" y="-768.2" font-family="Helvetica,sans-Serif" font-size="11.00">Dynu / Public DNS</text>
</g>
<!-- svc:traefik -->
<g id="node2" class="node">
<title>svc:traefik</title>
<path fill="#dcfce7" stroke="black" d="M559,-789C559,-789 513,-789 513,-789 507,-789 501,-783 501,-777 501,-777 501,-765 501,-765 501,-759 507,-753 513,-753 513,-753 559,-753 559,-753 565,-753 571,-759 571,-765 571,-765 571,-777 571,-777 571,-783 565,-789 559,-789"/>
<text text-anchor="middle" x="536" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik</text>
<text text-anchor="middle" x="536" y="-762.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- dynu&#45;&gt;svc:traefik -->
<g id="edge1" class="edge">
<title>dynu&#45;&gt;svc:traefik</title>
<path fill="none" stroke="#334155" stroke-width="1.6" d="M386.11,-771C419.13,-771 460.06,-771 490.64,-771"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.6" points="491,-774.5 501,-771 491,-767.5 491,-774.5"/>
</g>
<!-- svc:traefik&#45;&gt;svc:traefik -->
<g id="edge30" class="edge">
<title>svc:traefik&#45;&gt;svc:traefik</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M511.86,-789.35C485.91,-817.82 493.95,-854 536,-854 574.1,-854 584.29,-824.29 566.55,-797.52"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="569.07,-795.06 560.14,-789.35 563.55,-799.38 569.07,-795.06"/>
</g>
<!-- svc:authelia -->
<g id="node3" class="node">
<title>svc:authelia</title>
<path fill="#dcfce7" stroke="black" d="M763,-789C763,-789 726,-789 726,-789 720,-789 714,-783 714,-777 714,-777 714,-765 714,-765 714,-759 720,-753 726,-753 726,-753 763,-753 763,-753 769,-753 775,-759 775,-765 775,-765 775,-777 775,-777 775,-783 769,-789 763,-789"/>
<text text-anchor="middle" x="744.5" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">authelia</text>
<text text-anchor="middle" x="744.5" y="-762.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:authelia -->
<g id="edge2" class="edge">
<title>svc:traefik&#45;&gt;svc:authelia</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-771C607.43,-771 664.91,-771 703.39,-771"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="703.71,-774.5 713.71,-771 703.71,-767.5 703.71,-774.5"/>
</g>
<!-- svc:gitea -->
<g id="node5" class="node">
<title>svc:gitea</title>
<path fill="#dcfce7" stroke="black" d="M759.5,-688C759.5,-688 729.5,-688 729.5,-688 723.5,-688 717.5,-682 717.5,-676 717.5,-676 717.5,-656 717.5,-656 717.5,-650 723.5,-644 729.5,-644 729.5,-644 759.5,-644 759.5,-644 765.5,-644 771.5,-650 771.5,-656 771.5,-656 771.5,-676 771.5,-676 771.5,-682 765.5,-688 759.5,-688"/>
<text text-anchor="middle" x="744.5" y="-675.2" font-family="Helvetica,sans-Serif" font-size="11.00">gitea</text>
<text text-anchor="middle" x="744.5" y="-663.2" font-family="Helvetica,sans-Serif" font-size="11.00">:3000</text>
<text text-anchor="middle" x="744.5" y="-651.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:gitea -->
<g id="edge4" class="edge">
<title>svc:traefik&#45;&gt;svc:gitea</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-753.66C608.91,-734.44 669.61,-703.57 707.98,-684.06"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="709.89,-687.01 717.22,-679.36 706.72,-680.77 709.89,-687.01"/>
</g>
<!-- svc:gotify -->
<g id="node7" class="node">
<title>svc:gotify</title>
<path fill="#dcfce7" stroke="black" d="M759.5,-579C759.5,-579 729.5,-579 729.5,-579 723.5,-579 717.5,-573 717.5,-567 717.5,-567 717.5,-555 717.5,-555 717.5,-549 723.5,-543 729.5,-543 729.5,-543 759.5,-543 759.5,-543 765.5,-543 771.5,-549 771.5,-555 771.5,-555 771.5,-567 771.5,-567 771.5,-573 765.5,-579 759.5,-579"/>
<text text-anchor="middle" x="744.5" y="-564.2" font-family="Helvetica,sans-Serif" font-size="11.00">gotify</text>
<text text-anchor="middle" x="744.5" y="-552.2" font-family="Helvetica,sans-Serif" font-size="11.00">:80</text>
</g>
<!-- svc:traefik&#45;&gt;svc:gotify -->
<g id="edge6" class="edge">
<title>svc:traefik&#45;&gt;svc:gotify</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M553.8,-752.96C592.77,-711.11 686,-611 686,-611 686,-611 700.43,-598.45 714.83,-585.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-588.25 722.74,-579.05 712.9,-582.97 717.49,-588.25"/>
</g>
<!-- svc:grafana -->
<g id="node9" class="node">
<title>svc:grafana</title>
<path fill="#dcfce7" stroke="black" d="M762,-999C762,-999 727,-999 727,-999 721,-999 715,-993 715,-987 715,-987 715,-975 715,-975 715,-969 721,-963 727,-963 727,-963 762,-963 762,-963 768,-963 774,-969 774,-975 774,-975 774,-987 774,-987 774,-993 768,-999 762,-999"/>
<text text-anchor="middle" x="744.5" y="-984.2" font-family="Helvetica,sans-Serif" font-size="11.00">grafana</text>
<text text-anchor="middle" x="744.5" y="-972.2" font-family="Helvetica,sans-Serif" font-size="11.00">:3000</text>
</g>
<!-- svc:traefik&#45;&gt;svc:grafana -->
<g id="edge8" class="edge">
<title>svc:traefik&#45;&gt;svc:grafana</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M553.8,-789.04C592.77,-830.89 686,-931 686,-931 686,-931 700.43,-943.55 714.83,-956.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-959.03 722.74,-962.95 717.49,-953.75 712.9,-959.03"/>
</g>
<!-- svc:grampsweb -->
<g id="node11" class="node">
<title>svc:grampsweb</title>
<path fill="#dcfce7" stroke="black" d="M772.5,-1528C772.5,-1528 716.5,-1528 716.5,-1528 710.5,-1528 704.5,-1522 704.5,-1516 704.5,-1516 704.5,-1504 704.5,-1504 704.5,-1498 710.5,-1492 716.5,-1492 716.5,-1492 772.5,-1492 772.5,-1492 778.5,-1492 784.5,-1498 784.5,-1504 784.5,-1504 784.5,-1516 784.5,-1516 784.5,-1522 778.5,-1528 772.5,-1528"/>
<text text-anchor="middle" x="744.5" y="-1507.2" font-family="Helvetica,sans-Serif" font-size="11.00">grampsweb</text>
</g>
<!-- svc:traefik&#45;&gt;svc:grampsweb -->
<g id="edge10" class="edge">
<title>svc:traefik&#45;&gt;svc:grampsweb</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M540.91,-789.07C564.42,-897.78 686,-1460 686,-1460 686,-1460 700.43,-1472.55 714.83,-1485.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-1488.03 722.74,-1491.95 717.49,-1482.75 712.9,-1488.03"/>
</g>
<!-- svc:influxdb -->
<g id="node13" class="node">
<title>svc:influxdb</title>
<path fill="#dcfce7" stroke="black" d="M767.5,-898C767.5,-898 721.5,-898 721.5,-898 715.5,-898 709.5,-892 709.5,-886 709.5,-886 709.5,-866 709.5,-866 709.5,-860 715.5,-854 721.5,-854 721.5,-854 767.5,-854 767.5,-854 773.5,-854 779.5,-860 779.5,-866 779.5,-866 779.5,-886 779.5,-886 779.5,-892 773.5,-898 767.5,-898"/>
<text text-anchor="middle" x="744.5" y="-885.2" font-family="Helvetica,sans-Serif" font-size="11.00">influxdb</text>
<text text-anchor="middle" x="744.5" y="-873.2" font-family="Helvetica,sans-Serif" font-size="11.00">:8086</text>
<text text-anchor="middle" x="744.5" y="-861.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:influxdb -->
<g id="edge12" class="edge">
<title>svc:traefik&#45;&gt;svc:influxdb</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M571.1,-788.34C606.37,-806.27 661.57,-834.34 699.97,-853.87"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="698.85,-857.22 709.35,-858.64 702.02,-850.98 698.85,-857.22"/>
</g>
<!-- svc:monitor&#45;kuma -->
<g id="node15" class="node">
<title>svc:monitor&#45;kuma</title>
<path fill="#dcfce7" stroke="black" d="M778.5,-1427C778.5,-1427 710.5,-1427 710.5,-1427 704.5,-1427 698.5,-1421 698.5,-1415 698.5,-1415 698.5,-1403 698.5,-1403 698.5,-1397 704.5,-1391 710.5,-1391 710.5,-1391 778.5,-1391 778.5,-1391 784.5,-1391 790.5,-1397 790.5,-1403 790.5,-1403 790.5,-1415 790.5,-1415 790.5,-1421 784.5,-1427 778.5,-1427"/>
<text text-anchor="middle" x="744.5" y="-1412.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor&#45;kuma</text>
<text text-anchor="middle" x="744.5" y="-1400.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:monitor&#45;kuma -->
<g id="edge14" class="edge">
<title>svc:traefik&#45;&gt;svc:monitor&#45;kuma</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M541.62,-789.24C566.77,-888.49 686,-1359 686,-1359 686,-1359 700.43,-1371.55 714.83,-1384.07"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="712.9,-1387.03 722.74,-1390.95 717.49,-1381.75 712.9,-1387.03"/>
</g>
<!-- svc:mtls&#45;bridge -->
<g id="node17" class="node">
<title>svc:mtls&#45;bridge</title>
<path fill="#dcfce7" stroke="black" d="M772,-1326C772,-1326 717,-1326 717,-1326 711,-1326 705,-1320 705,-1314 705,-1314 705,-1294 705,-1294 705,-1288 711,-1282 717,-1282 717,-1282 772,-1282 772,-1282 778,-1282 784,-1288 784,-1294 784,-1294 784,-1314 784,-1314 784,-1320 778,-1326 772,-1326"/>
<text text-anchor="middle" x="744.5" y="-1313.2" font-family="Helvetica,sans-Serif" font-size="11.00">mtls&#45;bridge</text>
<text text-anchor="middle" x="744.5" y="-1301.2" font-family="Helvetica,sans-Serif" font-size="11.00">:8080</text>
<text text-anchor="middle" x="744.5" y="-1289.2" font-family="Helvetica,sans-Serif" font-size="11.00">[mTLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:mtls&#45;bridge -->
<g id="edge16" class="edge">
<title>svc:traefik&#45;&gt;svc:mtls&#45;bridge</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M542.66,-789.19C569.88,-876.69 686,-1250 686,-1250 686,-1250 698.77,-1262 712.28,-1274.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1277.43 719.78,-1281.72 714.89,-1272.33 710.1,-1277.43"/>
</g>
<!-- svc:nextcloud&#45;webapp -->
<g id="node19" class="node">
<title>svc:nextcloud&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M791,-137C791,-137 698,-137 698,-137 692,-137 686,-131 686,-125 686,-125 686,-113 686,-113 686,-107 692,-101 698,-101 698,-101 791,-101 791,-101 797,-101 803,-107 803,-113 803,-113 803,-125 803,-125 803,-131 797,-137 791,-137"/>
<text text-anchor="middle" x="744.5" y="-116.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:nextcloud&#45;webapp -->
<g id="edge18" class="edge">
<title>svc:traefik&#45;&gt;svc:nextcloud&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M541.62,-752.85C566.77,-654.11 686,-186 686,-186 686,-186 704.96,-163.91 721.1,-145.1"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="723.93,-147.18 727.79,-137.31 718.62,-142.62 723.93,-147.18"/>
</g>
<!-- svc:node&#45;red -->
<g id="node21" class="node">
<title>svc:node&#45;red</title>
<path fill="#dcfce7" stroke="black" d="M767.5,-1217C767.5,-1217 721.5,-1217 721.5,-1217 715.5,-1217 709.5,-1211 709.5,-1205 709.5,-1205 709.5,-1185 709.5,-1185 709.5,-1179 715.5,-1173 721.5,-1173 721.5,-1173 767.5,-1173 767.5,-1173 773.5,-1173 779.5,-1179 779.5,-1185 779.5,-1185 779.5,-1205 779.5,-1205 779.5,-1211 773.5,-1217 767.5,-1217"/>
<text text-anchor="middle" x="744.5" y="-1204.2" font-family="Helvetica,sans-Serif" font-size="11.00">node&#45;red</text>
<text text-anchor="middle" x="744.5" y="-1192.2" font-family="Helvetica,sans-Serif" font-size="11.00">:1880</text>
<text text-anchor="middle" x="744.5" y="-1180.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:node&#45;red -->
<g id="edge20" class="edge">
<title>svc:traefik&#45;&gt;svc:node&#45;red</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M544.29,-789.1C574.2,-863.38 686,-1141 686,-1141 686,-1141 698.77,-1153 712.28,-1165.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1168.43 719.78,-1172.72 714.89,-1163.33 710.1,-1168.43"/>
</g>
<!-- svc:passbolt&#45;webapp -->
<g id="node23" class="node">
<title>svc:passbolt&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M787.5,-36C787.5,-36 701.5,-36 701.5,-36 695.5,-36 689.5,-30 689.5,-24 689.5,-24 689.5,-12 689.5,-12 689.5,-6 695.5,0 701.5,0 701.5,0 787.5,0 787.5,0 793.5,0 799.5,-6 799.5,-12 799.5,-12 799.5,-24 799.5,-24 799.5,-30 793.5,-36 787.5,-36"/>
<text text-anchor="middle" x="744.5" y="-15.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:passbolt&#45;webapp -->
<g id="edge22" class="edge">
<title>svc:traefik&#45;&gt;svc:passbolt&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M540.83,-752.92C564.15,-642.88 686,-68 686,-68 686,-68 700.43,-55.45 714.83,-42.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-45.25 722.74,-36.05 712.9,-39.97 717.49,-45.25"/>
</g>
<!-- svc:portainer -->
<g id="node25" class="node">
<title>svc:portainer</title>
<path fill="#dcfce7" stroke="black" d="M766,-478C766,-478 723,-478 723,-478 717,-478 711,-472 711,-466 711,-466 711,-446 711,-446 711,-440 717,-434 723,-434 723,-434 766,-434 766,-434 772,-434 778,-440 778,-446 778,-446 778,-466 778,-466 778,-472 772,-478 766,-478"/>
<text text-anchor="middle" x="744.5" y="-465.2" font-family="Helvetica,sans-Serif" font-size="11.00">portainer</text>
<text text-anchor="middle" x="744.5" y="-453.2" font-family="Helvetica,sans-Serif" font-size="11.00">:9000</text>
<text text-anchor="middle" x="744.5" y="-441.2" font-family="Helvetica,sans-Serif" font-size="11.00">[TLS]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:portainer -->
<g id="edge24" class="edge">
<title>svc:traefik&#45;&gt;svc:portainer</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M547.48,-752.65C581.39,-693.24 686,-510 686,-510 686,-510 698.77,-498 712.28,-485.32"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="714.89,-487.67 719.78,-478.28 710.1,-482.57 714.89,-487.67"/>
</g>
<!-- svc:prometheus -->
<g id="node27" class="node">
<title>svc:prometheus</title>
<path fill="#dcfce7" stroke="black" d="M774,-1108C774,-1108 715,-1108 715,-1108 709,-1108 703,-1102 703,-1096 703,-1096 703,-1076 703,-1076 703,-1070 709,-1064 715,-1064 715,-1064 774,-1064 774,-1064 780,-1064 786,-1070 786,-1076 786,-1076 786,-1096 786,-1096 786,-1102 780,-1108 774,-1108"/>
<text text-anchor="middle" x="744.5" y="-1095.2" font-family="Helvetica,sans-Serif" font-size="11.00">prometheus</text>
<text text-anchor="middle" x="744.5" y="-1083.2" font-family="Helvetica,sans-Serif" font-size="11.00">:9090</text>
<text text-anchor="middle" x="744.5" y="-1071.2" font-family="Helvetica,sans-Serif" font-size="11.00">[authelia]</text>
</g>
<!-- svc:traefik&#45;&gt;svc:prometheus -->
<g id="edge26" class="edge">
<title>svc:traefik&#45;&gt;svc:prometheus</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M547.48,-789.35C581.39,-848.76 686,-1032 686,-1032 686,-1032 698.77,-1044 712.28,-1056.68"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="710.1,-1059.43 719.78,-1063.72 714.89,-1054.33 710.1,-1059.43"/>
</g>
<!-- svc:searxng&#45;webapp -->
<g id="node29" class="node">
<title>svc:searxng&#45;webapp</title>
<path fill="#dcfce7" stroke="black" d="M786,-369C786,-369 703,-369 703,-369 697,-369 691,-363 691,-357 691,-357 691,-345 691,-345 691,-339 697,-333 703,-333 703,-333 786,-333 786,-333 792,-333 798,-339 798,-345 798,-345 798,-357 798,-357 798,-363 792,-369 786,-369"/>
<text text-anchor="middle" x="744.5" y="-348.2" font-family="Helvetica,sans-Serif" font-size="11.00">searxng&#45;webapp</text>
</g>
<!-- svc:traefik&#45;&gt;svc:searxng&#45;webapp -->
<g id="edge28" class="edge">
<title>svc:traefik&#45;&gt;svc:searxng&#45;webapp</title>
<path fill="none" stroke="#334155" stroke-width="1.4" d="M544.29,-752.9C574.2,-678.62 686,-401 686,-401 686,-401 700.43,-388.45 714.83,-375.93"/>
<polygon fill="#334155" stroke="#334155" stroke-width="1.4" points="717.49,-378.25 722.74,-369.05 712.9,-372.97 717.49,-378.25"/>
</g>
<!-- net:traefik -->
<g id="node36" class="node">
<title>net:traefik</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-878" rx="31.04" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-875.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik</text>
</g>
<!-- svc:traefik&#45;&gt;net:traefik -->
<g id="edge55" class="edge">
<title>svc:traefik&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M542.75,-752.82C570.13,-666.27 686,-300 686,-300 686,-300 803,-300 803,-300 803,-300 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- svc:authelia&#45;&gt;net:traefik -->
<g id="edge32" class="edge">
<title>svc:authelia&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M775.21,-783.17C824.86,-803.51 924.25,-844.23 975.12,-865.06"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="974.26,-867.36 981.67,-867.75 976.12,-862.83 974.26,-867.36"/>
</g>
<!-- dns:auth.&lt;domain&gt; -->
<g id="node4" class="node">
<title>dns:auth.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="123.5,-1496 25.5,-1496 25.5,-1460 129.5,-1460 129.5,-1490 123.5,-1496"/>
<polyline fill="none" stroke="black" points="123.5,-1496 123.5,-1490 "/>
<polyline fill="none" stroke="black" points="129.5,-1490 123.5,-1490 "/>
<text text-anchor="middle" x="77.5" y="-1475.2" font-family="Helvetica,sans-Serif" font-size="11.00">auth.&lt;domain&gt;</text>
</g>
<!-- dns:auth.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge3" class="edge">
<title>dns:auth.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1459.95C128.03,-1445.63 155,-1428 155,-1428 155,-1428 286.65,-925.11 319.6,-799.28"/>
<polygon fill="#334155" stroke="#334155" points="323.07,-799.82 322.22,-789.26 316.3,-798.05 323.07,-799.82"/>
</g>
<!-- svc:gitea&#45;&gt;net:traefik -->
<g id="edge33" class="edge">
<title>svc:gitea&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M768.56,-688.05C784.55,-703.36 803,-721 803,-721 803,-721 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:gitea.&lt;domain&gt; -->
<g id="node6" class="node">
<title>dns:gitea.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="125,-1395 24,-1395 24,-1359 131,-1359 131,-1389 125,-1395"/>
<polyline fill="none" stroke="black" points="125,-1395 125,-1389 "/>
<polyline fill="none" stroke="black" points="131,-1389 125,-1389 "/>
<text text-anchor="middle" x="77.5" y="-1374.2" font-family="Helvetica,sans-Serif" font-size="11.00">gitea.&lt;domain&gt;</text>
</g>
<!-- dns:gitea.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge5" class="edge">
<title>dns:gitea.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1358.95C128.03,-1344.63 155,-1327 155,-1327 155,-1327 283.58,-911.36 318.4,-798.82"/>
<polygon fill="#334155" stroke="#334155" points="321.76,-799.77 321.38,-789.18 315.08,-797.7 321.76,-799.77"/>
</g>
<!-- svc:gotify&#45;&gt;net:traefik -->
<g id="edge34" class="edge">
<title>svc:gotify&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-579.05C782.73,-593.37 803,-611 803,-611 803,-611 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:gotify.&lt;domain&gt; -->
<g id="node8" class="node">
<title>dns:gotify.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="126,-1294 23,-1294 23,-1258 132,-1258 132,-1288 126,-1294"/>
<polyline fill="none" stroke="black" points="126,-1294 126,-1288 "/>
<polyline fill="none" stroke="black" points="132,-1288 126,-1288 "/>
<text text-anchor="middle" x="77.5" y="-1273.2" font-family="Helvetica,sans-Serif" font-size="11.00">gotify.&lt;domain&gt;</text>
</g>
<!-- dns:gotify.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge7" class="edge">
<title>dns:gotify.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1257.95C128.03,-1243.63 155,-1226 155,-1226 155,-1226 279.21,-897.41 316.53,-798.71"/>
<polygon fill="#334155" stroke="#334155" points="319.89,-799.71 320.15,-789.12 313.34,-797.23 319.89,-799.71"/>
</g>
<!-- net:monitor -->
<g id="node33" class="node">
<title>net:monitor</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-979" rx="35.46" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-976.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor</text>
</g>
<!-- svc:grafana&#45;&gt;net:monitor -->
<g id="edge35" class="edge">
<title>svc:grafana&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M774.2,-980.78C820.4,-980.43 911.47,-979.73 964.89,-979.32"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="965.19,-981.77 972.17,-979.26 965.15,-976.87 965.19,-981.77"/>
</g>
<!-- svc:grafana&#45;&gt;net:traefik -->
<g id="edge36" class="edge">
<title>svc:grafana&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M774.2,-969.68C823.41,-950.28 923.53,-910.8 974.83,-890.57"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="975.82,-892.81 981.44,-887.96 974.03,-888.25 975.82,-892.81"/>
</g>
<!-- dns:grafana.&lt;domain&gt; -->
<g id="node10" class="node">
<title>dns:grafana.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="132,-1193 17,-1193 17,-1157 138,-1157 138,-1187 132,-1193"/>
<polyline fill="none" stroke="black" points="132,-1193 132,-1187 "/>
<polyline fill="none" stroke="black" points="138,-1187 132,-1187 "/>
<text text-anchor="middle" x="77.5" y="-1172.2" font-family="Helvetica,sans-Serif" font-size="11.00">grafana.&lt;domain&gt;</text>
</g>
<!-- dns:grafana.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge9" class="edge">
<title>dns:grafana.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1156.95C128.03,-1142.63 155,-1125 155,-1125 155,-1125 273.61,-880.89 313.84,-798.09"/>
<polygon fill="#334155" stroke="#334155" points="317.01,-799.57 318.23,-789.04 310.72,-796.51 317.01,-799.57"/>
</g>
<!-- net:gramps -->
<g id="node32" class="node">
<title>net:gramps</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-1080" rx="34.76" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-1077.2" font-family="Helvetica,sans-Serif" font-size="11.00">gramps</text>
</g>
<!-- svc:grampsweb&#45;&gt;net:gramps -->
<g id="edge37" class="edge">
<title>svc:grampsweb&#45;&gt;net:gramps</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1491.95C782.73,-1477.63 803,-1460 803,-1460 803,-1460 949.23,-1187.21 993.89,-1103.88"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="996.27,-1104.64 997.41,-1097.32 991.95,-1102.33 996.27,-1104.64"/>
</g>
<!-- svc:grampsweb&#45;&gt;net:traefik -->
<g id="edge38" class="edge">
<title>svc:grampsweb&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1491.95C782.73,-1477.63 803,-1460 803,-1460 803,-1460 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:familytree.&lt;domain&gt; -->
<g id="node12" class="node">
<title>dns:familytree.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="139,-1092 10,-1092 10,-1056 145,-1056 145,-1086 139,-1092"/>
<polyline fill="none" stroke="black" points="139,-1092 139,-1086 "/>
<polyline fill="none" stroke="black" points="145,-1086 139,-1086 "/>
<text text-anchor="middle" x="77.5" y="-1071.2" font-family="Helvetica,sans-Serif" font-size="11.00">familytree.&lt;domain&gt;</text>
</g>
<!-- dns:familytree.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge11" class="edge">
<title>dns:familytree.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-1055.95C128.03,-1041.63 155,-1024 155,-1024 155,-1024 264.64,-862.73 308.84,-797.71"/>
<polygon fill="#334155" stroke="#334155" points="311.9,-799.43 314.63,-789.2 306.11,-795.5 311.9,-799.43"/>
</g>
<!-- svc:influxdb&#45;&gt;net:monitor -->
<g id="edge39" class="edge">
<title>svc:influxdb&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M779.65,-889.47C829.7,-909.2 922.46,-945.78 972.53,-965.53"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="971.89,-967.91 979.3,-968.2 973.69,-963.35 971.89,-967.91"/>
</g>
<!-- svc:influxdb&#45;&gt;net:traefik -->
<g id="edge40" class="edge">
<title>svc:influxdb&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M779.65,-876.26C828.54,-876.64 918.2,-877.32 968.99,-877.71"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="969.17,-880.16 976.19,-877.77 969.21,-875.26 969.17,-880.16"/>
</g>
<!-- dns:influxdb.&lt;domain&gt; -->
<g id="node14" class="node">
<title>dns:influxdb.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="132.5,-991 16.5,-991 16.5,-955 138.5,-955 138.5,-985 132.5,-991"/>
<polyline fill="none" stroke="black" points="132.5,-991 132.5,-985 "/>
<polyline fill="none" stroke="black" points="138.5,-985 132.5,-985 "/>
<text text-anchor="middle" x="77.5" y="-970.2" font-family="Helvetica,sans-Serif" font-size="11.00">influxdb.&lt;domain&gt;</text>
</g>
<!-- dns:influxdb.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge13" class="edge">
<title>dns:influxdb.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-954.95C128.03,-940.63 155,-923 155,-923 155,-923 250.14,-838.92 298.91,-795.83"/>
<polygon fill="#334155" stroke="#334155" points="301.42,-798.28 306.59,-789.03 296.78,-793.03 301.42,-798.28"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:monitor -->
<g id="edge41" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1390.95C782.73,-1376.63 803,-1359 803,-1359 803,-1359 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:monitor&#45;kuma&#45;&gt;net:traefik -->
<g id="edge42" class="edge">
<title>svc:monitor&#45;kuma&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-1390.95C782.73,-1376.63 803,-1359 803,-1359 803,-1359 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:monitor&#45;kuma.&lt;domain&gt; -->
<g id="node16" class="node">
<title>dns:monitor&#45;kuma.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="149,-890 0,-890 0,-854 155,-854 155,-884 149,-890"/>
<polyline fill="none" stroke="black" points="149,-890 149,-884 "/>
<polyline fill="none" stroke="black" points="155,-884 149,-884 "/>
<text text-anchor="middle" x="77.5" y="-869.2" font-family="Helvetica,sans-Serif" font-size="11.00">monitor&#45;kuma.&lt;domain&gt;</text>
</g>
<!-- dns:monitor&#45;kuma.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge15" class="edge">
<title>dns:monitor&#45;kuma.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M122.93,-853.94C165.02,-836.83 228.32,-811.11 273.25,-792.85"/>
<polygon fill="#334155" stroke="#334155" points="274.59,-796.08 282.54,-789.07 271.96,-789.59 274.59,-796.08"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:monitor -->
<g id="edge43" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1281.72C785.06,-1266.85 803,-1250 803,-1250 803,-1250 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:mtls&#45;bridge&#45;&gt;net:traefik -->
<g id="edge44" class="edge">
<title>svc:mtls&#45;bridge&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1281.72C785.06,-1266.85 803,-1250 803,-1250 803,-1250 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:mtls&#45;bridge.&lt;domain&gt; -->
<g id="node18" class="node">
<title>dns:mtls&#45;bridge.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="142,-789 7,-789 7,-753 148,-753 148,-783 142,-789"/>
<polyline fill="none" stroke="black" points="142,-789 142,-783 "/>
<polyline fill="none" stroke="black" points="148,-783 142,-783 "/>
<text text-anchor="middle" x="77.5" y="-768.2" font-family="Helvetica,sans-Serif" font-size="11.00">mtls&#45;bridge.&lt;domain&gt;</text>
</g>
<!-- dns:mtls&#45;bridge.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge17" class="edge">
<title>dns:mtls&#45;bridge.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M148.05,-771C182.94,-771 225.03,-771 259.61,-771"/>
<polygon fill="#334155" stroke="#334155" points="259.62,-774.5 269.62,-771 259.62,-767.5 259.62,-774.5"/>
</g>
<!-- net:nextcloud -->
<g id="node34" class="node">
<title>net:nextcloud</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-777" rx="42.89" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-774.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud</text>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud -->
<g id="edge45" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:nextcloud</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M761.21,-137.31C778.24,-157.15 803,-186 803,-186 803,-186 910,-727 910,-727 910,-727 945.48,-745.35 973.46,-759.81"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="972.67,-762.17 980.02,-763.2 974.92,-757.81 972.67,-762.17"/>
</g>
<!-- svc:nextcloud&#45;webapp&#45;&gt;net:traefik -->
<g id="edge46" class="edge">
<title>svc:nextcloud&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M761.21,-137.31C778.24,-157.15 803,-186 803,-186 803,-186 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:nextcloud.&lt;domain&gt; -->
<g id="node20" class="node">
<title>dns:nextcloud.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="138,-688 11,-688 11,-652 144,-652 144,-682 138,-688"/>
<polyline fill="none" stroke="black" points="138,-688 138,-682 "/>
<polyline fill="none" stroke="black" points="144,-682 138,-682 "/>
<text text-anchor="middle" x="77.5" y="-667.2" font-family="Helvetica,sans-Serif" font-size="11.00">nextcloud.&lt;domain&gt;</text>
</g>
<!-- dns:nextcloud.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge19" class="edge">
<title>dns:nextcloud.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M122.93,-688.06C165.02,-705.17 228.32,-730.89 273.25,-749.15"/>
<polygon fill="#334155" stroke="#334155" points="271.96,-752.41 282.54,-752.93 274.59,-745.92 271.96,-752.41"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:monitor -->
<g id="edge47" class="edge">
<title>svc:node&#45;red&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1172.72C785.06,-1157.85 803,-1141 803,-1141 803,-1141 910,-1030 910,-1030 910,-1030 947.73,-1010.1 976.05,-995.16"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="977.25,-997.3 982.29,-991.87 974.96,-992.97 977.25,-997.3"/>
</g>
<!-- svc:node&#45;red&#45;&gt;net:traefik -->
<g id="edge48" class="edge">
<title>svc:node&#45;red&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1172.72C785.06,-1157.85 803,-1141 803,-1141 803,-1141 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:node&#45;red.&lt;domain&gt; -->
<g id="node22" class="node">
<title>dns:node&#45;red.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="135.5,-587 13.5,-587 13.5,-551 141.5,-551 141.5,-581 135.5,-587"/>
<polyline fill="none" stroke="black" points="135.5,-587 135.5,-581 "/>
<polyline fill="none" stroke="black" points="141.5,-581 135.5,-581 "/>
<text text-anchor="middle" x="77.5" y="-566.2" font-family="Helvetica,sans-Serif" font-size="11.00">node&#45;red.&lt;domain&gt;</text>
</g>
<!-- dns:node&#45;red.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge21" class="edge">
<title>dns:node&#45;red.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-587.05C128.03,-601.37 155,-619 155,-619 155,-619 250.14,-703.08 298.91,-746.17"/>
<polygon fill="#334155" stroke="#334155" points="296.78,-748.97 306.59,-752.97 301.42,-743.72 296.78,-748.97"/>
</g>
<!-- net:passbolt -->
<g id="node35" class="node">
<title>net:passbolt</title>
<ellipse fill="#f8fafc" stroke="black" cx="1007.69" cy="-676" rx="37.77" ry="18"/>
<text text-anchor="middle" x="1007.69" y="-673.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt</text>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:passbolt -->
<g id="edge49" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:passbolt</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-36.05C782.73,-50.37 803,-68 803,-68 803,-68 960.4,-537.83 998.48,-651.47"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="996.16,-652.27 1000.71,-658.13 1000.81,-650.72 996.16,-652.27"/>
</g>
<!-- svc:passbolt&#45;webapp&#45;&gt;net:traefik -->
<g id="edge50" class="edge">
<title>svc:passbolt&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-36.05C782.73,-50.37 803,-68 803,-68 803,-68 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:passbolt.&lt;domain&gt; -->
<g id="node24" class="node">
<title>dns:passbolt.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="134,-486 15,-486 15,-450 140,-450 140,-480 134,-486"/>
<polyline fill="none" stroke="black" points="134,-486 134,-480 "/>
<polyline fill="none" stroke="black" points="140,-480 134,-480 "/>
<text text-anchor="middle" x="77.5" y="-465.2" font-family="Helvetica,sans-Serif" font-size="11.00">passbolt.&lt;domain&gt;</text>
</g>
<!-- dns:passbolt.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge23" class="edge">
<title>dns:passbolt.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-486.05C128.03,-500.37 155,-518 155,-518 155,-518 264.64,-679.27 308.84,-744.29"/>
<polygon fill="#334155" stroke="#334155" points="306.11,-746.5 314.63,-752.8 311.9,-742.57 306.11,-746.5"/>
</g>
<!-- svc:portainer&#45;&gt;net:traefik -->
<g id="edge51" class="edge">
<title>svc:portainer&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-478.28C785.06,-493.15 803,-510 803,-510 803,-510 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:portainer.&lt;domain&gt; -->
<g id="node26" class="node">
<title>dns:portainer.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="135.5,-385 13.5,-385 13.5,-349 141.5,-349 141.5,-379 135.5,-385"/>
<polyline fill="none" stroke="black" points="135.5,-385 135.5,-379 "/>
<polyline fill="none" stroke="black" points="141.5,-379 135.5,-379 "/>
<text text-anchor="middle" x="77.5" y="-364.2" font-family="Helvetica,sans-Serif" font-size="11.00">portainer.&lt;domain&gt;</text>
</g>
<!-- dns:portainer.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge25" class="edge">
<title>dns:portainer.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-385.05C128.03,-399.37 155,-417 155,-417 155,-417 273.61,-661.11 313.84,-743.91"/>
<polygon fill="#334155" stroke="#334155" points="310.72,-745.49 318.23,-752.96 317.01,-742.43 310.72,-745.49"/>
</g>
<!-- svc:prometheus&#45;&gt;net:monitor -->
<g id="edge52" class="edge">
<title>svc:prometheus&#45;&gt;net:monitor</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M786.03,-1069.4C837.48,-1048.32 925.31,-1012.34 973.18,-992.73"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="974.11,-995 979.66,-990.07 972.26,-990.46 974.11,-995"/>
</g>
<!-- svc:prometheus&#45;&gt;net:traefik -->
<g id="edge53" class="edge">
<title>svc:prometheus&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M769.22,-1063.72C785.06,-1048.85 803,-1032 803,-1032 803,-1032 910,-929 910,-929 910,-929 949.05,-908.4 977.52,-893.39"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="978.73,-895.52 983.78,-890.09 976.45,-891.18 978.73,-895.52"/>
</g>
<!-- dns:prometheus.&lt;domain&gt; -->
<g id="node28" class="node">
<title>dns:prometheus.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="144,-284 5,-284 5,-248 150,-248 150,-278 144,-284"/>
<polyline fill="none" stroke="black" points="144,-284 144,-278 "/>
<polyline fill="none" stroke="black" points="150,-278 144,-278 "/>
<text text-anchor="middle" x="77.5" y="-263.2" font-family="Helvetica,sans-Serif" font-size="11.00">prometheus.&lt;domain&gt;</text>
</g>
<!-- dns:prometheus.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge27" class="edge">
<title>dns:prometheus.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-284.05C128.03,-298.37 155,-316 155,-316 155,-316 279.21,-644.59 316.53,-743.29"/>
<polygon fill="#334155" stroke="#334155" points="313.34,-744.77 320.15,-752.88 319.89,-742.29 313.34,-744.77"/>
</g>
<!-- svc:searxng&#45;webapp&#45;&gt;net:traefik -->
<g id="edge54" class="edge">
<title>svc:searxng&#45;webapp&#45;&gt;net:traefik</title>
<path fill="none" stroke="#94a3b8" stroke-dasharray="5,2" d="M766.26,-369.05C782.73,-383.37 803,-401 803,-401 803,-401 910,-828 910,-828 910,-828 949.05,-848.19 977.52,-862.92"/>
<polygon fill="#94a3b8" stroke="#94a3b8" points="976.44,-865.11 983.78,-866.15 978.69,-860.76 976.44,-865.11"/>
</g>
<!-- dns:searxng.&lt;domain&gt; -->
<g id="node30" class="node">
<title>dns:searxng.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="133,-183 16,-183 16,-147 139,-147 139,-177 133,-183"/>
<polyline fill="none" stroke="black" points="133,-183 133,-177 "/>
<polyline fill="none" stroke="black" points="139,-177 133,-177 "/>
<text text-anchor="middle" x="77.5" y="-162.2" font-family="Helvetica,sans-Serif" font-size="11.00">searxng.&lt;domain&gt;</text>
</g>
<!-- dns:searxng.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge29" class="edge">
<title>dns:searxng.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-183.05C128.03,-197.37 155,-215 155,-215 155,-215 283.58,-630.64 318.4,-743.18"/>
<polygon fill="#334155" stroke="#334155" points="315.08,-744.3 321.38,-752.82 321.76,-742.23 315.08,-744.3"/>
</g>
<!-- dns:traefik.&lt;domain&gt; -->
<g id="node31" class="node">
<title>dns:traefik.&lt;domain&gt;</title>
<polygon fill="#fef3c7" stroke="black" points="128.5,-82 20.5,-82 20.5,-46 134.5,-46 134.5,-76 128.5,-82"/>
<polyline fill="none" stroke="black" points="128.5,-82 128.5,-76 "/>
<polyline fill="none" stroke="black" points="134.5,-76 128.5,-76 "/>
<text text-anchor="middle" x="77.5" y="-61.2" font-family="Helvetica,sans-Serif" font-size="11.00">traefik.&lt;domain&gt;</text>
</g>
<!-- dns:traefik.&lt;domain&gt;&#45;&gt;dynu -->
<g id="edge31" class="edge">
<title>dns:traefik.&lt;domain&gt;&#45;&gt;dynu</title>
<path fill="none" stroke="#334155" d="M106.12,-82.05C128.03,-96.37 155,-114 155,-114 155,-114 286.65,-616.89 319.6,-742.72"/>
<polygon fill="#334155" stroke="#334155" points="316.3,-743.95 322.22,-752.74 323.07,-742.18 316.3,-743.95"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 41 KiB

+21
View File
@@ -0,0 +1,21 @@
# Public Infrastructure Summary
This documentation is generated from the infrastructure repository. Sensitive values are redacted.
> Generated docs are sanitised/redacted before publishing to GitHub Pages.
## Infrastructure diagrams
### Physical / virtual topology
![Physical topology](physical-topology.svg)
### Docker, Traefik and Dynu routing
![Docker Traefik Dynu](docker-traefik-dynu.svg)
## Documents
- [Diagrams](diagrams.md)
- [Compose Inventory](compose-inventory.md)
- [Traefik Routes](traefik-routes.md)
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: PhysicalTopology Pages: 1 -->
<svg width="514pt" height="61pt"
viewBox="0.00 0.00 514.00 61.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 57)">
<title>PhysicalTopology</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-57 510,-57 510,4 -4,4"/>
<!-- placeholder:inventory -->
<g id="node1" class="node">
<title>placeholder:inventory</title>
<polygon fill="#fef3c7" stroke="black" points="500,-53 0,-53 0,0 506,0 506,-47 500,-53"/>
<polyline fill="none" stroke="black" points="500,-53 500,-47 "/>
<polyline fill="none" stroke="black" points="506,-47 500,-47 "/>
<text text-anchor="middle" x="253" y="-37.8" font-family="Times,serif" font-size="14.00">Host inventory JSON not found.</text>
<text text-anchor="middle" x="253" y="-22.8" font-family="Times,serif" font-size="14.00">Generate terraform inventory and rerun scripts/docs/generate&#45;all.sh</text>
<text text-anchor="middle" x="253" y="-7.8" font-family="Times,serif" font-size="14.00">(&#45;&#45;host&#45;inventory &lt;path&gt;).</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+23
View File
@@ -0,0 +1,23 @@
.md-content img, article img {
max-width: 100%;
width: auto;
height: auto;
}
.diagram-wrap {
width: 100%;
overflow-x: auto;
margin: 1rem 0 2rem;
}
.diagram-wrap img {
max-width: none;
width: 1400px;
height: auto;
}
@media (max-width: 900px) {
.diagram-wrap img {
width: 1200px;
}
}
+21
View File
@@ -0,0 +1,21 @@
# Traefik Routes
| Service | Router | Rule | Entrypoints | TLS | Middlewares | Target Port |
|---|---|---|---|---|---|---|
| authelia | authelia | Host(`auth.<domain>`) | websecure | true | | |
| error-pages | error-pages-router | HostRegexp(`{host:.+}`) | web | | error-pages-middleware | |
| gitea | gitea | Host(`gitea.<domain>`) | websecure | true | | 3000 |
| gotify | gotify | Host(`gotify.<domain>`) | websecure | | | 80 |
| grafana | grafana | Host(`grafana.<domain>`) | websecure | | | 3000 |
| grampsweb | gramps | Host(`familytree.<domain>`) | websecure | | | 5000 |
| influxdb | influxdb | Host(`influxdb.<domain>`) | websecure | | authelia | 8086 |
| monitor-kuma | monitor | Host(`monitor-kuma.<domain>`) | websecure | true | | 3001 |
| mtls-bridge | mtls-bridge | Host(`mtls-bridge.<domain>`) | websecure | | mtls-bridge-auth,mtls-bridge-cors | 8080 |
| mtls-bridge | mtls-bridge-preflight | Host(`mtls-bridge.<domain>`) && Method(`OPTIONS`) | websecure | | mtls-bridge-cors | |
| nextcloud-webapp | nextcloud | Host(`nextcloud.<domain>`) | websecure | | nextcloud-dav, nextcloud-webfinger | |
| node-red | node-red | Host(`node-red.<domain>`) | websecure | | authelia | 1880 |
| passbolt-webapp | passbolt | Host(`passbolt.<domain>`) | websecure | | | |
| portainer | portainer | Host(`portainer.<domain>`) | websecure | true | | 9000 |
| prometheus | prometheus | Host(`prometheus.<domain>`) | websecure | | authelia | 9090 |
| searxng-webapp | searxng | Host(`searxng.<domain>`) | websecure | | | 8080 |
| traefik | traefik | Host(`traefik.<domain>`) | websecure | | authelia | |
+6 -1
View File
@@ -8,6 +8,7 @@ This page explains where to find authoritative files quickly.
- `apps/` — user/business applications (Nextcloud, Passbolt, Gitea, Gramps, SearXNG).
- `monitoring/` — observability and operational tooling (Prometheus, Grafana, InfluxDB, Node-RED, etc.).
- `infrastructure/terraform/` — brownfield Terraform inventory/reconciliation layers.
- `infrastructure/ansible/` — phase-1 Ansible inventory/configuration scaffold and validation playbooks.
- `docs/` — repository-level architecture and workflow documentation.
- `archive/` — historical compose/config artifacts not part of active runtime composition.
- `secrets/` — local secret material and templates; never commit real values.
@@ -17,6 +18,8 @@ This page explains where to find authoritative files quickly.
- `services-up.sh` — runtime composition entrypoint for multi-compose environment.
- `default-network.yml` — shared docker network definitions used across compose files.
- `default-environment.env` — non-secret default env values for compose rendering.
- `scripts/codex-setup.sh` — Codex/bootstrap helper to install validation tooling and prepare dummy secret material.
- `scripts/codex-maintenance.sh` — Codex maintenance helper to refresh tooling, reconcile dummy secret material, and run safe Ansible validation checks.
- `docs/deployment-prerequisites.md` — prerequisite setup before runtime operations.
- `docs/security-secrets.md` — secrets documentation and inventory model.
@@ -30,8 +33,10 @@ This page explains where to find authoritative files quickly.
## Fast path for future Codex runs
1. Read [README.md](../README.md).
1. Read [README.md](https://github.com/beatz174-bit/docker/blob/main/README.md).
2. Read [docs/source-of-truth.md](source-of-truth.md).
3. Read [docs/docker-environment.md](docker-environment.md).
4. Read [docs/terraform-workflows.md](terraform-workflows.md).
5. Only then edit Compose/Terraform files.
6. For Ansible bootstrap changes, validate inventory and playbook syntax checks only.
+9 -5
View File
@@ -4,12 +4,13 @@
This page explains how secret material is organized in this repository and where to find both human-readable and machine-readable references.
For machine-readable inventory metadata, use [`../secrets/inventory.json`](../secrets/inventory.json).
For machine-readable inventory metadata, use [`secrets/inventory.json`](https://github.com/beatz174-bit/docker/blob/main/secrets/inventory.json).
## Scope and authority
- Canonical example template: [`../secrets/.env.secrets.example`](../secrets/.env.secrets.example)
- Canonical example template: [`secrets/.env.secrets.example`](https://github.com/beatz174-bit/docker/blob/main/secrets/.env.secrets.example)
- Runtime-loaded secret env file (local, non-committed): `../secrets/stack-secrets.env`
- Dynu DNS inventory env file (local, non-committed): `../secrets/dynu.env`
- Docker secret files (local, non-committed): `../secrets/*.txt`
Treat the example template as the canonical shape for expected environment variables.
@@ -20,14 +21,16 @@ Treat the example template as the canonical shape for expected environment varia
- Document expected variable names and usage expectations.
2. **Local runtime env file (`stack-secrets.env`)**
- Holds local runtime secret values loaded during compose rendering.
3. **Local Docker secret files (`*.txt`)**
3. **Local Dynu env file (`dynu.env`)**
- Holds `DYNU_*` values used by read-only Dynu DNS inventory scripts.
4. **Local Docker secret files (`*.txt`)**
- Hold password/token material consumed via `*_FILE` style configuration.
4. **Externally managed secret inputs**
5. **Externally managed secret inputs**
- Some values are managed outside shared templates and provided through file mounts or environment substitution.
## Machine-readable inventory
- Primary automation source: [`../secrets/inventory.json`](../secrets/inventory.json)
- Primary automation source: [`secrets/inventory.json`](https://github.com/beatz174-bit/docker/blob/main/secrets/inventory.json)
- Human guidance source: this page
Automation should parse `secrets/inventory.json` directly rather than scraping Markdown tables.
@@ -41,6 +44,7 @@ Before running compose operations, follow [`./deployment-prerequisites.md`](./de
Never commit:
- `secrets/stack-secrets.env`
- `secrets/dynu.env`
- real `secrets/*.txt` secret files
- real Terraform `.tfvars` files containing credentials
- Terraform state files with sensitive runtime metadata
+14
View File
@@ -0,0 +1,14 @@
# Public Showcase
This environment showcases practical infrastructure operations with a documentation-first approach.
Highlights:
- Container orchestration with Docker Compose.
- Reverse proxy routing and TLS-focused service exposure patterns.
- Monitoring and alerting configuration via version-controlled rules.
- Documentation automation through CI.
- Generated architecture diagrams to aid operational understanding.
- Sanitized public documentation for safe sharing.
This page intentionally avoids private hostnames, internal-only URLs, credentials, and secret values.
+16
View File
@@ -10,6 +10,7 @@ This repository has multiple layers. Knowing the authority for each layer preven
| Docker shared baseline inputs | `default-network.yml`, `default-environment.env`, `secrets/stack-secrets.env` | Shared network/env material applied during compose rendering. |
| Infrastructure inventory and reconciliation | Terraform under `infrastructure/terraform/` | Codified inventory of existing infrastructure and relationships, especially Proxmox VMs and selected Docker mirrors. |
| Secret policy and inventory | `docs/security-secrets.md` + `secrets/inventory.json` + local secret files in `secrets/` | What secrets exist, where they are expected, and what automation should parse. |
| Host/device configuration bootstrap (emerging) | Ansible under `infrastructure/ansible/` | Gradual inventory/configuration layer for hosts/devices; validation-first at current stage. |
## Practical meaning
@@ -29,6 +30,21 @@ Use Terraform when documenting/reconciling existing:
Do **not** treat Terraform as a full replacement for Compose operations in this repo.
- Dynu public DNS records remain authoritative at Dynu.
- Terraform Dynu configuration mirrors/reconciles Dynu DNS state for documentation and controlled drift management.
- Imported Dynu Terraform state reflects actual provider-side DNS state at import time.
### Ansible bootstrap decisions
Use Ansible under `infrastructure/ansible/` to build inventory and configuration structure incrementally.
At the current stage:
- Do **not** treat Ansible as replacement authority for Docker runtime operations.
- Do **not** treat Ansible as replacement authority for Terraform inventory/reconciliation.
- NixOS remains outside Ansible authority unless explicitly adopted in a later phase.
## Declared config vs observed/runtime state
- **Declared config**: files in this repository (Compose, Terraform, docs).
+16 -1
View File
@@ -43,6 +43,21 @@ Use for existing Proxmox VMs and metadata reconciliation.
5. Keep lifecycle ignore rules narrow and explicit.
6. Iterate per VM until plan stabilizes.
## Dynu DNS workflow
Directory: `infrastructure/terraform/dynu/`
Use for existing Dynu domains and DNS records.
1. Add or confirm the documentation catalog entry for one hostname.
2. Confirm the provider resource type and import ID format.
3. Import one existing domain or DNS record at a time.
4. Inspect state with `terraform state show`.
5. Reconcile only stable, meaningful attributes into hand-maintained `.tf`.
6. Keep record IDs, dynamic DNS targets, and provider-computed values out unless intentionally required.
7. Re-run plan until intended scope is clean.
## Physical host metadata workflow
Physical host metadata currently lives in Proxmox Terraform locals/outputs and is used as documentation inventory context.
@@ -75,4 +90,4 @@ Treat generated files as:
- [docs/source-of-truth.md](source-of-truth.md)
- [docs/infrastructure-inventory.md](infrastructure-inventory.md)
- [infrastructure/terraform/README.md](../infrastructure/terraform/README.md)
- [infrastructure/terraform/README.md](https://github.com/beatz174-bit/docker/blob/main/infrastructure/terraform/README.md)
+48
View File
@@ -0,0 +1,48 @@
# Ansible Foundation (Phase 1)
This directory provides a minimal Ansible bootstrap for this repository.
## Purpose
- Establish a maintainable inventory/configuration foundation for hosts and devices.
- Support gradual host onboarding and validation workflows.
- Keep boundaries clear with existing Compose and Terraform authorities.
This is intentionally a **foundation stage**, not full production automation.
## Boundaries
- Docker runtime authority remains in Compose files and `services-up.sh`.
- Terraform remains the primary structured infrastructure inventory/reconciliation layer.
- Ansible here is a complementary configuration/inventory layer.
- NixOS and network gear management are not authoritative through Ansible yet.
## Structure
- `ansible.cfg` - local defaults for inventory, collections, and output behavior.
- `inventory/hosts.yml` - YAML inventory scaffold with starter groups.
- `inventory/group_vars/` - shared/group variables.
- `inventory/host_vars/` - per-host variables.
- `playbooks/ping.yml` - minimal syntax/connection test playbook.
- `playbooks/dns-inventory.yml` - local-only Dynu DNS read-only inventory wrapper.
- `collections/requirements.yml` - lightweight baseline collections.
- `roles/` - reserved for future incremental role adoption.
## Basic commands
Run from repository root:
```bash
ansible --version
ansible-lint --version
ansible-galaxy collection install -r infrastructure/ansible/collections/requirements.yml -p infrastructure/ansible/collections
ansible-inventory -i infrastructure/ansible/inventory/hosts.yml --list
ansible-playbook -i infrastructure/ansible/inventory/hosts.yml infrastructure/ansible/playbooks/ping.yml --syntax-check
ansible-playbook -i infrastructure/ansible/inventory/hosts.yml infrastructure/ansible/playbooks/dns-inventory.yml --syntax-check
```
## Secrets and safety
- Do not commit real credentials or private keys.
- Put sensitive per-host variables in local, untracked files or a future vault approach.
- Keep host and device entries factual; avoid speculative production entries.
+9
View File
@@ -0,0 +1,9 @@
[defaults]
inventory = ./inventory/hosts.yml
collections_path = ./collections
retry_files_enabled = False
stdout_callback = yaml
host_key_checking = True
[inventory]
enable_plugins = yaml
@@ -0,0 +1,4 @@
---
collections:
- name: ansible.posix
- name: community.general
@@ -0,0 +1,14 @@
---
# Bootstrap defaults for the Ansible foundation in this repository.
# Keep secrets and environment-specific auth details out of version control.
# Common interpreter hint for modern Linux hosts. Override per-host if needed.
ansible_python_interpreter: /usr/bin/python3
# Placeholders for future connection/auth settings:
# ansible_user: ""
# ansible_port: 22
# ansible_ssh_private_key_file: ""
# Add group-specific settings under inventory/group_vars/<group>.yml
# and host-specific settings under inventory/host_vars/<host>.yml.
@@ -0,0 +1,17 @@
---
all:
children:
linux:
hosts: {}
network:
hosts: {}
virtualization:
hosts: {}
nixos:
hosts: {}
examples:
hosts:
example-managed-host:
ansible_host: example-host.local
ansible_connection: ssh
# Example only: replace/remove before real operations.
@@ -0,0 +1,26 @@
---
# This integration is intentionally read-only.
# No Dynu mutations are permitted in this repo at this stage.
- name: Build Dynu DNS read-only inventory artifacts
hosts: localhost
connection: local
gather_facts: false
vars:
repo_root: "{{ playbook_dir }}/../../.."
tasks:
- name: Assert read-only guard variable is set
ansible.builtin.assert:
that:
- lookup('ansible.builtin.env', 'DYNU_READ_ONLY') == 'true'
fail_msg: "Refusing to run: DYNU_READ_ONLY must be exactly 'true'."
- name: Fetch Dynu DNS (GET-only script)
ansible.builtin.command: python3 scripts/dynu/fetch_dynu_dns.py
args:
chdir: "{{ repo_root }}"
- name: Correlate Dynu with Traefik and generate docs
ansible.builtin.command: python3 scripts/dynu/correlate_dynu_with_traefik.py
args:
chdir: "{{ repo_root }}"
@@ -0,0 +1,7 @@
---
- name: Basic inventory and connectivity check
hosts: all
gather_facts: false
tasks:
- name: Ping managed hosts
ansible.builtin.ping:
+4
View File
@@ -10,17 +10,21 @@ It does **not** replace Docker Compose as runtime deployment authority.
- Physical host metadata represented in Terraform locals/outputs.
- Select Docker container mirror resources for documentation-oriented tracking.
- Outputs that can support documentation and later downstream tooling.
- Dynu DNS domain/record import and documentation inventory.
## What Terraform is not used for (today)
- Replacing `services-up.sh` / Compose for day-to-day app runtime orchestration.
- Broad, immediate greenfield provisioning of the whole stack.
- Casual `apply` operations across all infrastructure.
- Replacing Dynu as DNS authority.
- Blindly recreating production DNS records without import/reconciliation.
## Directory map
- `proxmox/` — imported/reconciled VM resources and host metadata outputs.
- `docker/` — selective Docker container import/mirror resources.
- `dynu/` — Dynu DNS brownfield import/reconciliation and DNS documentation outputs.
- `bootstrap/` — backend/provider bootstrap scaffolding.
- `modules/` — placeholder module directories for future stable abstractions.
- `scripts/reconcile_from_plan.sh` — helper to convert generated plan config into reviewable draft files.
+23
View File
@@ -0,0 +1,23 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/beatz174-bit/dynu" {
version = "0.3.0"
hashes = [
"h1:yftAEp/lcPmbVRV8YenFaMJElUkt3j79TSt3OcQnwk4=",
"zh:1f7344737dff5b12155e8308b1cb55b0cc6f83a4a5776eb1cc2273bc84bb8fa2",
"zh:212662c4f5b979401f282f7f9856480bad86d9e488110bebc589a4eeb892ff02",
"zh:2ed5294fc7db7639c41f99a9d7bcfff6585f1f372eb23cd8229adfe219cba63a",
"zh:30fc9df00120f309ae969ae107e4dc4a0b04517f2c07a78934a2c479995f77b8",
"zh:3126369f6dc86e8083ec12a2643ea87a13543ed12631b40375b7b9563da41474",
"zh:3c5775a6763608253e2698b85dcee42eafd6ca8e08a8851866123de403a331b0",
"zh:50bedffbee48505604d05181172018143e54c68249761a749fb7c115eec4ce04",
"zh:528549a2763dd2fbf3ffe047fa19c0524eb08addd777c9c0350800394ff16235",
"zh:99049e25d7d3fb26e2a94d6e609c8989efcee1af1568a77110a70bce2c01f1ef",
"zh:9a6490b67aca08b135e5ba7092fc315e67177be1f280f6c0d06b9c64a0892d3a",
"zh:bfaae08fb5a0b10184a7b6e8382038d0a0b9936ea13efa1462c35cee902b0c14",
"zh:c0dbe59b9bfcbd42f3da1732615669a579275acc654c75127612d86f319b38b3",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
"zh:f815fa2f8681477f159eb9a32b78f8988065e5040b7feea42604bac469c2c4eb",
]
}
+187
View File
@@ -0,0 +1,187 @@
# Dynu Terraform Layer (Brownfield DNS Reconciliation)
This Terraform root is for **Dynu DNS brownfield reconciliation**. The intended pattern is:
1. Import the existing root domain object.
2. Read inventory through `data.dynu_dns_records.root`.
3. Generate reviewable `dynu_dns_record` resources and import commands.
4. Import every existing DNS record into matching Terraform resources.
5. Use `terraform plan` as the reconciliation check before any apply.
## Provider behavior to keep in mind
- Source: `beatz174-bit/dynu`
- `dynu_domain` import requires a **numeric Dynu domain ID**.
- Importing `dynu_domain` imports only the root domain object.
- It **does not** import DNS records/subdomains.
- `dynu_dns_record` imports require `<domain_id>/<record_id>`.
## Variables
- `dynu_root_domain` (default: `lan.ddnsgeek.com`)
- `dynu_api_key` (sensitive)
- `dynu_username` / `dynu_password` (optional)
## Safe validation commands
```bash
cd infrastructure/terraform/dynu
terraform fmt -check -recursive
terraform init -backend=false -input=false
terraform validate
python3 -m py_compile scripts/generate-brownfield-records.py
```
## Brownfield workflow
```bash
cd infrastructure/terraform/dynu
terraform init
terraform import dynu_domain.lan_ddnsgeek_com '<numeric-dynu-domain-id>'
terraform apply -refresh-only
terraform output -json dynu_dns_records > /tmp/dynu-records.json
python3 scripts/generate-brownfield-records.py --dry-run
python3 scripts/generate-brownfield-records.py --overwrite
# Review generated/dynu_dns_records.generated.tf
# Review generated/import-dynu-dns-records.sh
bash generated/import-dynu-dns-records.sh
terraform plan
```
## What each component means
- `data.dynu_dns_records.root`: read-only live inventory from Dynu.
- `generated/dynu_dns_records.generated.tf`: generated management-intent resources; includes `prevent_destroy = true` on each record.
- `generated/import-dynu-dns-records.sh`: imports each discovered record to its generated `dynu_dns_record` address using `<domain_id>/<record_id>`.
- `terraform plan` after imports: reconciliation checkpoint. Any create/update/delete must be reviewed manually before apply.
## Generated artifacts
The helper script writes these files under `generated/`:
- `generated/dynu_dns_records_inventory.json`
- `generated/dynu_dns_records.generated.tf`
- `generated/import-dynu-dns-records.sh`
These are generated outputs meant for operator review before use in production.
### Generator output selection (interactive + automation)
The brownfield generator defaults to Terraform output `dynu_dns_records`:
```bash
python3 scripts/generate-brownfield-records.py --dry-run
```
If the default output is missing/unusable and stdin is interactive, the script shows a picker of available Terraform outputs and indicates which ones are usable for DNS imports.
```bash
# Interactive mode: choose from available Terraform outputs
python3 scripts/generate-brownfield-records.py --dry-run
# Non-interactive mode: specify output explicitly
python3 scripts/generate-brownfield-records.py \
--records-output dynu_dns_inventory \
--dry-run
# Disable menu and fail fast
python3 scripts/generate-brownfield-records.py \
--no-interactive \
--dry-run
# Use saved terraform output JSON and choose interactively
terraform output -json > generated/terraform-output.json
python3 scripts/generate-brownfield-records.py \
--from-file generated/terraform-output.json \
--dry-run
```
Notes:
- The menu shows Terraform outputs currently stored in state.
- If newly added outputs do not appear, run:
```bash
terraform apply -refresh-only
```
- The selected output must contain real Dynu provider record fields:
- `id`
- `domain_id`
- `hostname`
- `record_type`
## Troubleshooting
### Plan shows a large wall of `+` values under outputs
Cause:
Terraform is planning to save **new output values** to state (for example, live records from `data.dynu_dns_records.root`). This is not creating DNS records by itself.
How to verify:
- Output-only changes appear under `Changes to Outputs`.
- Real DNS changes appear as `dynu_dns_record` resource create/update/delete actions.
Use:
```bash
terraform apply -refresh-only
```
to persist refreshed data source and output values only.
### Error: `There is no function named "regexreplace"`
Cause:
`regexreplace` is not a Terraform function. Resource-name slugification should not be implemented in Terraform HCL for this workflow.
Fix:
- Keep `inventory.tf` focused on reading live records via `data.dynu_dns_records.root`.
- Keep Terraform outputs simple (for example, `<domain_id>/<record_id>` mappings).
- Let `scripts/generate-brownfield-records.py` generate Terraform-safe resource names with Python `tf_name(record)`.
### Error: `'"'"'dynu_dns_records'"'"'`
Cause:
The helper script reads `terraform output -json` and expects an output named `dynu_dns_records`.
Fix:
```bash
cd infrastructure/terraform/dynu
terraform init
terraform apply -refresh-only
terraform output -json | jq 'keys'
```
Confirm `dynu_dns_records` appears in the key list.
If it does not, check that the Terraform config contains:
```hcl
data "dynu_dns_records" "root" {
hostname = var.dynu_root_domain
}
output "dynu_dns_records" {
value = data.dynu_dns_records.root.records
}
```
Then rerun:
```bash
python3 scripts/generate-brownfield-records.py --dry-run
```
@@ -0,0 +1,7 @@
data "dynu_domain" "lan" {
hostname = "lan.ddnsgeek.com"
}
output "dynu_domain_id" {
value = data.dynu_domain.lan.domain.id
}
+11
View File
@@ -0,0 +1,11 @@
locals {
dynu_domain = var.dynu_root_domain
}
# Import-first resource skeleton for the production Dynu zone.
# `name` is required by provider schema and can be reconciled after import.
resource "dynu_domain" "lan_ddnsgeek_com" {
name = local.dynu_domain
}
@@ -0,0 +1,518 @@
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
hostname = "kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 90
enabled = true
content = "120.155.99.146"
node_name = "kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
hostname = "lan.ddnsgeek.com"
record_type = "SOA"
ttl = 120
enabled = true
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
@@ -0,0 +1,518 @@
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
hostname = "kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 60
enabled = true
content = "120.155.99.146"
node_name = "kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
hostname = "lan.ddnsgeek.com"
record_type = "SOA"
ttl = 120
enabled = true
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
@@ -0,0 +1,534 @@
[
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "auth.lan.ddnsgeek.com",
"id": 18483099,
"node_name": "auth",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-02-17T12:59:58.803"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "auth.lan.ddnsgeek.com",
"id": 19646048,
"node_name": "auth",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.693"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "edge.lan.ddnsgeek.com",
"id": 10453241,
"node_name": "edge",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2024-03-18T01:27:26"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "edge.lan.ddnsgeek.com",
"id": 19646062,
"node_name": "edge",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:45.237"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "familytree.lan.ddnsgeek.com",
"id": 17017685,
"node_name": "familytree",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-11-17T04:43:07.953"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "familytree.lan.ddnsgeek.com",
"id": 19646056,
"node_name": "familytree",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:44.25"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "gitea.lan.ddnsgeek.com",
"id": 14682463,
"node_name": "gitea",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-07-16T03:15:50.38"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "gitea.lan.ddnsgeek.com",
"id": 19646063,
"node_name": "gitea",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:45.343"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "gotify.lan.ddnsgeek.com",
"id": 17439061,
"node_name": "gotify",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-12-16T09:35:47.307"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "gotify.lan.ddnsgeek.com",
"id": 19646047,
"node_name": "gotify",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.693"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "grafana.lan.ddnsgeek.com",
"id": 18113762,
"node_name": "grafana",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-01-28T04:38:25.92"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "grafana.lan.ddnsgeek.com",
"id": 19646050,
"node_name": "grafana",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.72"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "influxdb.lan.ddnsgeek.com",
"id": 18562198,
"node_name": "influxdb",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-02-22T16:46:26.85"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "influxdb.lan.ddnsgeek.com",
"id": 19646059,
"node_name": "influxdb",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:44.85"
},
{
"content": "120.155.99.146",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "kuma.lan.ddnsgeek.com",
"id": 17454978,
"node_name": "kuma",
"record_type": "A",
"state": true,
"ttl": 60,
"updated_on": "2026-04-21T04:50:04.81"
},
{
"content": "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "lan.ddnsgeek.com",
"id": 8299670,
"node_name": null,
"record_type": "SOA",
"state": true,
"ttl": 120,
"updated_on": "2022-03-15T10:08:15"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "monitor-kuma.lan.ddnsgeek.com",
"id": 17462342,
"node_name": "monitor-kuma",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-12-17T14:47:25"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "monitor-kuma.lan.ddnsgeek.com",
"id": 19646051,
"node_name": "monitor-kuma",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.727"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "mtls-bridge.lan.ddnsgeek.com",
"id": 19232643,
"node_name": "mtls-bridge",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-04-13T04:20:00.11"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "mtls-bridge.lan.ddnsgeek.com",
"id": 19646058,
"node_name": "mtls-bridge",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:44.737"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "nextcloud.lan.ddnsgeek.com",
"id": 10453260,
"node_name": "nextcloud",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2024-03-18T01:40:00"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "nextcloud.lan.ddnsgeek.com",
"id": 19646057,
"node_name": "nextcloud",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:44.53"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "node-red.lan.ddnsgeek.com",
"id": 19041230,
"node_name": "node-red",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-03-30T04:51:39.68"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "node-red.lan.ddnsgeek.com",
"id": 19646053,
"node_name": "node-red",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.737"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "passbolt.lan.ddnsgeek.com",
"id": 10453262,
"node_name": "passbolt",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2024-03-18T01:40:18"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "passbolt.lan.ddnsgeek.com",
"id": 19646049,
"node_name": "passbolt",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.693"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "portainer.lan.ddnsgeek.com",
"id": 17458810,
"node_name": "portainer",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-12-17T10:23:40.077"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "portainer.lan.ddnsgeek.com",
"id": 19646046,
"node_name": "portainer",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.693"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "prometheus.lan.ddnsgeek.com",
"id": 18483311,
"node_name": "prometheus",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-02-17T13:17:00.55"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "prometheus.lan.ddnsgeek.com",
"id": 19646061,
"node_name": "prometheus",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:45.033"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "searxng.lan.ddnsgeek.com",
"id": 10453263,
"node_name": "searxng",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2024-03-18T01:40:34"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "searxng.lan.ddnsgeek.com",
"id": 19646055,
"node_name": "searxng",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:38.797"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "shifts.lan.ddnsgeek.com",
"id": 15901565,
"node_name": "shifts",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-09-30T04:25:20.65"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "shifts.lan.ddnsgeek.com",
"id": 19646052,
"node_name": "shifts",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.727"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "stockfill.lan.ddnsgeek.com",
"id": 17081867,
"node_name": "stockfill",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2025-11-21T06:49:33.47"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "stockfill.lan.ddnsgeek.com",
"id": 19646060,
"node_name": "stockfill",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:44.987"
},
{
"content": null,
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": null,
"host": null,
"hostname": "traefik.lan.ddnsgeek.com",
"id": 10453240,
"node_name": "traefik",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2024-03-18T01:27:13"
},
{
"content": "167.179.167.166",
"domain_id": 9695470,
"domain_name": "lan.ddnsgeek.com",
"group": "home",
"host": null,
"hostname": "traefik.lan.ddnsgeek.com",
"id": 19646054,
"node_name": "traefik",
"record_type": "A",
"state": true,
"ttl": 120,
"updated_on": "2026-05-12T21:11:37.737"
}
]
@@ -0,0 +1,243 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Imports existing Dynu DNS records into Terraform state.
# Does not apply changes.
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${TF_ROOT}"
# Re-running imports will fail for resources already in state.
# This script skips imports when state already contains the resource address.
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099'
else
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' '9695470/18483099'
fi
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048'
else
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' '9695470/19646048'
fi
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241'
else
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' '9695470/10453241'
fi
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062'
else
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' '9695470/19646062'
fi
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685'
else
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' '9695470/17017685'
fi
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056'
else
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' '9695470/19646056'
fi
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463'
else
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' '9695470/14682463'
fi
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063'
else
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' '9695470/19646063'
fi
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061'
else
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' '9695470/17439061'
fi
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047'
else
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' '9695470/19646047'
fi
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762'
else
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' '9695470/18113762'
fi
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050'
else
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' '9695470/19646050'
fi
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198'
else
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' '9695470/18562198'
fi
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059'
else
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' '9695470/19646059'
fi
if terraform state show 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978'
else
terraform import 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' '9695470/17454978'
fi
if terraform state show 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.lan_ddnsgeek_com_soa_8299670'
else
terraform import 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' '9695470/8299670'
fi
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342'
else
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' '9695470/17462342'
fi
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051'
else
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' '9695470/19646051'
fi
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643'
else
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' '9695470/19232643'
fi
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058'
else
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' '9695470/19646058'
fi
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260'
else
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' '9695470/10453260'
fi
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057'
else
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' '9695470/19646057'
fi
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230'
else
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' '9695470/19041230'
fi
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053'
else
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' '9695470/19646053'
fi
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262'
else
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' '9695470/10453262'
fi
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049'
else
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' '9695470/19646049'
fi
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810'
else
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' '9695470/17458810'
fi
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046'
else
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' '9695470/19646046'
fi
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311'
else
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' '9695470/18483311'
fi
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061'
else
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' '9695470/19646061'
fi
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263'
else
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' '9695470/10453263'
fi
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055'
else
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' '9695470/19646055'
fi
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565'
else
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' '9695470/15901565'
fi
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052'
else
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' '9695470/19646052'
fi
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867'
else
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' '9695470/17081867'
fi
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060'
else
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' '9695470/19646060'
fi
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240'
else
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' '9695470/10453240'
fi
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054'
else
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' '9695470/19646054'
fi
@@ -0,0 +1,16 @@
# Copy this file to imports.tf and adjust IDs after confirming the
# published provider docs for import ID formats.
# For dynu_domain, import ID is commonly the root domain name.
import {
to = dynu_domain.lan_ddnsgeek_com
id = var.dynu_root_domain
}
# DNS record imports are intentionally examples only because the provider
# requires explicit record_type/hostname in config before import.
#
# import {
# to = dynu_dns_record.grafana_lan_ddnsgeek_com
# id = var.dynu_record_import_id
# }
@@ -0,0 +1,3 @@
data "dynu_dns_records" "root" {
hostname = var.dynu_root_domain
}
+47
View File
@@ -0,0 +1,47 @@
output "dynu_domain" {
description = "Primary Dynu domain represented by this Terraform root."
value = local.dynu_domain
}
output "dynu_dns_records_catalog" {
description = "Documentation catalog of expected Dynu DNS records discovered from repo service exposure."
value = local.dynu_dns_records_catalog
}
output "dynu_dns_inventory" {
description = "Documentation-friendly Dynu DNS inventory for export and merge into broader infrastructure docs."
value = {
provider = "dynu"
domain = local.dynu_domain
record_count = length(local.dynu_dns_records_catalog)
records = local.dynu_dns_records_catalog
}
}
output "dynu_root_domain_id" {
description = "Dynu numeric domain ID resolved from dynu_root_domain."
value = data.dynu_dns_records.root.domain_id
}
output "dynu_root_domain_name" {
description = "Dynu root domain name resolved from dynu_root_domain."
value = data.dynu_dns_records.root.domain_name
}
output "dynu_dns_records" {
description = "Full read-only DNS record inventory returned by Dynu."
value = data.dynu_dns_records.root.records
}
output "dynu_dns_hostnames" {
description = "Sorted hostname list discovered for dynu_root_domain."
value = sort(distinct([for record in data.dynu_dns_records.root.records : record.hostname]))
}
output "dynu_dns_record_import_ids" {
description = "Map of Dynu DNS record identity to provider import IDs in domain_id/record_id format."
value = {
for record in data.dynu_dns_records.root.records :
format("%s/%s/%s", record.hostname, record.record_type, record.id) => format("%s/%s", record.domain_id, record.id)
}
}
@@ -0,0 +1,4 @@
provider "dynu" {
# Keep auth local-only; do not commit credentials.
api_key = var.dynu_api_key
}
+161
View File
@@ -0,0 +1,161 @@
locals {
dynu_dns_records_catalog_base = {
auth = {
hostname = "auth"
service = "authelia"
source = "core/authelia/docker-compose.yml"
purpose = "Authentication portal"
record_type = null
ttl = null
target = null
proxied = null
}
gitea = {
hostname = "gitea"
service = "gitea"
source = "apps/gitea/docker-compose.yml"
purpose = "Gitea service endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
gotify = {
hostname = "gotify"
service = "gotify"
source = "monitoring/gotify/docker-compose.yml"
purpose = "Gotify notifications"
record_type = null
ttl = null
target = null
proxied = null
}
grafana = {
hostname = "grafana"
service = "grafana"
source = "monitoring/grafana/docker-compose.yml"
purpose = "Grafana monitoring UI"
record_type = null
ttl = null
target = null
proxied = null
}
familytree = {
hostname = "familytree"
service = "gramps"
source = "apps/gramps/docker-compose.yml"
purpose = "Family tree application"
record_type = null
ttl = null
target = null
proxied = null
}
influxdb = {
hostname = "influxdb"
service = "influxdb"
source = "monitoring/influxdb/docker-compose.yml"
purpose = "InfluxDB metrics endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
monitor_kuma = {
hostname = "monitor-kuma"
service = "uptime-kuma"
source = "monitoring/uptime-kuma/docker-compose.yml"
purpose = "Uptime Kuma monitoring UI"
record_type = null
ttl = null
target = null
proxied = null
}
mtls_bridge = {
hostname = "mtls-bridge"
service = "mtls-bridge"
source = "monitoring/mtls-bridge/docker-compose.yml"
purpose = "mTLS bridge API"
record_type = null
ttl = null
target = null
proxied = null
}
nextcloud = {
hostname = "nextcloud"
service = "nextcloud-webapp"
source = "apps/nextcloud/docker-compose.yml"
purpose = "Nextcloud service endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
node_red = {
hostname = "node-red"
service = "node-red"
source = "monitoring/node-red/docker-compose.yml"
purpose = "Node-RED automation UI/API"
record_type = null
ttl = null
target = null
proxied = null
}
passbolt = {
hostname = "passbolt"
service = "passbolt-webapp"
source = "apps/passbolt/docker-compose.yml"
purpose = "Passbolt password management"
record_type = null
ttl = null
target = null
proxied = null
}
portainer = {
hostname = "portainer"
service = "portainer"
source = "monitoring/portainer/docker-compose.yml"
purpose = "Portainer admin endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
prometheus = {
hostname = "prometheus"
service = "prometheus"
source = "monitoring/prometheus/docker-compose.yml"
purpose = "Prometheus metrics endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
searxng = {
hostname = "searxng"
service = "searxng"
source = "apps/searxng/docker-compose.yml"
purpose = "SearXNG search endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
traefik = {
hostname = "traefik"
service = "traefik"
source = "core/traefik/docker-compose.yml"
purpose = "Traefik dashboard/API endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
}
dynu_dns_records_catalog = {
for key, record in local.dynu_dns_records_catalog_base :
key => merge(record, {
fqdn = format("%s.%s", record.hostname, local.dynu_domain)
})
}
}
@@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""Generate Terraform dynu_dns_record resources/import commands from Dynu inventory outputs."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve()
TF_ROOT = SCRIPT_PATH.parents[1]
GENERATED_DIR = TF_ROOT / "generated"
TF_FILE = GENERATED_DIR / "dynu_dns_records.generated.tf"
IMPORT_SCRIPT = GENERATED_DIR / "import-dynu-dns-records.sh"
INVENTORY_FILE = GENERATED_DIR / "dynu_dns_records_inventory.json"
DEFAULT_RECORDS_OUTPUT = "dynu_dns_records"
REQUIRED_RECORD_FIELDS = ("id", "domain_id", "hostname", "record_type")
HEADER_TF = """# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
"""
HEADER_SH = """#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Imports existing Dynu DNS records into Terraform state.
# Does not apply changes.
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${TF_ROOT}"
# Re-running imports will fail for resources already in state.
# This script skips imports when state already contains the resource address.
"""
OPTIONAL_FIELDS = ["group", "host", "priority", "weight", "port", "flags", "tag", "value", "node_name"]
def run_terraform_output() -> dict:
if not (TF_ROOT / ".terraform").exists():
raise RuntimeError("Terraform is not initialized in infrastructure/terraform/dynu. Run: terraform init")
cmd = ["terraform", "output", "-json"]
proc = subprocess.run(cmd, cwd=TF_ROOT, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"Failed to run {' '.join(cmd)}:\n{proc.stderr.strip()}")
return json.loads(proc.stdout)
def type_shape_name(value: object) -> str:
if isinstance(value, list):
return "list"
if isinstance(value, dict):
return "object"
return type(value).__name__
def extract_records(payload: object, output_name: str) -> list[dict]:
source = payload
if isinstance(payload, list):
source = payload
elif isinstance(payload, dict):
if isinstance(payload.get("value"), list):
source = payload["value"]
elif isinstance(payload.get("records"), list):
source = payload["records"]
elif isinstance(payload.get("value"), dict) and isinstance(payload["value"].get("records"), list):
source = payload["value"]["records"]
elif output_name in payload and isinstance(payload[output_name], dict):
output_wrapper = payload[output_name]
if isinstance(output_wrapper.get("value"), list):
source = output_wrapper["value"]
elif isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
source = output_wrapper["value"]["records"]
elif isinstance(output_wrapper.get("records"), list):
source = output_wrapper["records"]
else:
raise RuntimeError(f"Output '{output_name}' does not contain a records list.")
else:
raise RuntimeError(f"Output '{output_name}' not found and no records list discovered.")
else:
raise RuntimeError(f"Unsupported JSON payload type: {type(payload).__name__}")
if not isinstance(source, list):
raise RuntimeError(f"Output '{output_name}' did not resolve to a list of records.")
return source
def validate_records(records: list[dict], output_name: str) -> None:
for i, record in enumerate(records):
if not isinstance(record, dict):
raise RuntimeError(f"Selected output '{output_name}' has non-object record at index {i}: {type(record).__name__}.")
missing = [field for field in REQUIRED_RECORD_FIELDS if field not in record]
if missing:
missing_text = ", ".join(missing)
raise RuntimeError(
f"Selected output '{output_name}' contains records, but they are not importable Dynu provider records. "
f"Record #{i} is missing required fields: {missing_text}. "
"Choose an output sourced from data.dynu_dns_records.root, such as dynu_dns_records or dynu_dns_inventory."
)
def describe_output(output_name: str, output_wrapper: object, full_outputs: dict) -> dict:
details = {
"name": output_name,
"usable": False,
"shape": type_shape_name(output_wrapper),
"record_count": "none",
"error": "no records list found",
}
if isinstance(output_wrapper, dict) and "value" in output_wrapper:
details["shape"] = type_shape_name(output_wrapper.get("value"))
try:
records = extract_records(full_outputs, output_name)
except RuntimeError as exc:
details["error"] = str(exc)
return details
details["record_count"] = len(records)
try:
validate_records(records, output_name)
except RuntimeError as exc:
details["error"] = str(exc)
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
details["shape"] = "object with records list"
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
details["shape"] = "list"
return details
details["usable"] = True
details["error"] = None
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
details["shape"] = "object with records list"
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
details["shape"] = "list"
return details
def choose_output_interactively(outputs: dict, descriptions: list[dict]) -> str | None:
print("\nAvailable Terraform outputs:\n")
indexed = {str(i): item for i, item in enumerate(descriptions, 1)}
by_name = {item["name"]: item for item in descriptions}
for i, item in enumerate(descriptions, 1):
print(f" {i}) {item['name']}")
print(f" usable: {'yes' if item['usable'] else 'no'}")
print(f" shape: {item['shape']}")
print(f" record count: {item['record_count']}")
if item["error"]:
print(f" reason: {item['error']}")
print()
attempts = 0
while attempts < 3:
attempts += 1
try:
selection = input(f"Choose an output to use for DNS records [1-{len(descriptions)}], or press Enter to cancel: ").strip()
except KeyboardInterrupt:
print("\nSelection cancelled.")
return None
if selection == "":
print("Selection cancelled.")
return None
candidate = indexed.get(selection) or by_name.get(selection)
if candidate is None:
print("Invalid selection. Enter a number from the list or an exact output name.")
continue
if not candidate["usable"]:
print(f"Output '{candidate['name']}' is not usable: {candidate['error']}")
continue
return candidate["name"]
raise RuntimeError("Too many invalid selections. Exiting without writing files.")
def tf_name(record: dict) -> str:
base = f"{record.get('hostname', '')}_{record.get('record_type', '')}_{record.get('id', '')}".lower()
base = base.replace("*", "wildcard")
base = re.sub(r"[^a-z0-9_]+", "_", base)
base = re.sub(r"_+", "_", base).strip("_")
if not base or not re.match(r"^[a-z]", base):
base = f"record_{base}" if base else "record"
if not base.endswith(str(record.get("id", ""))):
base = f"{base}_{record.get('id', '')}"
return base
def hcl_value(value):
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value)
def generate_resources(records: list[dict]) -> str:
chunks = [HEADER_TF.rstrip(), ""]
for rec in records:
name = tf_name(rec)
lines = [f'resource "dynu_dns_record" "{name}" {{']
lines.append(f" hostname = {hcl_value(rec.get('hostname'))}")
lines.append(f" record_type = {hcl_value(rec.get('record_type'))}")
if rec.get("ttl") is not None:
lines.append(f" ttl = {hcl_value(rec.get('ttl'))}")
enabled = rec.get("enabled")
if enabled is None:
enabled = rec.get("state")
if enabled is not None:
lines.append(f" enabled = {hcl_value(enabled)}")
content = rec.get("content")
rtype = str(rec.get("record_type", "")).upper()
if content in (None, "") and rtype in {"A", "AAAA"}:
lines.append(" dynamic = true")
elif content not in (None, ""):
lines.append(f" content = {hcl_value(content)}")
for field in OPTIONAL_FIELDS:
value = rec.get(field)
if value not in (None, ""):
lines.append(f" {field.ljust(11)}= {hcl_value(value)}")
lines.extend([
"",
" lifecycle {",
" prevent_destroy = true",
" }",
"}",
"",
])
chunks.extend(lines)
return "\n".join(chunks).rstrip() + "\n"
def generate_import_script(records: list[dict]) -> str:
lines = [HEADER_SH.rstrip(), ""]
for rec in records:
name = tf_name(rec)
import_id = f"{rec['domain_id']}/{rec['id']}"
addr = f"dynu_dns_record.{name}"
lines.append(f"if terraform state show '{addr}' >/dev/null 2>&1; then")
lines.append(f" echo 'Skipping already imported: {addr}'")
lines.append("else")
lines.append(f" terraform import '{addr}' '{import_id}'")
lines.append("fi")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def write_file(path: Path, content: str, dry_run: bool, overwrite: bool) -> None:
if path.exists() and not overwrite:
raise RuntimeError(f"Refusing to overwrite existing file: {path}. Re-run with --overwrite.")
if dry_run:
print(f"[dry-run] Would write {path}")
return
path.write_text(content, encoding="utf-8")
print(f"Wrote {path}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="Print intended output paths without writing files.")
parser.add_argument("--overwrite", "--force", action="store_true", dest="overwrite", help="Overwrite existing generated files.")
parser.add_argument("--from-file", type=Path, help="Load inventory JSON from a file instead of calling terraform output.")
parser.add_argument(
"--records-output",
default=None,
help=(
"Terraform output name containing Dynu DNS records. "
f"Defaults to {DEFAULT_RECORDS_OUTPUT}; if missing in an interactive terminal, "
"the script prompts you to choose from available outputs."
),
)
parser.add_argument("--no-interactive", action="store_true", help="Disable interactive output selection.")
args = parser.parse_args()
records_output_explicit = args.records_output is not None
records_output = args.records_output or DEFAULT_RECORDS_OUTPUT
try:
payload = json.loads(args.from_file.read_text(encoding="utf-8")) if args.from_file else run_terraform_output()
selected_output = records_output
descriptions: list[dict] = []
if isinstance(payload, dict):
descriptions = [describe_output(name, payload[name], payload) for name in sorted(payload)]
try:
records = extract_records(payload, selected_output)
validate_records(records, selected_output)
except RuntimeError as exc:
is_interactive = sys.stdin.isatty() and not args.no_interactive
should_prompt = isinstance(payload, dict) and not records_output_explicit and is_interactive
if should_prompt:
print(f"Terraform output '{selected_output}' was not found or is unusable.\n")
chosen = choose_output_interactively(payload, descriptions)
if chosen is None:
print("Exiting without writing files.")
return 1
selected_output = chosen
records = extract_records(payload, selected_output)
validate_records(records, selected_output)
else:
if isinstance(payload, dict):
available = ", ".join(sorted(payload.keys())) or "(none)"
if records_output_explicit:
raise RuntimeError(
f"Missing or unusable Terraform output '{selected_output}'. "
f"Available outputs: {available}. Details: {exc}"
)
raise RuntimeError(
f"Missing or unusable Terraform output '{selected_output}'. "
f"Available outputs: {available}.\n\n"
"Run interactively to choose an output, or pass one explicitly, for example:\n\n"
" python3 scripts/generate-brownfield-records.py --records-output dynu_dns_inventory --dry-run"
)
raise
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
write_file(INVENTORY_FILE, json.dumps(records, indent=2, sort_keys=True) + "\n", args.dry_run, args.overwrite)
write_file(TF_FILE, generate_resources(records), args.dry_run, args.overwrite)
write_file(IMPORT_SCRIPT, generate_import_script(records), args.dry_run, args.overwrite)
if not args.dry_run:
IMPORT_SCRIPT.chmod(0o755)
return 0
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,7 @@
# Local-only credentials. Do not commit real values.
dynu_api_key = "replace-with-dynu-api-key"
dynu_username = null
dynu_password = null
dynu_root_domain = "lan.ddnsgeek.com"
dynu_record_import_id = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
@@ -0,0 +1,33 @@
variable "dynu_root_domain" {
description = "Dynu root domain name to reconcile/import (for example: lan.ddnsgeek.com)."
type = string
default = "lan.ddnsgeek.com"
}
variable "dynu_api_key" {
description = "Dynu API key/token used by the Dynu Terraform provider."
type = string
sensitive = true
default = null
}
variable "dynu_username" {
description = "Optional Dynu username, only if required by the provider."
type = string
sensitive = true
default = null
}
variable "dynu_password" {
description = "Optional Dynu password, only if required by the provider."
type = string
sensitive = true
default = null
}
variable "dynu_record_import_id" {
description = "Placeholder import ID for a single dynu_dns_record during one-at-a-time reconciliation."
type = string
default = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.6.0"
required_providers {
dynu = {
source = "beatz174-bit/dynu"
}
}
}
@@ -13,10 +13,16 @@ output "physical_hosts" {
value = local.physical_hosts
}
output "virtual_hosts" {
description = "Virtual host/VM inventory used for documentation"
value = local.virtual_hosts
}
output "infrastructure_inventory" {
description = "Combined infrastructure inventory"
value = {
physical_hosts = local.physical_hosts
virtual_hosts = local.virtual_hosts
}
}
+55
View File
@@ -21,4 +21,59 @@ locals {
notes = "Raspberry Pi host"
}
}
# Virtual host inventory for documentation output. This is intentionally
# concise and shaped for docs tooling (not a full provider object dump).
virtual_hosts = {
docker = {
name = "docker"
type = "virtual"
role = "docker-host"
proxmox_node = "pve"
vm_id = 103
management_ip = ""
os_family = "linux"
notes = "Primary Docker VM"
}
server_nixos = {
name = "server-nixos"
type = "virtual"
role = "nixos-server"
proxmox_node = "pve"
vm_id = 104
management_ip = ""
os_family = "nixos"
notes = "General-purpose NixOS VM"
}
nix_cache = {
name = "nix-cache"
type = "virtual"
role = "cache"
proxmox_node = "pve"
vm_id = 105
management_ip = ""
os_family = "linux"
notes = "Nix binary cache VM"
}
pbs = {
name = "pbs"
type = "virtual"
role = "backup"
proxmox_node = "pve"
vm_id = 106
management_ip = ""
os_family = "linux"
notes = "Proxmox Backup Server VM"
}
pihole = {
name = "pihole"
type = "virtual"
role = "dns"
proxmox_node = "pve"
vm_id = 108
management_ip = ""
os_family = "linux"
notes = "DNS filtering VM"
}
}
}
+21
View File
@@ -0,0 +1,21 @@
site_name: Public Infrastructure Documentation
site_description: Public-facing infrastructure documentation
repo_url: https://github.com/beatz174-bit/docker
site_url: https://beatz174-bit.github.io/docker/
docs_dir: docs/public
site_dir: site-public
nav:
- Home: index.md
- Diagrams: diagrams.md
- Compose Inventory: compose-inventory.md
- Traefik Routes: traefik-routes.md
theme:
name: mkdocs
extra_css:
- stylesheets/extra.css
validation:
nav:
omitted_files: ignore
+25
View File
@@ -0,0 +1,25 @@
site_name: Infrastructure Documentation
site_description: Generated and maintained infrastructure documentation
repo_url: https://github.com/beatz174-bit/docker
docs_dir: docs
nav:
- Home: index.md
- Docker Environment: docker.md
- Networking: networking.md
- Monitoring: monitoring.md
- Automation: automation.md
- Operations: operations.md
- Public Showcase: showcase.md
- Generated:
- Compose Inventory: generated/compose-inventory.md
- Traefik Routes: generated/traefik-routes.md
theme:
name: mkdocs
validation:
nav:
omitted_files: ignore
exclude_docs: |
README.md
@@ -1,3 +1,28 @@
{
"dockerUpdateAttempts": {}
"dockerUpdateAttempts": {
"nextcloud-redis|redis:latest|docker": {
"time": 1778483142172,
"status": "test_failed",
"failedAt": 1778483206801,
"notified": true
},
"telegraf|telegraf:latest|raspi": {
"time": 1778569512188,
"status": "pull_failed",
"failedAt": 1778569512904,
"notified": true
},
"traefik|traefik:3|raspi": {
"time": 1778569512188,
"status": "pull_failed",
"failedAt": 1778569512880,
"notified": true
},
"searxng-webapp|searxng/searxng:latest|docker": {
"time": 1778613012267,
"status": "success",
"completedAt": 1778613022513,
"notified": true
}
}
}
@@ -442,3 +442,79 @@
{"ts":"2026-04-17T07:05:12.795Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-17T07:05:12.795Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-17T19:11:29.490Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-21T07:05:42.645Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-21T07:05:42.646Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-21T07:05:59.089Z","flow":"docker-updates","event":"completed","container":"gitea","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-21T07:06:45.971Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-21T07:06:45.971Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-21T07:06:56.112Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-21T07:06:56.112Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-22T07:05:43.226Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-22T07:05:43.226Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-22T07:05:43.237Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-22T07:05:43.237Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-22T07:06:55.068Z","flow":"docker-updates","event":"completed","container":"influxdb","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-22T07:06:55.069Z","flow":"docker-updates","event":"completed","container":"influxdb","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-22T11:50:51.776Z","flow":"docker-updates","event":"completed","container":"gitea","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-24T07:06:46.595Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-24T07:06:46.596Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-25T07:06:01.141Z","flow":"docker-updates","event":"completed","container":"gitea","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":1,"code":0,"error":""}
{"ts":"2026-04-25T07:06:05.952Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-25T08:21:45.451Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-25T08:21:45.451Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-26T07:05:42.647Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-26T07:05:42.647Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-26T07:06:59.195Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-26T19:10:52.147Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-26T19:10:52.183Z","flow":"docker-updates","event":"completed","container":"gitea","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-27T07:05:42.810Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-27T07:05:42.811Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-27T07:05:42.820Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-27T07:05:42.820Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-27T19:11:52.376Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-29T07:06:01.058Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-30T07:05:42.792Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-30T07:05:42.793Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-04-30T07:07:03.429Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-04-30T07:55:52.210Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-02T07:06:02.030Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-03T19:11:00.985Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-04T07:05:43.437Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-04T07:05:43.437Z","flow":"docker-updates","event":"completed","container":"gramps-web-celery","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-04T07:05:43.478Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-04T07:05:43.478Z","flow":"docker-updates","event":"completed","container":"gramps-web","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-04T07:06:49.671Z","flow":"docker-updates","event":"completed","container":"pihole-exporter","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-05T07:06:53.883Z","flow":"docker-updates","event":"completed","container":"influxdb","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-05T07:06:53.883Z","flow":"docker-updates","event":"completed","container":"influxdb","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-05T07:15:53.751Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-05T19:11:46.870Z","flow":"docker-updates","event":"completed","container":"pihole-exporter","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-06T07:05:43.370Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-06T07:05:43.371Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-06T07:06:52.049Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-06T07:06:52.050Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-06T07:07:04.711Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-06T19:21:00.548Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-07T07:05:43.486Z","flow":"docker-updates","event":"completed","container":"portainer-agent","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-07T07:05:43.486Z","flow":"docker-updates","event":"completed","container":"portainer-agent","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-07T07:06:00.453Z","flow":"docker-updates","event":"completed","container":"gramps-redis","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-07T07:06:54.399Z","flow":"docker-updates","event":"completed","container":"portainer","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-07T07:16:46.710Z","flow":"docker-updates","event":"completed","container":"pihole-exporter","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-07T19:11:59.538Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-08T07:25:53.059Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-08T19:10:53.346Z","flow":"docker-updates","event":"completed","container":"gramps-redis","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-08T19:11:48.007Z","flow":"docker-updates","event":"completed","container":"portainer","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":1,"code":0,"error":""}
{"ts":"2026-05-08T19:21:46.912Z","flow":"docker-updates","event":"completed","container":"pihole-exporter","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-09T07:05:43.856Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-09T07:05:43.856Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-09T07:06:56.795Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-09T07:06:56.795Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-09T07:16:59.180Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-09T19:30:53.415Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-11T07:06:02.234Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-11T07:06:46.802Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"failed","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-11T07:06:46.802Z","flow":"docker-updates","event":"completed","container":"nextcloud-redis","project":"unknown","host":"docker","status":"locked","success":0,"failed":1,"duration_ms":0,"code":0,"error":""}
{"ts":"2026-05-12T07:05:12.881Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-12T07:05:12.881Z","flow":"docker-updates","event":"completed","container":"traefik","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-12T07:05:12.904Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"failed","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-12T07:05:12.904Z","flow":"docker-updates","event":"completed","container":"telegraf","project":"unknown","host":"raspi","status":"locked","success":0,"failed":1,"duration_ms":0,"code":1,"error":""}
{"ts":"2026-05-12T19:10:22.519Z","flow":"docker-updates","event":"completed","container":"searxng-webapp","project":"unknown","host":"docker","status":"success","success":1,"failed":0,"duration_ms":0,"code":0,"error":""}
+34 -2
View File
@@ -41,7 +41,8 @@ dummy_value_for_key() {
local key="$1"
case "$key" in
*EMAIL* ) echo "dummy@example.com" ;;
*USER*|*USERNAME* ) echo "dummy-user" ;;
*DB_USER* ) echo "dummyuser" ;;
*USERNAME*|*USER* ) echo "dummy-user" ;;
*DOMAIN* ) echo "example.lan.ddnsgeek.com" ;;
*TZ ) echo "Australia/Brisbane" ;;
*URL* ) echo "https://example.lan.ddnsgeek.com" ;;
@@ -49,7 +50,6 @@ dummy_value_for_key() {
*PASSWORD*|*PASS*|*TOKEN*|*SECRET*|*KEY*|*JWT* ) echo "dummy-${key,,}" ;;
*FINGERPRINT* ) echo "0000000000000000000000000000000000000000" ;;
*DB_NAME* ) echo "dummydb" ;;
*DB_USER* ) echo "dummyuser" ;;
*NAME* ) echo "dummy-name" ;;
*ADDRESS* ) echo "dummy" ;;
* ) echo "dummy-value" ;;
@@ -120,3 +120,35 @@ reconcile_file_based_secrets
echo "== Dummy secret reconciliation complete =="
echo "stack env: $STACK_ENV"
jq -r '.file_based_secrets[].path' "$INVENTORY_JSON" | sed 's/^/file secret: /'
REPO_ROOT="${CODEX_REPO_DIR:-$PWD}"
ANSIBLE_DIR="$REPO_ROOT/infrastructure/ansible"
ANSIBLE_CONFIG="$ANSIBLE_DIR/ansible.cfg"
ANSIBLE_COLLECTIONS_REQ="$ANSIBLE_DIR/collections/requirements.yml"
ANSIBLE_INVENTORY="$ANSIBLE_DIR/inventory/hosts.yml"
ANSIBLE_PING_PLAYBOOK="$ANSIBLE_DIR/playbooks/ping.yml"
if [[ -f "$ANSIBLE_COLLECTIONS_REQ" ]]; then
echo "== Refresh Ansible collections (bootstrap) =="
ansible-galaxy collection install -r "$ANSIBLE_COLLECTIONS_REQ" -p "$ANSIBLE_DIR/collections" || true
fi
if command -v ansible >/dev/null 2>&1; then
echo "== Ansible bootstrap validation =="
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" ansible --version | head -n 1 || true
if command -v ansible-lint >/dev/null 2>&1; then
ansible-lint --version || true
fi
if [[ -f "$ANSIBLE_INVENTORY" ]]; then
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" \
ansible-inventory -i "$ANSIBLE_INVENTORY" --list > /dev/null || true
fi
if [[ -f "$ANSIBLE_PING_PLAYBOOK" && -f "$ANSIBLE_INVENTORY" ]]; then
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" \
ansible-playbook -i "$ANSIBLE_INVENTORY" "$ANSIBLE_PING_PLAYBOOK" --syntax-check || true
fi
fi
+34 -2
View File
@@ -104,7 +104,8 @@ dummy_value_for_key() {
local key="$1"
case "$key" in
*EMAIL* ) echo "dummy@example.com" ;;
*USER*|*USERNAME* ) echo "dummy-user" ;;
*DB_USER* ) echo "dummyuser" ;;
*USERNAME*|*USER* ) echo "dummy-user" ;;
*DOMAIN* ) echo "example.lan.ddnsgeek.com" ;;
*TZ ) echo "Australia/Brisbane" ;;
*URL* ) echo "https://example.lan.ddnsgeek.com" ;;
@@ -112,7 +113,6 @@ dummy_value_for_key() {
*PASSWORD*|*PASS*|*TOKEN*|*SECRET*|*KEY*|*JWT* ) echo "dummy-${key,,}" ;;
*FINGERPRINT* ) echo "0000000000000000000000000000000000000000" ;;
*DB_NAME* ) echo "dummydb" ;;
*DB_USER* ) echo "dummyuser" ;;
*NAME* ) echo "dummy-name" ;;
*ADDRESS* ) echo "dummy" ;;
* ) echo "dummy-value" ;;
@@ -152,6 +152,38 @@ ensure_dummy_secret_files() {
render_dummy_stack_env
ensure_dummy_secret_files
ANSIBLE_DIR="$REPO_ROOT/infrastructure/ansible"
ANSIBLE_CONFIG="$ANSIBLE_DIR/ansible.cfg"
ANSIBLE_COLLECTIONS_REQ="$ANSIBLE_DIR/collections/requirements.yml"
ANSIBLE_INVENTORY="$ANSIBLE_DIR/inventory/hosts.yml"
ANSIBLE_PING_PLAYBOOK="$ANSIBLE_DIR/playbooks/ping.yml"
if [[ -f "$ANSIBLE_COLLECTIONS_REQ" ]]; then
echo "== Ansible collections (bootstrap) =="
ansible-galaxy collection install -r "$ANSIBLE_COLLECTIONS_REQ" -p "$ANSIBLE_DIR/collections" || true
fi
if command -v ansible >/dev/null 2>&1; then
echo "== Ansible bootstrap validation =="
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" ansible --version | head -n 1 || true
if command -v ansible-lint >/dev/null 2>&1; then
ansible-lint --version || true
else
echo "ansible-lint not available; skipping version check"
fi
if [[ -f "$ANSIBLE_INVENTORY" ]]; then
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" \
ansible-inventory -i "$ANSIBLE_INVENTORY" --list > /dev/null || true
fi
if [[ -f "$ANSIBLE_PING_PLAYBOOK" && -f "$ANSIBLE_INVENTORY" ]]; then
ANSIBLE_CONFIG="$ANSIBLE_CONFIG" \
ansible-playbook -i "$ANSIBLE_INVENTORY" "$ANSIBLE_PING_PLAYBOOK" --syntax-check || true
fi
fi
echo
echo "== Installed versions =="
bash --version | head -n 1 || true
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
inv_dir="${repo_root}/infrastructure/terraform/proxmox"
json_out="${repo_root}/data/terraform/proxmox-inventory.json"
md_out="${repo_root}/docs/generated/host-topology.md"
mkdir -p "$(dirname "${json_out}")" "$(dirname "${md_out}")"
(
cd "${inv_dir}"
terraform output -json infrastructure_inventory > "${json_out}"
)
python3 "${repo_root}/scripts/docs/generate_host_topology.py" \
--input "${json_out}" \
--output "${md_out}"
echo "Generated: ${md_out}"
+4
View File
@@ -0,0 +1,4 @@
PROJECT_ROOT=.
TZ=UTC
DOMAIN=example.internal
SECRETS_ENV=scripts/docs/ci-secrets-placeholder.env
+2
View File
@@ -0,0 +1,2 @@
EXAMPLE_PASSWORD=placeholder
EXAMPLE_TOKEN=placeholder
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
ALLOW_STALE_TERRAFORM=0
SKIP_TERRAFORM=0
SKIP_DNS=0
usage() {
cat <<'USAGE'
Usage: scripts/docs/generate-all.sh [--allow-stale-terraform] [--skip-terraform] [--skip-dns]
Options:
--allow-stale-terraform Use existing Terraform JSON when terraform is unavailable.
--skip-terraform Skip Terraform refresh and require existing JSON artifacts.
--skip-dns Skip Dynu DNS Terraform refresh.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--allow-stale-terraform) ALLOW_STALE_TERRAFORM=1 ;;
--skip-terraform) SKIP_TERRAFORM=1 ;;
--skip-dns) SKIP_DNS=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: Unknown option: $1" >&2; usage; exit 1 ;;
esac
shift
done
mkdir -p docs/generated docs/diagrams docs/public data/terraform
[[ -d infrastructure/terraform/proxmox ]] && mkdir -p infrastructure/terraform/proxmox/generated
[[ -d infrastructure/terraform/dynu ]] && mkdir -p infrastructure/terraform/dynu/generated
if ! scripts/docs/render-compose-config.sh; then
echo "ERROR: Docker Compose config render failed." >&2
echo "Check services-up.sh, compose files, and default-environment.env." >&2
exit 1
fi
if [[ ! -s docs/generated/docker-compose.resolved.yml ]]; then
echo "ERROR: Expected non-empty docs/generated/docker-compose.resolved.yml after compose render." >&2
exit 1
fi
PROXMOX_DIR="infrastructure/terraform/proxmox"
PRIMARY_HOST_INV="data/terraform/proxmox-inventory.json"
FALLBACK_HOST_INV="infrastructure/terraform/proxmox/generated/infrastructure_inventory.json"
HOST_INVENTORY=""
if [[ -d "$PROXMOX_DIR" ]]; then
if [[ "$SKIP_TERRAFORM" -eq 1 ]]; then
echo "INFO: --skip-terraform set; using pre-generated Terraform JSON artifacts." >&2
elif command -v terraform >/dev/null 2>&1; then
scripts/docs/build_host_topology.sh
elif [[ "$ALLOW_STALE_TERRAFORM" -eq 1 ]]; then
echo "WARNING: terraform unavailable; using stale Terraform JSON due to --allow-stale-terraform." >&2
else
echo "ERROR: terraform is not available, but $PROXMOX_DIR exists and must be refreshed." >&2
echo "Install terraform and rerun scripts/docs/generate-all.sh, or use --allow-stale-terraform/--skip-terraform intentionally." >&2
exit 1
fi
else
echo "ERROR: Required Terraform directory missing: $PROXMOX_DIR" >&2
echo "Physical topology diagrams require host inventory from Terraform." >&2
exit 1
fi
for p in "$PRIMARY_HOST_INV" "$FALLBACK_HOST_INV"; do
if [[ -s "$p" ]]; then
HOST_INVENTORY="$p"
break
fi
done
if [[ -z "$HOST_INVENTORY" ]]; then
echo "ERROR: Host topology inventory is missing." >&2
echo "Expected one of: $PRIMARY_HOST_INV or $FALLBACK_HOST_INV" >&2
echo "Run scripts/docs/build_host_topology.sh from a machine with Terraform state access, then rerun scripts/docs/generate-all.sh." >&2
exit 1
fi
DNS_INVENTORY="infrastructure/terraform/dynu/generated/dynu_dns_records_inventory.json"
if [[ "$SKIP_DNS" -eq 0 && -d infrastructure/terraform/dynu ]]; then
if [[ "$SKIP_TERRAFORM" -eq 1 ]]; then
echo "INFO: Skipping Dynu Terraform refresh due to --skip-terraform." >&2
elif command -v terraform >/dev/null 2>&1; then
scripts/docs/generate_dynu_dns_inventory.sh
elif [[ "$ALLOW_STALE_TERRAFORM" -eq 1 ]]; then
echo "WARNING: terraform unavailable; using stale Dynu DNS inventory due to --allow-stale-terraform." >&2
else
echo "ERROR: terraform unavailable; cannot refresh Dynu DNS inventory while infrastructure/terraform/dynu exists." >&2
echo "Install terraform and rerun, or use --allow-stale-terraform/--skip-terraform intentionally." >&2
exit 1
fi
fi
python3 scripts/docs/generate-compose-inventory.py docs/generated/docker-compose.resolved.yml docs/generated/compose-inventory.md
python3 scripts/docs/generate-traefik-routes.py docs/generated/docker-compose.resolved.yml docs/generated/traefik-routes.md
python3 scripts/docs/generate-docs-index.py docs/generated/index.md
GEN_ARGS=(--compose docs/generated/docker-compose.resolved.yml --out-dir docs/diagrams --domain-display redacted-label --host-inventory "$HOST_INVENTORY")
[[ -s "$DNS_INVENTORY" && "$SKIP_DNS" -eq 0 ]] && GEN_ARGS+=(--dns-inventory "$DNS_INVENTORY")
python3 scripts/docs/generate-diagrams.py "${GEN_ARGS[@]}"
python3 scripts/docs/sanitize-public-docs.py docs/generated docs/diagrams docs/public
[[ -s docs/public/physical-topology.svg ]] || { echo "ERROR: docs/public/physical-topology.svg missing or empty." >&2; exit 1; }
if grep -Fq "Host inventory JSON not found" docs/public/physical-topology.svg || grep -Fq "Generate terraform inventory" docs/public/physical-topology.svg; then
echo "ERROR: docs/public/physical-topology.svg contains placeholder/error text; host inventory refresh failed." >&2
exit 1
fi
[[ -s docs/public/docker-traefik-dynu.svg ]] || { echo "ERROR: docs/public/docker-traefik-dynu.svg missing or empty." >&2; exit 1; }
[[ -s docs/generated/docker-compose.resolved.yml ]] || { echo "ERROR: docs/generated/docker-compose.resolved.yml missing or empty." >&2; exit 1; }
[[ -s docs/generated/host-topology.md ]] || { echo "ERROR: docs/generated/host-topology.md missing or empty." >&2; exit 1; }
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import hashlib
import sys, yaml
def md(v): return str(v).replace('|','\\|') if v is not None else ''
inp,out=sys.argv[1],sys.argv[2]
with open(inp) as f: c=yaml.safe_load(f) or {}
svcs=c.get('services',{}) or {}
nets=c.get('networks',{}) or {}
vols=c.get('volumes',{}) or {}
raw=open(inp,'rb').read()
fingerprint=hashlib.sha256(raw).hexdigest()[:12]
lines=["# Docker Compose Inventory","",f"Source fingerprint: `{fingerprint}`","","## Summary","","| Item | Count |","|---|---:|",f"| Services | {len(svcs)} |",f"| Networks | {len(nets)} |",f"| Volumes | {len(vols)} |","","## Services","","| Service | Container | Image | Build | Profiles | Networks | Ports | Restart |","|---|---|---|---|---|---|---|---|"]
for n,s in sorted(svcs.items()):
build=s.get('build','')
if isinstance(build,dict): build=build.get('context','')
ports=', '.join(str(p) for p in s.get('ports',[]) )
networks=', '.join((s.get('networks') or {}).keys() if isinstance(s.get('networks'),dict) else (s.get('networks') or []))
profiles=', '.join(s.get('profiles',[]) or [])
lines.append(f"| {md(n)} | {md(s.get('container_name',''))} | {md(s.get('image',''))} | {md(build)} | {md(profiles)} | {md(networks)} | {md(ports)} | {md(s.get('restart',''))} |")
lines += ["","## Networks","","| Network | Driver | External |","|---|---|---|"]
for n,v in sorted(nets.items()): lines.append(f"| {md(n)} | {md((v or {}).get('driver',''))} | {md((v or {}).get('external',False))} |")
lines += ["","## Volumes","","| Volume | External |","|---|---|"]
for n,v in sorted(vols.items()): lines.append(f"| {md(n)} | {md((v or {}).get('external',False))} |")
open(out,'w').write('\n'.join(lines)+'\n')
+360
View File
@@ -0,0 +1,360 @@
#!/usr/bin/env python3
import argparse
import json
import re
import subprocess
import shutil
from pathlib import Path
import yaml
INTERNAL_DOMAIN_RE = re.compile(r"\b[a-zA-Z0-9.-]+\.lan\.ddnsgeek\.com\b")
HOST_MATCH_RE = re.compile(r"Host\(([^)]*)\)")
TICKED_HOST_RE = re.compile(r"`([^`]+)`")
def require_dot() -> None:
if not shutil.which("dot"):
raise SystemExit(
"Graphviz 'dot' not found in environment. Install graphviz before running docs generation."
)
def load_compose(path: Path) -> dict:
with path.open() as handle:
return yaml.safe_load(handle) or {}
PARENT_KEYS = ("node", "node_name", "host", "physical_host", "hypervisor_host", "proxmox_node")
def display_domain(value: str, mode: str) -> str:
if mode == "full":
return value
if mode == "placeholder":
return "<internal-domain>" if INTERNAL_DOMAIN_RE.search(value) else value
if INTERNAL_DOMAIN_RE.search(value):
label = re.sub(r"\.lan\.ddnsgeek\.com$", "", value)
return f"{label}.<domain>"
return value
def parse_labels(service: dict) -> dict[str, str]:
labels = service.get("labels") or {}
if isinstance(labels, list):
mapped = {}
for item in labels:
if "=" in str(item):
k, v = str(item).split("=", 1)
mapped[k] = v
return mapped
if isinstance(labels, dict):
return {str(k): str(v) for k, v in labels.items()}
return {}
def extract_hosts(rule: str) -> list[str]:
hosts: list[str] = []
for m in HOST_MATCH_RE.findall(rule or ""):
for host in TICKED_HOST_RE.findall(m):
hosts.append(host.strip())
return hosts
def render_svg(dot_path: Path, svg_path: Path) -> None:
subprocess.run(["dot", "-Tsvg", str(dot_path), "-o", str(svg_path)], check=True)
def write_dot(path: Path, lines: list[str]) -> None:
path.write_text("\n".join(lines) + "\n")
def infer_host(service_name: str, service: dict) -> str:
labels = parse_labels(service)
for key in ("com.docker.compose.project", "infra.host", "host"):
value = labels.get(key)
if value:
return value.lower()
s = service_name.lower()
if "raspi" in s or "pi" in s:
return "raspberrypi"
if "proxmox" in s:
return "proxmox"
return "docker"
def categorize_service(service_name: str) -> str:
s = service_name.lower()
if any(k in s for k in ["traefik", "authelia", "oauth", "auth", "proxy", "nginx", "caddy"]):
return "edge/proxy/auth"
if any(k in s for k in ["prometheus", "grafana", "loki", "promtail", "alert", "node-exporter", "cadvisor"]):
return "monitoring"
if any(k in s for k in ["watchtower", "diun", "ansible", "cron", "runner", "backup"]):
return "automation"
if any(k in s for k in ["postgres", "mariadb", "mysql", "redis", "minio", "nfs", "storage", "db", "queue"]):
return "storage/database/support"
return "apps"
def load_inventory(path: Path | None) -> dict:
if not path or not path.exists():
return {}
payload = json.loads(path.read_text())
return payload.get("value", payload) if isinstance(payload, dict) else {}
def to_records(data) -> list[dict]:
if not isinstance(data, dict):
return []
out = []
for key, value in data.items():
if isinstance(value, dict):
rec = dict(value)
rec.setdefault("_key", str(key))
rec.setdefault("name", rec.get("hostname") or rec.get("vm_name") or str(key))
out.append(rec)
return out
def parent_name(item: dict) -> str:
for k in PARENT_KEYS:
v = item.get(k)
if v:
return str(v)
return ""
def generate_physical_topology(compose: dict, inventory: dict, out_dot: Path, out_svg: Path) -> None:
physical = to_records(inventory.get("physical_hosts", {}))
virtual = to_records(inventory.get("virtual_hosts", {})) + to_records(inventory.get("vms", {}))
if not physical and not virtual:
lines = [
"digraph PhysicalTopology {",
" graph [rankdir=LR, fontname=\"Helvetica\", nodesep=1.0, ranksep=1.5];",
' "placeholder:inventory" [shape=note, style="filled", fillcolor="#fef3c7", label="Host inventory JSON not found.\\nGenerate terraform inventory and rerun scripts/docs/generate-all.sh\\n(--host-inventory <path>)."];',
]
lines.append("}")
write_dot(out_dot, lines)
render_svg(out_dot, out_svg)
return
lines = [
"digraph PhysicalTopology {",
" graph [rankdir=LR, compound=true, splines=polyline, nodesep=0.95, ranksep=1.7, ratio=compress, fontname=\"Helvetica\", fontsize=13, concentrate=true, newrank=true];",
" node [fontname=\"Helvetica\", fontsize=12, style=\"rounded,filled\", fillcolor=\"#ffffff\"];",
" edge [fontname=\"Helvetica\", fontsize=10, color=\"#64748b\"];",
]
phys_names = {str(p.get("name")): p for p in physical}
children: dict[str, list[dict]] = {k: [] for k in phys_names}
orphans: list[dict] = []
for vm in virtual:
parent = parent_name(vm)
if parent in children:
children[parent].append(vm)
else:
orphans.append(vm)
for host, record in sorted(phys_names.items()):
host_role = str(record.get("role", "") or "")
cluster_label = f"{host}\\n{host_role}" if host_role else host
lines.extend([
f' subgraph "cluster_{host}" {{',
f' label="{cluster_label}";',
' style="rounded,filled";',
' color="#60a5fa";',
' fillcolor="#eff6ff";',
f' "phys:{host}" [label="{host}", shape=box3d, fillcolor="#bfdbfe"];',
])
for vm in sorted(children.get(host, []), key=lambda x: str(x.get("name", "")).lower()):
vm_name = str(vm.get("name"))
vm_role = str(vm.get("role", "") or "virtual host")
cluster_id = f"cluster_{host}_{re.sub(r'[^a-zA-Z0-9]+', '_', vm_name)}"
lines.extend([
f' subgraph "{cluster_id}" {{',
f' label="{vm_name}";',
' style="rounded,dashed";',
' color="#bfdbfe";',
' fillcolor="#f8fbff";',
f' "vm:{vm_name}" [label="{vm_name}\\n{vm_role}", shape=component, fillcolor="#dcfce7"];',
])
if "docker" in vm_role.lower() or "docker" in vm_name.lower():
lines.append(f' "role:{vm_name}" [label="Docker host", shape=box, fillcolor="#fef3c7"];')
lines.append(f' "vm:{vm_name}" -> "role:{vm_name}" [style=dashed, label="runs"];')
lines.append(' }')
lines.append(f' "phys:{host}" -> "vm:{vm_name}" [label="hosts"];')
lines.append(' }')
if orphans:
lines.extend([
' subgraph "cluster_orphans" {',
' label="Unmapped virtual hosts"; style="rounded,dashed"; color="#d1d5db";',
])
for vm in sorted(orphans, key=lambda x: str(x.get("name", "")).lower()):
vm_name = str(vm.get("name"))
lines.append(f' "vm:{vm_name}" [label="{vm_name}", shape=component, fillcolor="#fee2e2"];')
lines.append(" }")
lines.extend([
' subgraph "cluster_legend" {',
' label="Legend"; style="rounded"; color="#d1d5db";',
' "leg_host" [label="Physical host", shape=box3d, fillcolor="#eff6ff"];',
' "leg_vm" [label="Virtual machine", shape=component, fillcolor="#dcfce7"];',
' "leg_role" [label="Hosted role", shape=box, fillcolor="#fef3c7"];',
' "leg_host" -> "leg_vm" [label="hosts"];',
' "leg_vm" -> "leg_role" [style=dashed, label="runs"];',
' }',
"}",
])
write_dot(out_dot, lines)
render_svg(out_dot, out_svg)
def generate_docker_traefik_dynu(compose: dict, dns_inventory: dict, domain_mode: str, out_dot: Path, out_svg: Path) -> None:
services = compose.get("services") or {}
networks = compose.get("networks") or {}
lines = [
"digraph DockerTraefikDynu {",
" graph [rankdir=LR, compound=true, splines=polyline, nodesep=0.9, ranksep=1.6, fontname=\"Helvetica\", concentrate=true, newrank=true];",
" node [fontname=\"Helvetica\", fontsize=11, style=\"rounded,filled\"];",
" edge [fontname=\"Helvetica\", fontsize=9, color=\"#334155\"];",
' "dynu" [label="Dynu / Public DNS", shape=box, fillcolor="#fde68a"];',
' "svc:traefik" [label="Traefik", shape=box, fillcolor="#bfdbfe"];',
' "dynu" -> "svc:traefik" [penwidth=1.6];',
]
routes: dict[str, dict] = {}
dns_nodes: set[str] = set()
for svc_name, svc in sorted(services.items()):
labels = parse_labels(svc)
router_prefix = "traefik.http.routers."
service_prefix = "traefik.http.services."
lb_ports = {}
for k, v in labels.items():
if k.startswith(service_prefix) and k.endswith(".loadbalancer.server.port"):
lb_ports[k[len(service_prefix):].split(".", 1)[0]] = v
routers = sorted({k[len(router_prefix):].split(".", 1)[0] for k in labels if k.startswith(router_prefix)})
for router in routers:
rule = labels.get(f"{router_prefix}{router}.rule", "")
target = labels.get(f"{router_prefix}{router}.service", svc_name)
middlewares = labels.get(f"{router_prefix}{router}.middlewares", "")
tls = labels.get(f"{router_prefix}{router}.tls", "false")
port = lb_ports.get(target, "")
badges = []
if str(tls).lower() in ("true", "1"):
badges.append("TLS")
mw_low = middlewares.lower()
if "authelia" in mw_low:
badges.append("authelia")
if "mtls" in mw_low:
badges.append("mTLS")
hosts = [display_domain(h, domain_mode) for h in extract_hosts(rule)]
if not hosts:
continue
info = routes.setdefault(svc_name, {"hosts": set(), "port": port, "badges": set()})
info["hosts"].update(hosts)
if port:
info["port"] = port
info["badges"].update(badges)
for svc_name, info in sorted(routes.items()):
label = svc_name
if info.get("port"):
label += f"\n:{info['port']}"
if info.get("badges"):
label += "\n[" + ", ".join(sorted(info["badges"])) + "]"
lines.append(f' "svc:{svc_name}" [label="{label}", shape=box, fillcolor="#dcfce7"];')
lines.append(f' "svc:traefik" -> "svc:{svc_name}" [penwidth=1.4];')
for host in sorted(info["hosts"]):
dns_nodes.add(host)
lines.append(f' "dns:{host}" [label="{host}", shape=note, fillcolor="#fef3c7"];')
lines.append(f' "dns:{host}" -> "dynu";')
for record in (dns_inventory.get("records", []) if isinstance(dns_inventory, dict) else []):
host = record.get("hostname") or record.get("name")
if not host:
continue
host_disp = display_domain(str(host), domain_mode)
if host_disp in dns_nodes:
continue
dns_nodes.add(host_disp)
lines.append(f' "dns:{host_disp}" [label="{host_disp}", shape=note, fillcolor="#fef3c7"];')
lines.append(f' "dns:{host_disp}" -> "dynu" [style=dashed, color=\"#94a3b8\"];')
lines.append(' { rank=same; ' + '; '.join([f'"dns:{d}"' for d in sorted(dns_nodes)]) + '; }' if dns_nodes else '')
lines.append(' subgraph "cluster_networks" {')
lines.append(' label="Docker backend networks"; style="rounded,dashed"; color="#d1d5db";')
for net in sorted(networks.keys()):
lines.append(f' "net:{net}" [label="{net}", shape=ellipse, fillcolor="#f8fafc"];')
lines.append(' }')
for svc_name in sorted(routes.keys()):
svc = services.get(svc_name, {})
svc_nets = svc.get("networks") or []
if isinstance(svc_nets, dict):
svc_nets = svc_nets.keys()
for net in svc_nets:
lines.append(f' "svc:{svc_name}" -> "net:{net}" [style=dashed, color="#94a3b8", arrowsize=0.7];')
lines.append("}")
write_dot(out_dot, lines)
render_svg(out_dot, out_svg)
def generate_compose_topology(compose: dict, out_dot: Path, out_svg: Path) -> None:
services = compose.get("services") or {}
networks = compose.get("networks") or {}
lines = [
"digraph Compose {",
" rankdir=LR;",
' node [fontname="Helvetica"];',
]
for service in sorted(services):
lines.append(f' "svc:{service}" [label="{service}", shape=box, style=filled, fillcolor="#dfefff"];')
for net in sorted(networks):
lines.append(f' "net:{net}" [label="{net}", shape=ellipse, style=filled, fillcolor="#f4f4f4"];')
for service, svc in sorted(services.items()):
svc_nets = svc.get("networks") or []
if isinstance(svc_nets, dict):
svc_nets = svc_nets.keys()
for net in svc_nets:
lines.append(f' "svc:{service}" -> "net:{net}";')
lines.append("}")
write_dot(out_dot, lines)
render_svg(out_dot, out_svg)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("legacy", nargs="*")
parser.add_argument("--compose")
parser.add_argument("--out-dir")
parser.add_argument("--host-inventory")
parser.add_argument("--dns-inventory")
parser.add_argument("--domain-display", choices=["full", "redacted-label", "placeholder"], default="redacted-label")
args = parser.parse_args()
require_dot()
if args.compose and args.out_dir:
compose_path = Path(args.compose)
out_dir = Path(args.out_dir)
elif len(args.legacy) == 3:
compose_path = Path(args.legacy[0])
out_dir = Path(args.legacy[1]).parent
else:
raise SystemExit("Usage: generate-diagrams.py --compose <compose.yml> --out-dir <dir>")
out_dir.mkdir(parents=True, exist_ok=True)
compose = load_compose(compose_path)
host_inventory = load_inventory(Path(args.host_inventory)) if args.host_inventory else {}
dns_inventory = load_inventory(Path(args.dns_inventory)) if args.dns_inventory else {}
generate_docker_traefik_dynu(compose, dns_inventory, args.domain_display, out_dir / "docker-traefik-dynu.dot", out_dir / "docker-traefik-dynu.svg")
generate_physical_topology(compose, host_inventory, out_dir / "physical-topology.dot", out_dir / "physical-topology.svg")
generate_compose_topology(compose, out_dir / "docker-compose.dot", out_dir / "docker-compose.svg")
if __name__ == "__main__":
main()
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
out = Path(sys.argv[1])
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(
"""# Generated Documentation
This directory contains documentation generated automatically from repository configuration.
## Files
- [Compose file list](compose-files.txt)
- [Resolved Docker Compose config](docker-compose.resolved.yml)
- [Compose inventory](compose-inventory.md)
- [Traefik routes](traefik-routes.md)
- [Docker Compose diagram](../diagrams/docker-compose.svg)
"""
)
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
import sys,yaml,glob
out=sys.argv[1]
patterns=["monitoring/prometheus/rules/**/*.yml","monitoring/prometheus/rules/**/*.yaml","**/prometheus/rules/**/*.yml","**/prometheus/rules/**/*.yaml"]
files=sorted({f for p in patterns for f in glob.glob(p,recursive=True)})
lines=["# Prometheus Rules","", "| File | Group | Alert | Expr | For | Labels | Annotations |","|---|---|---|---|---|---|---|"]
if not files:
open(out,'w').write("# Prometheus Rules\n\nNo Prometheus rule files were found.\n"); sys.exit(0)
for fp in files:
try:data=yaml.safe_load(open(fp)) or {}
except Exception as e: raise SystemExit(f"Malformed YAML in {fp}: {e}")
for g in data.get('groups',[]) or []:
for r in g.get('rules',[]) or []:
lines.append(f"| {fp} | {g.get('name','')} | {r.get('alert','')} | {str(r.get('expr','')).replace('|','\\|')} | {r.get('for','')} | {r.get('labels',{})} | {r.get('annotations',{})} |")
open(out,'w').write('\n'.join(lines)+'\n')
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import sys,yaml,re
inp,out=sys.argv[1],sys.argv[2]
with open(inp) as f: c=yaml.safe_load(f) or {}
rows=[]
for sname,svc in (c.get('services') or {}).items():
labels=svc.get('labels') or {}
if isinstance(labels,list):
d={}
for l in labels:
if '=' in str(l):k,v=str(l).split('=',1);d[k]=v
labels=d
routers={}
for k,v in labels.items():
m=re.match(r'traefik\.http\.routers\.([^.]+)\.(rule|entrypoints|tls|middlewares)$',k)
if m: routers.setdefault(m.group(1),{})[m.group(2)]=v
ports={}
for k,v in labels.items():
m=re.match(r'traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$',k)
if m: ports[m.group(1)]=v
for r,rv in routers.items():
rows.append((sname,r,rv.get('rule',''),rv.get('entrypoints',''),rv.get('tls',''),rv.get('middlewares',''),ports.get(r,'')))
lines=["# Traefik Routes","", "| Service | Router | Rule | Entrypoints | TLS | Middlewares | Target Port |","|---|---|---|---|---|---|---|"]
if not rows:
lines=["# Traefik Routes","","No Traefik routes were detected."]
else:
for r in sorted(rows): lines.append('| '+' | '.join(str(x).replace('|','\\|') for x in r)+' |')
open(out,'w').write('\n'.join(lines)+'\n')
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
dynu_dir="${repo_root}/infrastructure/terraform/dynu"
out_json="${dynu_dir}/generated/dynu_dns_records_inventory.json"
if [[ ! -d "${dynu_dir}" ]]; then
echo "ERROR: Dynu Terraform directory not found at ${dynu_dir}." >&2
exit 1
fi
if ! command -v terraform >/dev/null 2>&1; then
echo "ERROR: terraform is required to refresh Dynu DNS inventory." >&2
exit 1
fi
mkdir -p "$(dirname "${out_json}")"
(
cd "${dynu_dir}"
if terraform output -json dynu_dns_inventory > "${out_json}"; then
:
elif terraform output -json dynu_dns_records_catalog > "${out_json}"; then
:
else
echo "ERROR: Failed to export Dynu DNS inventory from Terraform outputs. Tried: dynu_dns_inventory, dynu_dns_records_catalog." >&2
echo "Run 'cd infrastructure/terraform/dynu && terraform output' to inspect available outputs." >&2
exit 1
fi
)
if [[ ! -s "${out_json}" ]]; then
echo "ERROR: Dynu DNS inventory output file is empty: ${out_json}" >&2
exit 1
fi
echo "Generated: ${out_json}"
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Generate host topology Markdown/Mermaid from Terraform inventory JSON."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
PARENT_KEYS = [
"node",
"node_name",
"host",
"physical_host",
"hypervisor_host",
"proxmox_node",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate Markdown topology from terraform output JSON"
)
parser.add_argument("--input", required=True, help="Input JSON path")
parser.add_argument("--output", required=True, help="Output Markdown path")
return parser.parse_args()
def load_inventory(raw: dict[str, Any]) -> dict[str, Any]:
payload = raw.get("value", raw)
if not isinstance(payload, dict):
raise ValueError("Inventory payload must be an object")
return payload
def infer_name(key: str, item: dict[str, Any]) -> str:
for candidate in ("name", "hostname", "vm_name", "id"):
value = item.get(candidate)
if value:
return str(value)
return key
def to_records(host_map: Any) -> list[dict[str, Any]]:
if isinstance(host_map, dict):
records: list[dict[str, Any]] = []
for key, val in host_map.items():
if isinstance(val, dict):
rec = dict(val)
rec.setdefault("_key", str(key))
rec.setdefault("name", infer_name(str(key), rec))
records.append(rec)
return sorted(records, key=lambda x: str(x.get("name", "")).lower())
return []
def escape_cell(value: Any) -> str:
if value is None:
return ""
text = str(value).replace("|", "\\|").replace("\n", "<br>")
return text
def get_first(item: dict[str, Any], *keys: str) -> str:
for key in keys:
value = item.get(key)
if value is not None and value != "":
return str(value)
return ""
def detect_virtual_hosts(inv: dict[str, Any]) -> list[dict[str, Any]]:
virtual: list[dict[str, Any]] = []
for key in ("virtual_hosts", "vms"):
virtual.extend(to_records(inv.get(key, {})))
return sorted(virtual, key=lambda x: str(x.get("name", "")).lower())
def build_mermaid(physical: list[dict[str, Any]], virtual: list[dict[str, Any]]) -> list[str]:
lines = ["```mermaid", "flowchart TD"]
for p in physical:
pname = get_first(p, "name", "hostname", "_key")
pid = f"phys_{pname.lower().replace('-', '_').replace(' ', '_')}"
lines.append(f' {pid}["{pname}\\nphysical"]')
for v in virtual:
vname = get_first(v, "name", "hostname", "_key")
vid = f"virt_{vname.lower().replace('-', '_').replace(' ', '_')}"
parent = ""
for k in PARENT_KEYS:
parent = get_first(v, k)
if parent:
break
lines.append(f' {vid}["{vname}\\nvirtual"]')
if parent:
pid = f"phys_{parent.lower().replace('-', '_').replace(' ', '_')}"
lines.append(f" {pid} --> {vid}")
lines.append("```")
return lines
def build_table(headers: list[str], rows: list[list[str]]) -> list[str]:
out = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"]
for row in rows:
out.append("| " + " | ".join(escape_cell(cell) for cell in row) + " |")
return out
def main() -> int:
args = parse_args()
raw = json.loads(Path(args.input).read_text(encoding="utf-8"))
inv = load_inventory(raw)
physical = to_records(inv.get("physical_hosts", {}))
virtual = detect_virtual_hosts(inv)
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
lines: list[str] = [
"# Host Topology",
"",
f"> Generated by `scripts/docs/generate_host_topology.py` on {now}.",
"",
"## Topology Diagram",
"",
]
lines.extend(build_mermaid(physical, virtual))
lines.extend(["", "## Physical Hosts", ""])
physical_rows = [
[
get_first(p, "name", "hostname", "_key"),
get_first(p, "type"),
get_first(p, "role"),
get_first(p, "management", "management_ip"),
get_first(p, "os", "os_family"),
get_first(p, "hypervisor"),
get_first(p, "location"),
get_first(p, "notes"),
]
for p in physical
]
lines.extend(
build_table(
["Name", "Type", "Role", "Management", "OS", "Hypervisor", "Location", "Notes"],
physical_rows,
)
)
lines.extend(["", "## Virtual Hosts", ""])
if virtual:
virtual_rows = []
for v in virtual:
parent = ""
for k in PARENT_KEYS:
parent = get_first(v, k)
if parent:
break
virtual_rows.append(
[
get_first(v, "name", "hostname", "_key"),
get_first(v, "type"),
get_first(v, "role"),
parent,
get_first(v, "management", "management_ip", "ip"),
get_first(v, "os", "os_family"),
get_first(v, "notes"),
]
)
lines.extend(
build_table(
["Name", "Type", "Role", "Parent/Node", "Management", "OS", "Notes"],
virtual_rows,
)
)
else:
lines.append("No VM/virtual host data found in the current Terraform inventory output.")
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
# Discover compose files for docs tooling and CI without running containers.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
declare -a files=()
if [ -f services-up.sh ]; then
# Parse literal FILES array entries in services-up.sh (e.g. default-network.yml).
while IFS= read -r line; do
path=$(sed -E 's#.*\$PROJECT_ROOT/([^" ]+).*#\1#' <<<"$line")
[ -f "$path" ] && files+=("$path")
done < <(awk '/^FILES=\(/,/^\)/ {print}' services-up.sh | grep -E '\-f[[:space:]]+"\$PROJECT_ROOT/')
# Reuse the same compose roots used by services-up.sh to avoid archived compose files.
if grep -q 'find "\$PROJECT_ROOT/apps" "\$PROJECT_ROOT/monitoring" "\$PROJECT_ROOT/core"' services-up.sh; then
while IFS= read -r f; do files+=("$f"); done < <(
find apps monitoring core -maxdepth 2 -type f \
\( -name 'docker-compose.yml' -o -name 'docker-compose.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) \
| sed 's#^\./##' | sort
)
fi
fi
if [ "${#files[@]}" -eq 0 ]; then
while IFS= read -r f; do files+=("$f"); done < <(
find . -type f \
\( -name 'docker-compose.yml' -o -name 'docker-compose.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) \
| sed 's#^\./##' | grep -v '^archive/' | sort
)
fi
printf '%s\n' "${files[@]}" | awk 'NF' | awk '!seen[$0]++'
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
mkdir -p docs/generated
mapfile -t COMPOSE_FILES < <(scripts/docs/list-compose-files.sh)
if [ "${#COMPOSE_FILES[@]}" -eq 0 ]; then
echo "No compose files found" >&2
exit 1
fi
printf '%s\n' "${COMPOSE_FILES[@]}" > docs/generated/compose-files.txt
if [ ! -x ./services-up.sh ]; then
echo "services-up.sh is missing or not executable" >&2
exit 1
fi
./services-up.sh --profile all config > docs/generated/docker-compose.resolved.yml
service_count="$(
python3 - <<'PY'
import yaml
from pathlib import Path
data = yaml.safe_load(Path("docs/generated/docker-compose.resolved.yml").read_text()) or {}
print(len(data.get("services") or {}))
PY
)"
if [ "$service_count" -eq 0 ]; then
echo "ERROR: rendered compose config contains zero services; check --profile all / COMPOSE_PROFILES." >&2
exit 1
fi
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
src_generated = Path(sys.argv[1])
src_diagrams = Path(sys.argv[2])
out_dir = Path(sys.argv[3])
out_dir.mkdir(parents=True, exist_ok=True)
def sanitize_text(content: str) -> str:
content = re.sub(r'\b([a-zA-Z0-9-]+)\.lan\.ddnsgeek\.com\b', r'\1.<domain>', content)
content = re.sub(
r'\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b',
'<private-ip>',
content,
)
content = re.sub(r'(?i)\b(password|token|api[_-]?key|secret)\s*[:=]\s*[^\s\n]+', r'\1=<redacted>', content)
content = re.sub(r'(?m)^([A-Z0-9_]*(?:PASSWORD|TOKEN|API_KEY|SECRET)[A-Z0-9_]*)\s*[:=]\s*.*$', r'\1=<redacted>', content)
return content
for name in ['compose-inventory.md', 'traefik-routes.md']:
src = src_generated / name
if src.exists():
(out_dir / name).write_text(sanitize_text(src.read_text(errors='ignore')))
for svg_name in ['docker-compose.svg', 'physical-topology.svg', 'docker-traefik-dynu.svg']:
src = src_diagrams / svg_name
if src.exists():
(out_dir / svg_name).write_text(sanitize_text(src.read_text(errors='ignore')))
(out_dir / 'index.md').write_text(
"""# Public Infrastructure Summary
This documentation is generated from the infrastructure repository. Sensitive values are redacted.
> Generated docs are sanitised/redacted before publishing to GitHub Pages.
## Infrastructure diagrams
### Physical / virtual topology
![Physical topology](physical-topology.svg)
### Docker, Traefik and Dynu routing
![Docker Traefik Dynu](docker-traefik-dynu.svg)
## Documents
- [Diagrams](diagrams.md)
- [Compose Inventory](compose-inventory.md)
- [Traefik Routes](traefik-routes.md)
"""
)
(out_dir / 'diagrams.md').write_text(
"""# Infrastructure diagrams
## Physical / virtual topology
This view groups containers by inferred host and service role (edge/proxy/auth, monitoring, automation, apps, and supporting storage/services).
<div class="diagram-wrap">
<img src="physical-topology.svg" alt="Physical topology">
</div>
## Docker, Traefik and Dynu routing
This view shows sanitised public DNS names flowing to Traefik, then to exposed Docker services, with backend Docker network membership shown as secondary context.
_Diagrams are generated from Compose data and Traefik labels._
<div class="diagram-wrap">
<img src="docker-traefik-dynu.svg" alt="Docker Traefik Dynu">
</div>
"""
)
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
# This integration is intentionally read-only.
# No Dynu mutations are permitted in this repo at this stage.
# Optional convenience: auto-load local Dynu env file when variables are unset.
if [[ -f "secrets/dynu.env" ]]; then
set -a
# shellcheck source=/dev/null
source "secrets/dynu.env"
set +a
fi
if [[ "${DYNU_READ_ONLY:-}" != "true" ]]; then
echo "Refusing to run: DYNU_READ_ONLY must be exactly 'true'." >&2
exit 2
fi
if [[ -z "${DYNU_API_KEY:-}" ]]; then
echo "Missing DYNU_API_KEY. Set it in env or secrets/dynu.env." >&2
exit 2
fi
python3 scripts/dynu/fetch_dynu_dns.py
python3 scripts/dynu/correlate_dynu_with_traefik.py
+465
View File
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
"""Correlate Dynu DNS data with Traefik host rules in compose sources.
This integration is intentionally read-only.
No Dynu mutations are permitted in this repo at this stage.
"""
from __future__ import annotations
import json
import os
import re
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Set
import yaml
BASE_DOMAIN = "lan.ddnsgeek.com"
ALLOWED_UNMAPPED_HOSTNAMES = ["edge.lan.ddnsgeek.com"]
DYN_DATA = Path("data/dns/dynu_live.json")
OUT_JSON = Path("data/dns/dynu_traefik_inventory.json")
OUT_MD = Path("docs/generated/dns-inventory.md")
HOST_CALL_RE = re.compile(r"Host\s*\(([^)]*)\)", re.IGNORECASE)
QUOTED_HOST_RE = re.compile(r"[`\"']([^`\"']+)[`\"']")
ROUTER_LABEL_RE = re.compile(r"^traefik\.http\.routers\.([^.]+)\.(.+)$")
class ReadOnlyError(RuntimeError):
pass
def require_read_only() -> None:
if os.environ.get("DYNU_READ_ONLY") != "true":
raise ReadOnlyError(
"Refusing to run: DYNU_READ_ONLY must be exactly 'true'. "
"This integration is intentionally read-only."
)
def compose_files(root: Path) -> List[Path]:
files: Set[Path] = set()
if (root / "default-network.yml").exists():
files.add(root / "default-network.yml")
for area in ("apps", "monitoring", "core"):
base = root / area
if not base.exists():
continue
for pattern in ("**/docker-compose.yml", "**/docker-compose.yaml"):
files.update(p for p in base.glob(pattern) if p.is_file())
return sorted(files)
def parse_hosts_from_rule(rule: str) -> List[str]:
hosts: Set[str] = set()
for call_fragment in HOST_CALL_RE.findall(rule):
quoted_hosts = QUOTED_HOST_RE.findall(call_fragment)
for host in quoted_hosts:
clean = host.strip().strip(".").lower()
if clean:
hosts.add(clean)
if not quoted_hosts:
for token in call_fragment.split(","):
clean = token.strip().strip(".`\"'").lower()
if clean:
hosts.add(clean)
return sorted(hosts)
def load_env_defaults(repo_root: Path) -> Dict[str, str]:
env_values: Dict[str, str] = {}
for candidate in (repo_root / "default-environment.env", repo_root / ".env"):
if not candidate.exists():
continue
for line in candidate.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
env_values[key.strip()] = value.strip().strip("'\"")
return env_values
def resolve_rule_variables(rule: str, env_values: Dict[str, str]) -> str:
var_re = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
def replacer(match: re.Match[str]) -> str:
key = match.group(1)
if key in os.environ:
return os.environ[key]
return env_values.get(key, match.group(0))
return var_re.sub(replacer, rule)
def normalize_labels(raw_labels: Any) -> Dict[str, str]:
labels: Dict[str, str] = {}
if isinstance(raw_labels, dict):
for key, value in raw_labels.items():
labels[str(key)] = "" if value is None else str(value)
return labels
if isinstance(raw_labels, list):
for item in raw_labels:
if isinstance(item, str) and "=" in item:
key, value = item.split("=", 1)
labels[key.strip()] = value.strip()
elif isinstance(item, str):
labels[item.strip()] = ""
return labels
return labels
def infer_stack(compose_file: Path) -> str:
parts = compose_file.parts
return parts[0] if parts else "unknown"
def boolish(value: str) -> bool:
return value.strip().lower() in {"1", "true", "yes", "on"}
def parse_middlewares(raw_value: str) -> List[str]:
return [item.strip() for item in raw_value.split(",") if item.strip()]
def extract_traefik_hosts(path: Path, env_values: Dict[str, str]) -> List[Dict[str, Any]]:
try:
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except yaml.YAMLError as exc:
raise RuntimeError(f"Failed to parse compose YAML in {path}: {exc}") from exc
services = payload.get("services")
if not isinstance(services, dict):
return []
entries: List[Dict[str, Any]] = []
stack = infer_stack(path)
for service_name, service_payload in services.items():
if not isinstance(service_payload, dict):
continue
labels = normalize_labels(service_payload.get("labels"))
router_fields: Dict[str, Dict[str, str]] = defaultdict(dict)
for label_key, label_value in labels.items():
match = ROUTER_LABEL_RE.match(label_key)
if not match:
continue
router_name, field_name = match.groups()
router_fields[router_name][field_name] = label_value
for router_name, fields in router_fields.items():
rule = fields.get("rule", "")
if not rule:
continue
router_label_key = f"traefik.http.routers.{router_name}.rule"
middlewares = parse_middlewares(fields.get("middlewares", ""))
tls_options = fields.get("tls.options", "")
tls_enabled = boolish(fields.get("tls", "")) or bool(tls_options) or bool(fields.get("tls.certresolver", ""))
lowered_metadata = " ".join([tls_options, ",".join(middlewares)]).lower()
uses_mtls = "mtls" in lowered_metadata
uses_authelia = "authelia" in lowered_metadata
resolved_rule = resolve_rule_variables(rule, env_values)
for fqdn in parse_hosts_from_rule(resolved_rule):
entries.append(
{
"fqdn": fqdn,
"service": str(service_name),
"stack": stack,
"source_compose_file": str(path),
"router": router_name,
"router_label_keys": [router_label_key],
"raw_rule": rule,
"resolved_rule": resolved_rule,
"uses_tls": tls_enabled,
"tls_options": tls_options,
"middlewares": middlewares,
"uses_mtls": uses_mtls,
"uses_authelia": uses_authelia,
}
)
return entries
def load_dynu(path: Path) -> Dict[str, List[Dict[str, str]]]:
payload = json.loads(path.read_text(encoding="utf-8"))
if payload.get("base_domain") != BASE_DOMAIN:
raise RuntimeError(
f"Dynu JSON base_domain mismatch. Expected {BASE_DOMAIN}, got {payload.get('base_domain')}"
)
index: Dict[str, List[Dict[str, str]]] = defaultdict(list)
for domain in payload.get("domains", []):
for record in domain.get("records", []):
host = str(record.get("hostname", "")).strip(".").lower()
if host:
index[host].append(
{
"type": str(record.get("type", "")),
"value": str(record.get("value", "")),
"target": str(record.get("target") or ""),
"ttl": str(record.get("ttl") if record.get("ttl") is not None else ""),
}
)
for host in index:
index[host] = sorted(index[host], key=lambda x: (x["type"], x["value"], x["target"], x["ttl"]))
return index
def is_subdomain_of_base(fqdn: str) -> bool:
return fqdn.endswith(f".{BASE_DOMAIN}")
def summarize_reasons(
has_traefik: bool,
has_dns: bool,
is_allowed_unmapped: bool,
is_ambiguous: bool,
is_enforced_dns_subdomain: bool,
) -> List[str]:
reasons: List[str] = []
if has_traefik and has_dns:
reasons.append("mapped")
if has_dns and not has_traefik and is_allowed_unmapped:
reasons.append("allowed_unmapped")
if has_dns and not has_traefik and is_enforced_dns_subdomain and not is_allowed_unmapped:
reasons.append("unexpected_unmapped")
if has_dns and not has_traefik:
reasons.append("dns_only")
if has_traefik and not has_dns:
reasons.append("traefik_only")
if is_ambiguous:
reasons.append("duplicate_mapping")
reasons.append("ambiguous_mapping")
return reasons
def write_markdown(data: Dict[str, Any]) -> None:
inventory = data["inventory"]
lines = [
"# DNS Inventory (Dynu + Traefik)",
"",
"> This integration is intentionally read-only. No Dynu mutations are permitted in this repo at this stage.",
"",
f"- Base domain: `{data['base_domain']}`",
f"- Dynu fetched at: `{data['dynu_fetched_at']}`",
f"- Inventory generated at: `{data['generated_at']}`",
"",
"## Summary",
"",
f"- Traefik hostnames discovered: **{data['summary']['traefik_hostnames']}**",
f"- Dynu hostnames discovered: **{data['summary']['dynu_hostnames']}**",
f"- Mapped hostnames: **{data['summary']['mapped_hostnames']}**",
f"- DNS-only hostnames: **{data['summary']['dns_only_hostnames']}**",
f"- Traefik-only hostnames: **{data['summary']['traefik_only_hostnames']}**",
f"- Ambiguous hostnames: **{len(data['validation']['ambiguous_hostnames'])}**",
"",
"## Validation",
"",
f"- Validation ok: **{str(data['validation']['validation_ok']).lower()}**",
f"- Allowed unmapped hostnames: `{', '.join(data['validation']['allowed_unmapped_hostnames'])}`",
f"- Unexpected unmapped hostnames: **{len(data['validation']['unexpected_unmapped_hostnames'])}**",
f"- Duplicate hostnames: **{len(data['validation']['duplicate_hostnames'])}**",
f"- Ambiguous hostnames: **{len(data['validation']['ambiguous_hostnames'])}**",
"",
]
def bullet_list(title: str, values: Iterable[str]) -> None:
rows = list(values)
lines.extend([f"### {title}", ""])
if not rows:
lines.append("_None._")
else:
for value in rows:
lines.append(f"- `{value}`")
lines.append("")
bullet_list("Allowed unmapped hostnames", data["validation"]["allowed_unmapped_hostnames"])
bullet_list("Unexpected unmapped hostnames", data["validation"]["unexpected_unmapped_hostnames"])
bullet_list("Duplicate hostnames", data["validation"]["duplicate_hostnames"])
bullet_list("Ambiguous hostnames", data["validation"]["ambiguous_hostnames"])
lines.extend(
[
"## Correlation",
"",
"| Hostname | Status | Reasons | Service(s) | Route metadata | DNS records |",
"|---|---|---|---|---|---|",
]
)
for row in inventory:
services = sorted({f"{entry['stack']}/{entry['service']}" for entry in row["traefik_entries"]})
service_cell = ", ".join(services) if services else "-"
reason_cell = ", ".join(row["reasons"]) if row["reasons"] else "-"
route_chunks = []
for entry in row["traefik_entries"]:
middlewares = ",".join(entry.get("middlewares", [])) or "-"
route_chunks.append(
f"{entry['router']} [tls={str(entry['uses_tls']).lower()}, mtls={str(entry['uses_mtls']).lower()}, authelia={str(entry['uses_authelia']).lower()}, tls_options={entry.get('tls_options') or '-'}, middlewares={middlewares}]"
)
route_cell = "<br>".join(route_chunks) if route_chunks else "-"
dns_cell = ", ".join(f"{item['type']}:{item['value']}" for item in row["dynu_records"]) if row["dynu_records"] else "-"
lines.append(f"| `{row['fqdn']}` | `{row['status']}` | `{reason_cell}` | {service_cell} | {route_cell} | {dns_cell} |")
OUT_MD.parent.mkdir(parents=True, exist_ok=True)
OUT_MD.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> int:
try:
require_read_only()
except ReadOnlyError as exc:
print(str(exc), file=sys.stderr)
return 2
if not DYN_DATA.exists():
print(f"Missing {DYN_DATA}. Run fetch_dynu_dns.py first.", file=sys.stderr)
return 3
dyn_payload = json.loads(DYN_DATA.read_text(encoding="utf-8"))
dynu_index = load_dynu(DYN_DATA)
repo_root = Path(__file__).resolve().parents[2]
env_values = load_env_defaults(repo_root)
hosts: List[Dict[str, Any]] = []
for cf in compose_files(repo_root):
hosts.extend(extract_traefik_hosts(cf.relative_to(repo_root), env_values))
by_fqdn: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
for entry in hosts:
if entry["fqdn"] == BASE_DOMAIN or is_subdomain_of_base(entry["fqdn"]):
by_fqdn[entry["fqdn"]].append(entry)
duplicate_hostnames = sorted(k for k, v in by_fqdn.items() if len(v) > 1)
combined_fqdns = sorted(set(by_fqdn.keys()) | set(dynu_index.keys()))
inventory = []
ambiguous_hostnames: List[str] = []
for fqdn in combined_fqdns:
traefik_entries = sorted(
by_fqdn.get(fqdn, []),
key=lambda x: (x["stack"], x["service"], x["source_compose_file"], x["router"]),
)
dns_records = dynu_index.get(fqdn, [])
is_allowed_unmapped = fqdn in ALLOWED_UNMAPPED_HOSTNAMES
has_traefik = bool(traefik_entries)
has_dns = bool(dns_records)
service_keys = {f"{item['stack']}/{item['service']}" for item in traefik_entries}
is_ambiguous = len(service_keys) > 1
if is_ambiguous:
ambiguous_hostnames.append(fqdn)
is_enforced_dns_subdomain = is_subdomain_of_base(fqdn)
if has_traefik and has_dns:
status = "mapped"
elif has_dns and is_allowed_unmapped:
status = "allowed_unmapped"
elif has_dns and not has_traefik and is_enforced_dns_subdomain:
status = "unexpected_unmapped"
elif has_dns and not has_traefik:
status = "dns_only"
else:
status = "traefik_only"
reasons = summarize_reasons(
has_traefik, has_dns, is_allowed_unmapped, is_ambiguous, is_enforced_dns_subdomain
)
inventory.append(
{
"fqdn": fqdn,
"status": status,
"reasons": reasons,
"duplicate": fqdn in duplicate_hostnames,
"traefik_entries": traefik_entries,
"dynu_records": dns_records,
}
)
subdomain_dns_hosts = sorted(host for host in dynu_index if is_subdomain_of_base(host))
unexpected_unmapped_hostnames = sorted(
host for host in subdomain_dns_hosts if host not in by_fqdn and host not in ALLOWED_UNMAPPED_HOSTNAMES
)
validation = {
"allowed_unmapped_hostnames": sorted(ALLOWED_UNMAPPED_HOSTNAMES),
"unexpected_unmapped_hostnames": unexpected_unmapped_hostnames,
"duplicate_hostnames": duplicate_hostnames,
"ambiguous_hostnames": sorted(set(ambiguous_hostnames)),
"validation_ok": len(unexpected_unmapped_hostnames) == 0,
}
dynu_rows = []
for fqdn in sorted(dynu_index.keys()):
for rec in dynu_index[fqdn]:
dynu_rows.append(
{
"hostname": fqdn,
"type": rec["type"],
"value": rec["value"],
"ttl": rec["ttl"],
}
)
output = {
"source": "dynu+traefik",
"read_only": True,
"base_domain": BASE_DOMAIN,
"dynu_fetched_at": dyn_payload.get("fetched_at"),
"generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
"summary": {
"traefik_hostnames": len(by_fqdn),
"dynu_hostnames": len(dynu_index),
"mapped_hostnames": sum(1 for x in inventory if x["status"] == "mapped"),
"dns_only_hostnames": sum(1 for x in inventory if "dns_only" in x["reasons"]),
"traefik_only_hostnames": sum(1 for x in inventory if x["status"] == "traefik_only"),
},
"validation": validation,
"inventory": inventory,
"dynu_records_table": dynu_rows,
}
OUT_JSON.parent.mkdir(parents=True, exist_ok=True)
OUT_JSON.write_text(json.dumps(output, indent=2, sort_keys=True) + "\n", encoding="utf-8")
write_markdown(output)
print(f"Wrote {OUT_JSON}")
print(f"Wrote {OUT_MD}")
if os.environ.get("DYNU_ENFORCE_VALIDATION") == "true" and not validation["validation_ok"]:
print(
"Validation failed: unexpected unmapped hostnames were found: "
+ ", ".join(validation["unexpected_unmapped_hostnames"]),
file=sys.stderr,
)
return 4
return 0
if __name__ == "__main__":
raise SystemExit(main())

Some files were not shown because too many files have changed in this diff Show More