Compare commits

..

64 Commits

Author SHA1 Message Date
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
86 changed files with 8580 additions and 39 deletions
+64
View File
@@ -0,0 +1,64 @@
name: Generate Docs
on:
workflow_dispatch:
schedule:
- cron: "0 */6 * * *"
jobs:
generate:
runs-on: docker-server
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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
+22
View File
@@ -0,0 +1,22 @@
name: Validate Docs (Gitea)
on:
push:
branches: [ main ]
jobs:
validate:
runs-on: docker-server
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 #!**/Dockerfile
#!docker-compose.yml #!docker-compose.yml
**/data/ **/data/
apps/gitea/runner-data/
**/db/ **/db/
**/database/ **/database/
apps/nextcloud/config/ apps/nextcloud/config/
@@ -28,3 +29,25 @@ secrets/*
!.env.example !.env.example
core/traefik/certs/* core/traefik/certs/*
!core/traefik/certs/.gitkeep !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
+47
View File
@@ -18,6 +18,7 @@ If you only read one section, read **[Source-of-truth boundaries](docs/source-of
- Terraform workflows (brownfield import/reconciliation): [docs/terraform-workflows.md](docs/terraform-workflows.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) - 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) - 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) - Ansible bootstrap workflows: [docs/ansible-workflows.md](docs/ansible-workflows.md)
- Deployment prerequisites and secrets setup: [docs/deployment-prerequisites.md](docs/deployment-prerequisites.md) - Deployment prerequisites and secrets setup: [docs/deployment-prerequisites.md](docs/deployment-prerequisites.md)
- Secrets inventory: [docs/security-secrets.md](docs/security-secrets.md) - Secrets inventory: [docs/security-secrets.md](docs/security-secrets.md)
@@ -119,6 +120,52 @@ flowchart TB
For request-flow and network detail, see [docs/architecture.md](docs/architecture.md). 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 ## Codex setup and maintenance scripts
+1 -1
View File
@@ -15,7 +15,7 @@ GITEA_DB_TYPE=sqlite3
GITEA_ROOT_URL=https://gitea.lan.ddnsgeek.com/ GITEA_ROOT_URL=https://gitea.lan.ddnsgeek.com/
# Generate a token in Gitea: Site Administration → Actions → Runners # Generate a token in Gitea: Site Administration → Actions → Runners
# or Repo → Settings → Actions → Runners # or Repo → Settings → Actions → Runners
GITEA_RUNNER_REGISTRATION_TOKEN= GITEA_RUNNER_REGISTRATION_TOKEN=vYDNxzMvayREkXoaAR3x3UREkxQB2PU4eORzmkZ9
GITEA_RUNNER_NAME=docker-runner-01 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 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
+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.
+1
View File
@@ -88,6 +88,7 @@ Use architecture docs together with:
- [docs/source-of-truth.md](source-of-truth.md) - [docs/source-of-truth.md](source-of-truth.md)
- [docs/terraform-workflows.md](terraform-workflows.md) - [docs/terraform-workflows.md](terraform-workflows.md)
- [docs/infrastructure-inventory.md](infrastructure-inventory.md) - [docs/infrastructure-inventory.md](infrastructure-inventory.md)
- [docs/generated/host-topology.md](generated/host-topology.md)
## Notes on runtime vs declared state ## 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.
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 |
|---|---|
+8 -10
View File
@@ -3,15 +3,15 @@
> This integration is intentionally read-only. No Dynu mutations are permitted in this repo at this stage. > This integration is intentionally read-only. No Dynu mutations are permitted in this repo at this stage.
- Base domain: `lan.ddnsgeek.com` - Base domain: `lan.ddnsgeek.com`
- Dynu fetched at: `2026-04-21T03:55:09+00:00` - Dynu fetched at: `2026-04-21T04:18:38+00:00`
- Inventory generated at: `2026-04-21T04:08:43+00:00` - Inventory generated at: `2026-04-21T04:18:39+00:00`
## Summary ## Summary
- Traefik hostnames discovered: **15** - Traefik hostnames discovered: **17**
- Dynu hostnames discovered: **20** - Dynu hostnames discovered: **20**
- Mapped hostnames: **15** - Mapped hostnames: **17**
- DNS-only hostnames: **5** - DNS-only hostnames: **3**
- Traefik-only hostnames: **0** - Traefik-only hostnames: **0**
- Ambiguous hostnames: **0** - Ambiguous hostnames: **0**
@@ -19,7 +19,7 @@
- Validation ok: **false** - Validation ok: **false**
- Allowed unmapped hostnames: `edge.lan.ddnsgeek.com` - Allowed unmapped hostnames: `edge.lan.ddnsgeek.com`
- Unexpected unmapped hostnames: **3** - Unexpected unmapped hostnames: **1**
- Duplicate hostnames: **1** - Duplicate hostnames: **1**
- Ambiguous hostnames: **0** - Ambiguous hostnames: **0**
@@ -30,8 +30,6 @@
### Unexpected unmapped hostnames ### Unexpected unmapped hostnames
- `kuma.lan.ddnsgeek.com` - `kuma.lan.ddnsgeek.com`
- `shifts.lan.ddnsgeek.com`
- `stockfill.lan.ddnsgeek.com`
### Duplicate hostnames ### Duplicate hostnames
@@ -62,6 +60,6 @@ _None._
| `portainer.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/portainer | portainer [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, 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: | | `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: | | `searxng.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/searxng-webapp | searxng [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
| `shifts.lan.ddnsgeek.com` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | 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` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | 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: | | `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/
```
+7
View File
@@ -50,6 +50,12 @@ Compose files define intended service runtime composition, networking, labels, a
Dynu write operations are intentionally blocked in this repository stage. 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 ## Output shaping expectations
When adding Terraform outputs for documentation/tooling: When adding Terraform outputs for documentation/tooling:
@@ -61,6 +67,7 @@ When adding Terraform outputs for documentation/tooling:
## Limitations today ## 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. - No full generated inventory document pipeline is present yet.
- Some Terraform files still include generated boilerplate comments requiring ongoing cleanup. - Some Terraform files still include generated boilerplate comments requiring ongoing cleanup.
- Ansible is currently a bootstrap inventory/configuration layer and is not authoritative for full operations yet. - Ansible is currently a bootstrap inventory/configuration layer and is not authoritative for full operations yet.
+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 | |
+1 -1
View File
@@ -33,7 +33,7 @@ This page explains where to find authoritative files quickly.
## Fast path for future Codex runs ## 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). 2. Read [docs/source-of-truth.md](source-of-truth.md).
3. Read [docs/docker-environment.md](docker-environment.md). 3. Read [docs/docker-environment.md](docker-environment.md).
4. Read [docs/terraform-workflows.md](terraform-workflows.md). 4. Read [docs/terraform-workflows.md](terraform-workflows.md).
+3 -3
View File
@@ -4,11 +4,11 @@
This page explains how secret material is organized in this repository and where to find both human-readable and machine-readable references. 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 ## 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` - Runtime-loaded secret env file (local, non-committed): `../secrets/stack-secrets.env`
- Dynu DNS inventory env file (local, non-committed): `../secrets/dynu.env` - Dynu DNS inventory env file (local, non-committed): `../secrets/dynu.env`
- Docker secret files (local, non-committed): `../secrets/*.txt` - Docker secret files (local, non-committed): `../secrets/*.txt`
@@ -30,7 +30,7 @@ Treat the example template as the canonical shape for expected environment varia
## Machine-readable inventory ## 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 - Human guidance source: this page
Automation should parse `secrets/inventory.json` directly rather than scraping Markdown tables. Automation should parse `secrets/inventory.json` directly rather than scraping Markdown tables.
+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.
+4
View File
@@ -30,6 +30,10 @@ Use Terraform when documenting/reconciling existing:
Do **not** treat Terraform as a full replacement for Compose operations in this repo. 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 ### Ansible bootstrap decisions
+16 -1
View File
@@ -43,6 +43,21 @@ Use for existing Proxmox VMs and metadata reconciliation.
5. Keep lifecycle ignore rules narrow and explicit. 5. Keep lifecycle ignore rules narrow and explicit.
6. Iterate per VM until plan stabilizes. 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 workflow
Physical host metadata currently lives in Proxmox Terraform locals/outputs and is used as documentation inventory context. 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/source-of-truth.md](source-of-truth.md)
- [docs/infrastructure-inventory.md](infrastructure-inventory.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)
+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. - Physical host metadata represented in Terraform locals/outputs.
- Select Docker container mirror resources for documentation-oriented tracking. - Select Docker container mirror resources for documentation-oriented tracking.
- Outputs that can support documentation and later downstream tooling. - Outputs that can support documentation and later downstream tooling.
- Dynu DNS domain/record import and documentation inventory.
## What Terraform is not used for (today) ## What Terraform is not used for (today)
- Replacing `services-up.sh` / Compose for day-to-day app runtime orchestration. - Replacing `services-up.sh` / Compose for day-to-day app runtime orchestration.
- Broad, immediate greenfield provisioning of the whole stack. - Broad, immediate greenfield provisioning of the whole stack.
- Casual `apply` operations across all infrastructure. - Casual `apply` operations across all infrastructure.
- Replacing Dynu as DNS authority.
- Blindly recreating production DNS records without import/reconciliation.
## Directory map ## Directory map
- `proxmox/` — imported/reconciled VM resources and host metadata outputs. - `proxmox/` — imported/reconciled VM resources and host metadata outputs.
- `docker/` — selective Docker container import/mirror resources. - `docker/` — selective Docker container import/mirror resources.
- `dynu/` — Dynu DNS brownfield import/reconciliation and DNS documentation outputs.
- `bootstrap/` — backend/provider bootstrap scaffolding. - `bootstrap/` — backend/provider bootstrap scaffolding.
- `modules/` — placeholder module directories for future stable abstractions. - `modules/` — placeholder module directories for future stable abstractions.
- `scripts/reconcile_from_plan.sh` — helper to convert generated plan config into reviewable draft files. - `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 value = local.physical_hosts
} }
output "virtual_hosts" {
description = "Virtual host/VM inventory used for documentation"
value = local.virtual_hosts
}
output "infrastructure_inventory" { output "infrastructure_inventory" {
description = "Combined infrastructure inventory" description = "Combined infrastructure inventory"
value = { value = {
physical_hosts = local.physical_hosts physical_hosts = local.physical_hosts
virtual_hosts = local.virtual_hosts
} }
} }
+55
View File
@@ -21,4 +21,59 @@ locals {
notes = "Raspberry Pi host" 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":"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-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-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":""}
+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>
"""
)
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
mkdir -p docs/generated docs/diagrams docs/public
scripts/docs/generate-all.sh
if [ ! -d docs/public ] || [ -z "$(find docs/public -mindepth 1 -print -quit)" ]; then
echo "ERROR: docs/public is missing or empty after generation." >&2
exit 1
fi
echo "Public docs generated in docs/public. Review changes before commit."
+185 -9
View File
@@ -26,6 +26,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--diagrams-dir", default="docs/diagrams", help="Diagram output directory.") parser.add_argument("--diagrams-dir", default="docs/diagrams", help="Diagram output directory.")
parser.add_argument("--readme-file", default="README.md", help="README path for regeneration notes.") parser.add_argument("--readme-file", default="README.md", help="README path for regeneration notes.")
parser.add_argument("--architecture-file", default="docs/architecture.md", help="Architecture markdown path.") parser.add_argument("--architecture-file", default="docs/architecture.md", help="Architecture markdown path.")
parser.add_argument(
"--dynu-dns-inventory-file",
default="infrastructure/terraform/dynu/generated/dynu_dns_records_inventory.json",
help="Path to Dynu DNS brownfield inventory JSON.",
)
parser.add_argument("--skip-dynu-dns", action="store_true", help="Skip Dynu DNS inventory loading/rendering.")
parser.add_argument("--network-file", default="docs/network.md", help="Network markdown path.") parser.add_argument("--network-file", default="docs/network.md", help="Network markdown path.")
parser.add_argument("--coverage-file", default="docs/monitoring-coverage.md", help="Coverage markdown path.") parser.add_argument("--coverage-file", default="docs/monitoring-coverage.md", help="Coverage markdown path.")
parser.add_argument("--dry-run", action="store_true", help="Print changes instead of writing files.") parser.add_argument("--dry-run", action="store_true", help="Print changes instead of writing files.")
@@ -41,6 +47,109 @@ def load_json(path: Path) -> dict[str, Any]:
return data return data
def load_optional_json(path: Path) -> Any | None:
if not path.exists():
return None
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def normalize_dynu_dns_records(payload: Any) -> list[dict[str, Any]]:
records_payload = payload
if isinstance(payload, dict):
if isinstance(payload.get("records"), list):
records_payload = payload["records"]
elif isinstance(payload.get("value"), list):
records_payload = payload["value"]
if not isinstance(records_payload, list):
raise ValueError("Dynu DNS inventory must be a list or object with list field `records`/`value`.")
normalized: list[dict[str, Any]] = []
for record in records_payload:
if not isinstance(record, dict):
continue
normalized.append(
{
"id": record.get("id"),
"domain_id": record.get("domain_id"),
"domain_name": str(record.get("domain_name") or "unknown"),
"hostname": str(record.get("hostname") or "unknown"),
"node_name": str(record.get("node_name") or "unknown"),
"record_type": str(record.get("record_type") or "unknown"),
"content": record.get("content"),
"state": record.get("state") if "state" in record else record.get("enabled"),
"ttl": record.get("ttl"),
"updated_on": record.get("updated_on"),
}
)
normalized.sort(
key=lambda r: (
str(r.get("domain_name") or ""),
str(r.get("hostname") or ""),
str(r.get("record_type") or ""),
str(r.get("id") or ""),
)
)
return normalized
def render_dynu_dns_architecture_section(records: list[dict[str, Any]], inventory_path: Path | None) -> str:
domains = sorted({r["domain_name"] for r in records if r["domain_name"] != "unknown"})
dynamic_count = 0
static_count = 0
disabled_count = 0
rows: list[list[str]] = []
for record in records:
rtype = str(record.get("record_type") or "").upper()
raw_content = record.get("content")
content = "" if raw_content is None else str(raw_content).strip()
is_dynamic = rtype in {"A", "AAAA"} and not content
mode = "dynamic" if is_dynamic else ("static" if content else ("record" if rtype not in {"A", "AAAA"} else "static"))
target = "dynamic" if is_dynamic else (content or ("unknown" if not content else content))
if is_dynamic:
dynamic_count += 1
else:
static_count += 1
enabled_value = record.get("state")
enabled_text = "unknown" if enabled_value is None else str(bool(enabled_value)).lower()
if enabled_value is False:
disabled_count += 1
rows.append(
[
str(record.get("hostname") or "unknown"),
rtype or "unknown",
target,
mode,
str(record.get("ttl") if record.get("ttl") is not None else "unknown"),
enabled_text,
str(record.get("id") if record.get("id") is not None else "unknown"),
str(record.get("updated_on") or "unknown"),
]
)
domain_text = ", ".join(domains) if domains else "unknown"
lines = [
"### Dynu DNS brownfield inventory",
"",
"Dynu DNS is managed as a brownfield reconciliation source. Terraform imports the root domain and individual DNS records into state, while generated configuration provides reviewable management intent.",
"",
f"- Inventory source: `{inventory_path}`" if inventory_path else "- Inventory source: `unknown`",
f"- Records observed: `{len(records)}`",
f"- Domains observed: `{domain_text}`",
f"- Dynamic A/AAAA records: `{dynamic_count}`",
f"- Static records: `{static_count}`",
f"- Disabled records: `{disabled_count}`",
"",
"#### DNS records",
"",
markdown_table(["hostname", "type", "target/content", "mode", "ttl", "enabled", "record id", "updated"], rows or [["none", "", "", "", "", "", "", ""]]),
"",
]
return "\n".join(lines)
def merged_labels(target: dict[str, Any]) -> dict[str, str]: def merged_labels(target: dict[str, Any]) -> dict[str, str]:
discovered = target.get("discovered_labels") or {} discovered = target.get("discovered_labels") or {}
labels = target.get("labels") or {} labels = target.get("labels") or {}
@@ -244,12 +353,18 @@ def render_network_doc(inventory: dict[str, Any], targets: list[dict[str, Any]])
return "\n".join(lines) return "\n".join(lines)
def render_architecture_section(inventory: dict[str, Any], targets: list[dict[str, Any]]) -> str: def render_architecture_section(
inventory: dict[str, Any],
targets: list[dict[str, Any]],
dynu_records: list[dict[str, Any]] | None = None,
dynu_inventory_path: Path | None = None,
dynu_inventory_missing: bool = False,
) -> str:
summaries = summarize_targets(targets, normalize_targets({"targets": inventory.get("unhealthy_targets") or []})) summaries = summarize_targets(targets, normalize_targets({"targets": inventory.get("unhealthy_targets") or []}))
notes = inventory.get("notes") or [] notes = inventory.get("notes") or []
lines = [ lines = [
"## Runtime visibility from Prometheus", "## Runtime and infrastructure inventory",
"", "",
GENERATED_BEGIN, GENERATED_BEGIN,
"", "",
@@ -267,12 +382,42 @@ def render_architecture_section(inventory: dict[str, Any], targets: list[dict[st
[[job, str(data["active"]), str(data["unhealthy"])] for job, data in summaries["by_job"].items()] or [["none", "0", "0"]], [[job, str(data["active"]), str(data["unhealthy"])] for job, data in summaries["by_job"].items()] or [["none", "0", "0"]],
), ),
"", "",
"### Data sources",
"",
"- `docs/runtime/prometheus-inventory.json` (normalized runtime export)",
"- Prometheus scrape metadata (`targets` + label sets)",
"- Existing repository architecture docs for declared topology",
] ]
if dynu_records is not None:
lines.extend(["", render_dynu_dns_architecture_section(dynu_records, dynu_inventory_path).rstrip(), ""])
elif dynu_inventory_missing:
lines.extend(
[
"### Dynu DNS brownfield inventory",
"",
f"Dynu DNS inventory was not found at `{dynu_inventory_path}`.",
"",
"Generate it with:",
"",
"```bash",
"cd infrastructure/terraform/dynu",
"python3 scripts/generate-brownfield-records.py --overwrite",
"```",
"",
]
)
lines.extend(
[
"### Data sources",
"",
"- `docs/runtime/prometheus-inventory.json` (normalized runtime export)",
]
)
if dynu_records is not None:
lines.append(f"- `{dynu_inventory_path}` (Dynu DNS brownfield inventory)")
elif dynu_inventory_missing:
lines.append("- Dynu DNS inventory not available; run the Dynu brownfield generator.")
lines.extend(
[
"- Prometheus scrape metadata (`targets` + label sets)",
"- Existing repository architecture docs for declared topology",
]
)
if notes: if notes:
lines.extend(["", "### Notes from inventory", ""]) lines.extend(["", "### Notes from inventory", ""])
for note in notes: for note in notes:
@@ -284,14 +429,26 @@ def render_architecture_section(inventory: dict[str, Any], targets: list[dict[st
def upsert_generated_section(path: Path, section_markdown: str, dry_run: bool, verbose: bool) -> None: def upsert_generated_section(path: Path, section_markdown: str, dry_run: bool, verbose: bool) -> None:
existing = path.read_text(encoding="utf-8") if path.exists() else "" existing = path.read_text(encoding="utf-8") if path.exists() else ""
section_body = section_markdown section_body = section_markdown
legacy_heading = "## Runtime visibility from Prometheus"
new_heading = "## Runtime and infrastructure inventory"
if GENERATED_BEGIN in existing and GENERATED_END in existing: if GENERATED_BEGIN in existing and GENERATED_END in existing:
# Migration: replace legacy heading that exists outside the generated markers.
existing = re.sub(
rf"^{re.escape(legacy_heading)}(?=\n)",
new_heading,
existing,
count=1,
flags=re.MULTILINE,
)
pattern = re.compile( pattern = re.compile(
rf"{re.escape(GENERATED_BEGIN)}.*?{re.escape(GENERATED_END)}", rf"{re.escape(GENERATED_BEGIN)}.*?{re.escape(GENERATED_END)}",
re.DOTALL, re.DOTALL,
) )
replacement = "\n".join( replacement = "\n".join(
line for line in section_body.splitlines() if line.strip() not in {"## Runtime visibility from Prometheus"} line
for line in section_body.splitlines()
if line.strip() not in {legacy_heading, new_heading}
) )
updated = pattern.sub(replacement.strip(), existing) updated = pattern.sub(replacement.strip(), existing)
else: else:
@@ -384,6 +541,7 @@ def main() -> int:
inventory_path = Path(args.inventory_file) inventory_path = Path(args.inventory_file)
docs_dir = Path(args.docs_dir) docs_dir = Path(args.docs_dir)
diagrams_dir = Path(args.diagrams_dir) diagrams_dir = Path(args.diagrams_dir)
dynu_inventory_path = Path(args.dynu_dns_inventory_file)
inventory = load_json(inventory_path) inventory = load_json(inventory_path)
targets = normalize_targets(inventory) targets = normalize_targets(inventory)
@@ -394,7 +552,25 @@ def main() -> int:
coverage_md = render_monitoring_coverage(inventory, targets) coverage_md = render_monitoring_coverage(inventory, targets)
network_md = render_network_doc(inventory, targets) network_md = render_network_doc(inventory, targets)
architecture_section = render_architecture_section(inventory, targets) dynu_records: list[dict[str, Any]] | None = None
dynu_inventory_missing = False
if not args.skip_dynu_dns:
try:
dynu_payload = load_optional_json(dynu_inventory_path)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON in Dynu DNS inventory file {dynu_inventory_path}: {exc}") from exc
if dynu_payload is None:
dynu_inventory_missing = True
else:
dynu_records = normalize_dynu_dns_records(dynu_payload)
architecture_section = render_architecture_section(
inventory,
targets,
dynu_records=dynu_records,
dynu_inventory_path=dynu_inventory_path,
dynu_inventory_missing=dynu_inventory_missing,
)
monitoring_mmd = render_monitoring_mermaid(targets) monitoring_mmd = render_monitoring_mermaid(targets)
architecture_mmd = render_architecture_mermaid(targets) architecture_mmd = render_architecture_mermaid(targets)
+13 -13
View File
@@ -1,13 +1,13 @@
10:17:35 INFO: === Update started: 2026-04-21 10:17:35 === 12:50:25 INFO: === Update started: 2026-05-12 12:50:25 ===
10:17:35 WARNING: Skipping traefik (directory does not exist) 12:50:25 WARNING: Skipping traefik (directory does not exist)
10:17:35 WARNING: Skipping nextcloud (directory does not exist) 12:50:25 WARNING: Skipping nextcloud (directory does not exist)
10:17:35 WARNING: Skipping passbolt (directory does not exist) 12:50:25 WARNING: Skipping passbolt (directory does not exist)
10:17:35 WARNING: Skipping searxng (directory does not exist) 12:50:25 WARNING: Skipping searxng (directory does not exist)
10:17:35 WARNING: Skipping gitea (directory does not exist) 12:50:25 WARNING: Skipping gitea (directory does not exist)
10:17:35 WARNING: Skipping gotify (directory does not exist) 12:50:25 WARNING: Skipping gotify (directory does not exist)
10:17:35 WARNING: Skipping grafana (directory does not exist) 12:50:25 WARNING: Skipping grafana (directory does not exist)
10:17:35 WARNING: Skipping gramps (directory does not exist) 12:50:25 WARNING: Skipping gramps (directory does not exist)
10:17:35 WARNING: Skipping portainer (directory does not exist) 12:50:25 WARNING: Skipping portainer (directory does not exist)
10:17:35 WARNING: Skipping prometheus (directory does not exist) 12:50:25 WARNING: Skipping prometheus (directory does not exist)
10:17:35 WARNING: Skipping uptime-kuma (directory does not exist) 12:50:25 WARNING: Skipping uptime-kuma (directory does not exist)
10:17:35 INFO: Pruning unused containers, images, networks, and volumes... 12:50:25 INFO: Pruning unused containers, images, networks, and volumes...