Repository: ChristianLempa/boilerplates Branch: main Commit: c7e46f7762f8 Files: 246 Total size: 894.6 KB Directory structure: gitextract_wuyffx3g/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── issue_template.md │ ├── pull_request_template.md │ ├── scripts/ │ │ ├── generate_wiki_docs.py │ │ └── sync-template-version.sh │ └── workflows/ │ ├── codequality-ruff.yaml │ ├── codequality-yamllint.yaml │ ├── docs-update-wiki.yaml │ ├── release-create-cli-release.yaml │ └── renovate-sync-versions.yaml ├── .gitignore ├── .wiki/ │ ├── Core-Concepts-Defaults.md │ ├── Core-Concepts-Libraries.md │ ├── Core-Concepts-Templates.md │ ├── Core-Concepts-Variables.md │ ├── Getting-Started.md │ ├── Home.md │ ├── Installation.md │ ├── Variables-Ansible.md │ ├── Variables-Compose.md │ ├── Variables-Helm.md │ ├── Variables-Kubernetes.md │ ├── Variables-Packer.md │ ├── Variables-Terraform.md │ ├── Variables.md │ └── _Sidebar.md ├── .yamllint ├── AGENTS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── cli/ │ ├── __init__.py │ ├── __main__.py │ ├── core/ │ │ ├── config/ │ │ │ ├── __init__.py │ │ │ └── config_manager.py │ │ ├── display/ │ │ │ ├── __init__.py │ │ │ ├── display_base.py │ │ │ ├── display_icons.py │ │ │ ├── display_settings.py │ │ │ ├── display_status.py │ │ │ ├── display_table.py │ │ │ ├── display_template.py │ │ │ └── display_variable.py │ │ ├── exceptions.py │ │ ├── input/ │ │ │ ├── __init__.py │ │ │ ├── input_manager.py │ │ │ ├── input_settings.py │ │ │ └── prompt_manager.py │ │ ├── library.py │ │ ├── module/ │ │ │ ├── __init__.py │ │ │ ├── base_commands.py │ │ │ ├── base_module.py │ │ │ ├── config_commands.py │ │ │ └── helpers.py │ │ ├── prompt.py │ │ ├── registry.py │ │ ├── repo.py │ │ ├── template/ │ │ │ ├── __init__.py │ │ │ ├── template.py │ │ │ ├── variable.py │ │ │ ├── variable_collection.py │ │ │ └── variable_section.py │ │ ├── validators.py │ │ └── version.py │ └── modules/ │ ├── __init__.py │ ├── ansible/ │ │ └── __init__.py │ ├── compose/ │ │ ├── __init__.py │ │ └── validate.py │ ├── helm/ │ │ └── __init__.py │ ├── kubernetes/ │ │ └── __init__.py │ ├── packer/ │ │ └── __init__.py │ └── terraform/ │ └── __init__.py ├── flake.nix ├── library/ │ ├── ansible/ │ │ ├── checkmk-install-agent/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── checkmk-manage-host/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── docker-certs/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── docker-certs-enable/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── docker-install-ubuntu/ │ │ │ ├── main.yml.j2 │ │ │ └── template.yaml │ │ ├── docker-prune/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── ubuntu-add-sshkey/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ ├── ubuntu-apt-update/ │ │ │ ├── playbook.yaml.j2 │ │ │ └── template.yaml │ │ └── ubuntu-vm-core/ │ │ ├── playbook.yaml.j2 │ │ └── template.yaml │ ├── compose/ │ │ ├── adguardhome/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── alloy/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ ├── common.alloy.j2 │ │ │ │ ├── logs_docker.alloy.j2 │ │ │ │ ├── logs_system.alloy.j2 │ │ │ │ ├── metrics_docker.alloy.j2 │ │ │ │ ├── metrics_system.alloy.j2 │ │ │ │ └── targets.alloy.j2 │ │ │ └── template.yaml │ │ ├── authentik/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── bind9/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ ├── named.conf.j2 │ │ │ │ ├── named.conf.zones.j2 │ │ │ │ └── tsig.key.j2 │ │ │ └── template.yaml │ │ ├── checkmk/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── dockge/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── gitea/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── gitlab/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ └── gitlab.rb.j2 │ │ │ └── template.yaml │ │ ├── gitlab-runner/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ └── config.toml │ │ │ └── template.yaml │ │ ├── grafana/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── homeassistant/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── homepage/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── homer/ │ │ │ ├── assets/ │ │ │ │ └── config.yml.j2 │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── influxdb/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── komodo/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── loki/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ └── config.yaml.j2 │ │ │ └── template.yaml │ │ ├── mariadb/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── n8n/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── netbox/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── nextcloud/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── nginx/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ └── default.conf │ │ │ ├── data/ │ │ │ │ └── index.html │ │ │ └── template.yaml │ │ ├── openwebui/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── passbolt/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── pihole/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── portainer/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── postgres/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── compose.yaml.j2.final │ │ │ └── template.yaml │ │ ├── prometheus/ │ │ │ ├── compose.yaml.j2 │ │ │ ├── config/ │ │ │ │ └── prometheus.yaml │ │ │ └── template.yaml │ │ ├── renovate/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── semaphoreui/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── traefik/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── twingate-connector/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ ├── uptimekuma/ │ │ │ ├── compose.yaml.j2 │ │ │ └── template.yaml │ │ └── whoami/ │ │ ├── compose.yaml.j2 │ │ └── template.yaml │ ├── helm/ │ │ ├── authentik/ │ │ │ ├── secrets.yaml.j2 │ │ │ ├── template.yaml │ │ │ └── values.yaml.j2 │ │ ├── certmanager/ │ │ │ ├── template.yaml │ │ │ └── values.yaml.j2 │ │ ├── longhorn/ │ │ │ ├── template.yaml │ │ │ └── values.yaml.j2 │ │ ├── netbox/ │ │ │ ├── template.yaml │ │ │ └── values.yaml.j2 │ │ ├── portainer/ │ │ │ ├── template.yaml │ │ │ └── values.yaml.j2 │ │ └── traefik/ │ │ ├── template.yaml │ │ └── values.yaml.j2 │ ├── kubernetes/ │ │ ├── certmanager-certificate/ │ │ │ ├── certificate.yaml.j2 │ │ │ └── template.yaml │ │ ├── certmanager-clusterissuer/ │ │ │ ├── clusterissuer.yaml.j2 │ │ │ └── template.yaml │ │ ├── certmanager-issuer/ │ │ │ ├── issuer.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-configmap/ │ │ │ ├── configmap.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-ingress/ │ │ │ ├── ingress.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-ingressclass/ │ │ │ ├── ingressclass.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-persistentvolume/ │ │ │ ├── pv.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-persistentvolumeclaim/ │ │ │ ├── pvc.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-secret/ │ │ │ └── template.yaml │ │ ├── core-service/ │ │ │ ├── service.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-serviceaccount/ │ │ │ ├── serviceaccount.yaml.j2 │ │ │ └── template.yaml │ │ ├── core-storageclass/ │ │ │ ├── storageclass.yaml.j2 │ │ │ └── template.yaml │ │ ├── traefik-ingressroute/ │ │ │ ├── ingressroute.yaml.j2 │ │ │ └── template.yaml │ │ ├── traefik-ingressroutetcp/ │ │ │ ├── ingressroutetcp.yaml.j2 │ │ │ └── template.yaml │ │ ├── traefik-middleware/ │ │ │ ├── middleware.yaml.j2 │ │ │ └── template.yaml │ │ └── twingate-connector/ │ │ ├── connector.yaml.j2 │ │ └── template.yaml │ ├── packer/ │ │ └── proxmox-iso-ubuntu/ │ │ ├── files/ │ │ │ └── 99-pve.cfg │ │ ├── http/ │ │ │ ├── meta-data │ │ │ └── user-data.j2 │ │ ├── proxmox-iso-ubuntu.pkr.hcl.j2 │ │ ├── template.yaml │ │ └── variables.pkrvars.hcl.example │ └── terraform/ │ ├── cloudflare-dns-record/ │ │ ├── cloudflare_dns_record.tf.j2 │ │ ├── cloudflare_zone.tf.j2 │ │ └── template.yaml │ ├── cloudflare-ztna-application/ │ │ ├── cloudflare_account_zone.tf.j2 │ │ ├── cloudflare_zero_trust_access_application.tf.j2 │ │ ├── cloudflare_zero_trust_access_policy.tf.j2 │ │ └── template.yaml │ ├── dns-a-record/ │ │ ├── dns_a_record_set.tf.j2 │ │ └── template.yaml │ └── netbox-vm/ │ ├── netbox_virtual_machine.tf.j2 │ └── template.yaml ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── scripts/ │ └── install.sh └── tests/ ├── __init__.py ├── test_variable.py └── test_version.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.json] indent_size = 2 [*.{js,jsx,ts,tsx}] indent_size = 2 [*.md] indent_size = unset trim_trailing_whitespace = false [*.py] indent_size = 4 [{*.{yaml,yml},.yamllint}] indent_size = 2 ================================================ FILE: .github/FUNDING.yml ================================================ --- # These are supported funding model platforms patreon: christianlempa ================================================ FILE: .github/issue_template.md ================================================ ### Issue Reporting *Please write all text in English in order to facilitate communication and collaboration. Thank you!* #### Description [Provide a clear and concise description of the issue] #### Steps to Reproduce 1. [First step] 2. [Second step] 3. [Any subsequent steps] #### Expected Behavior [Describe what you expected to happen] #### Actual Behavior [Describe what actually happened] #### Screenshots [If applicable, add screenshots to help explain the issue] #### Additional Information [Any additional information or context that can be helpful in resolving the issue] #### Environment [Include as many relevant details about the environment you experienced the bug in.] ================================================ FILE: .github/pull_request_template.md ================================================ ### Pull Request *Please write all text in English in order to facilitate communication and collaboration. Thank you!* --- ================================================ FILE: .github/scripts/generate_wiki_docs.py ================================================ #!/usr/bin/env python3 """Generate GitHub Wiki documentation for module variables. This script auto-generates variable documentation in GitHub Wiki markdown format for all registered modules, using the latest schema version for each. """ import sys from pathlib import Path # Add project root to path (script is in .github/scripts, so go up twice) project_root = Path(__file__).parent.parent.parent sys.path.insert(0, str(project_root)) # ruff: noqa: E402 # Import all modules to register them import cli.modules.ansible import cli.modules.compose import cli.modules.helm import cli.modules.kubernetes import cli.modules.packer import cli.modules.terraform # noqa: F401 from cli.core.registry import registry # Module import after path manipulation def format_value(value): """Format value for markdown display.""" if value is None or value == "": return "_none_" if isinstance(value, bool): return "✓" if value else "✗" if isinstance(value, list): return ", ".join(f"`{v}`" for v in value) return f"`{value}`" def generate_module_docs(module_name: str, output_dir: Path): # noqa: PLR0912, PLR0915 """Generate wiki documentation for a single module.""" # Get module class from registry module_classes = dict(registry.iter_module_classes()) if module_name not in module_classes: sys.stderr.write(f"Warning: Module '{module_name}' not found, skipping\n") return False module_cls = module_classes[module_name] schema_version = module_cls.schema_version # Get the spec for the latest schema version if hasattr(module_cls, "schemas") and schema_version in module_cls.schemas: spec = module_cls.schemas[schema_version] elif hasattr(module_cls, "spec"): spec = module_cls.spec else: sys.stderr.write(f"Warning: No spec found for module '{module_name}', skipping\n") return False # Generate markdown content lines = [] # Header lines.append(f"# {module_name.title()} Variables") lines.append("") lines.append(f"**Module:** `{module_name}` ") lines.append(f"**Schema Version:** `{schema_version}` ") lines.append(f"**Description:** {module_cls.description}") lines.append("") lines.append("---") lines.append("") lines.append( "This page documents all available variables for the " + f"{module_name} module. Variables are organized into sections " + "that can be enabled/disabled based on your configuration needs." ) lines.append("") # Table of contents lines.append("## Table of Contents") lines.append("") for section_key, section_data in spec.items(): section_title = section_data.get("title", section_key) anchor = section_title.lower().replace(" ", "-").replace("/", "") lines.append(f"- [{section_title}](#{anchor})") lines.append("") lines.append("---") lines.append("") # Process each section for section_key, section_data in spec.items(): section_title = section_data.get("title", section_key) section_desc = section_data.get("description", "") section_toggle = section_data.get("toggle", "") section_needs = section_data.get("needs", "") section_required = section_data.get("required", False) section_vars = section_data.get("vars", {}) # Section header lines.append(f"## {section_title}") lines.append("") # Section metadata metadata = [] if section_required: metadata.append("**Required:** Yes") if section_toggle: metadata.append(f"**Toggle Variable:** `{section_toggle}`") if section_needs: if isinstance(section_needs, list): needs_str = ", ".join(f"`{n}`" for n in section_needs) else: needs_str = f"`{section_needs}`" metadata.append(f"**Depends On:** {needs_str}") if metadata: lines.append(" \n".join(metadata)) lines.append("") if section_desc: lines.append(section_desc) lines.append("") # Skip sections with no variables if not section_vars: lines.append("_No variables defined in this section._") lines.append("") continue # Variables table lines.append("| Variable | Type | Default | Description |") lines.append("|----------|------|---------|-------------|") for var_name, var_data in section_vars.items(): var_type = var_data.get("type", "str") var_default = format_value(var_data.get("default")) var_description = var_data.get("description", "").replace("\n", " ") # Add extra metadata to description extra_parts = [] if var_data.get("sensitive"): extra_parts.append("**Sensitive**") if var_data.get("autogenerated"): extra_parts.append("**Auto-generated**") if "options" in var_data: opts = ", ".join(f"`{o}`" for o in var_data["options"]) extra_parts.append(f"**Options:** {opts}") if "needs" in var_data: extra_parts.append(f"**Needs:** `{var_data['needs']}`") if "extra" in var_data: extra_parts.append(var_data["extra"]) if extra_parts: var_description += "
" + " • ".join(extra_parts) lines.append(f"| `{var_name}` | `{var_type}` | {var_default} | {var_description} |") lines.append("") lines.append("---") lines.append("") # Footer lines.append("## Notes") lines.append("") lines.append("- **Required sections** must be configured") lines.append("- **Toggle variables** enable/disable entire sections") lines.append("- **Dependencies** (`needs`) control when sections/variables are available") lines.append("- **Sensitive variables** are masked during prompts") lines.append("- **Auto-generated variables** are populated automatically if not provided") lines.append("") lines.append("---") lines.append("") lines.append(f"_Last updated: Schema version {schema_version}_") # Write to file output_file = output_dir / f"Variables-{module_name.title()}.md" output_file.write_text("\n".join(lines)) sys.stdout.write(f"Generated: {output_file.name}\n") return True def generate_variables_index(modules: list[str], output_dir: Path): """Generate index page for all variable documentation.""" lines = [] lines.append("# Variables Documentation") lines.append("") lines.append("This section contains auto-generated documentation for all " + "available variables in each module.") lines.append("") lines.append("## Available Modules") lines.append("") for module_name in sorted(modules): lines.append(f"- [{module_name.title()}](Variables-{module_name.title()})") lines.append("") lines.append("---") lines.append("") lines.append("Each module page includes:") lines.append("") lines.append("- Schema version information") lines.append("- Complete list of sections and variables") lines.append("- Variable types, defaults, and descriptions") lines.append("- Section dependencies and toggle configurations") lines.append("") lines.append("---") lines.append("") lines.append("_This documentation is auto-generated from module schemas._") output_file = output_dir / "Variables.md" output_file.write_text("\n".join(lines)) sys.stdout.write(f"Generated: {output_file.name}\n") # Minimum required arguments MIN_ARGS = 2 def main(): """Main entry point.""" if len(sys.argv) < MIN_ARGS: sys.stderr.write("Usage: python3 scripts/generate_wiki_docs.py \n") sys.exit(1) output_dir = Path(sys.argv[1]) output_dir.mkdir(parents=True, exist_ok=True) sys.stdout.write(f"Generating wiki documentation in: {output_dir}\n") sys.stdout.write("\n") # Get all registered modules module_classes = dict(registry.iter_module_classes()) successful_modules = [] for module_name in sorted(module_classes.keys()): if generate_module_docs(module_name, output_dir): successful_modules.append(module_name) sys.stdout.write("\n") # Generate index page if successful_modules: generate_variables_index(successful_modules, output_dir) sys.stdout.write("\n") sys.stdout.write(f"✓ Successfully generated documentation for {len(successful_modules)} module(s)\n") else: sys.stderr.write("Error: No documentation generated\n") sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: .github/scripts/sync-template-version.sh ================================================ #!/usr/bin/env bash # Sync Docker image versions to template.yaml metadata # Triggered by GitHub Actions when Renovate updates dependencies set -euo pipefail # Extract version from different file types extract_version() { local file="$1" local filename=$(basename "$file") case "$filename" in compose.yaml.j2|*.j2) # Docker Compose or K8s manifest: extract from image: line grep -E '^\s*image:\s*[^{]*:[^{}\s]+' "$file" | head -n1 | sed -E 's/.*:([^:]+)$/\1/' | tr -d ' ' || true ;; values.yaml|values.yml) # Helm values: extract from repository + tag grep -A1 'repository:' "$file" | grep 'tag:' | sed -E 's/.*tag:\s*['\''"]?([^'\''"]+)['\''"]?/\1/' | tr -d ' ' || true ;; esac } # Update template.yaml if version differs update_template() { local template_file="$1" local new_version="$2" local current_date=$(date +%Y-%m-%d) local current_version=$(grep -E '^\s*version:\s*' "$template_file" | sed -E 's/.*version:\s*['\''"]?([^'\''"]+)['\''"]?/\1/' | tr -d ' ' || true) if [ -n "$current_version" ] && [ "$new_version" != "$current_version" ]; then echo "✓ Updating $template_file: $current_version → $new_version (date: $current_date)" sed -i "s/version: .*/version: $new_version/" "$template_file" sed -i "s/date: .*/date: '$current_date'/" "$template_file" return 0 fi return 1 } # Main processing updated=0 files=("${@:-$(find library -type f \( -name 'compose.yaml.j2' -o -name 'values.yaml' -o -name 'values.yml' \) 2>/dev/null)}") for file in "${files[@]}"; do [ ! -f "$file" ] && continue template_file="$(dirname "$file")/template.yaml" [ ! -f "$template_file" ] && continue version=$(extract_version "$file") [ -z "$version" ] || [[ "$version" =~ \{\{ ]] && continue update_template "$template_file" "$version" && ((updated++)) || true done echo "Processed ${#files[@]} file(s), updated $updated template(s)" exit 0 ================================================ FILE: .github/workflows/codequality-ruff.yaml ================================================ --- name: Code Quality - Ruff 'on': pull_request: branches: - main push: branches: - main - 'release/**' permissions: contents: read jobs: ruff: name: Python Linting and Formatting runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.14' - name: Install Ruff run: pip install ruff - name: Run Ruff Linting run: ruff check . - name: Run Ruff Formatting Check run: ruff format --check . ================================================ FILE: .github/workflows/codequality-yamllint.yaml ================================================ --- name: Code Quality - yamllint 'on': pull_request: branches: - main permissions: contents: read jobs: lint: name: Linters runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - run: yamllint --strict -- $(git ls-files '*.yaml' '*.yml') ================================================ FILE: .github/workflows/docs-update-wiki.yaml ================================================ --- name: Docs - Update Wiki 'on': push: branches: - main paths: - 'cli/core/schema/**/*.json' # JSON schema files - '.wiki/**' # Static wiki pages - '.github/scripts/generate_wiki_docs.py' # Wiki generation script - '.github/workflows/docs-update-wiki.yaml' # This workflow workflow_dispatch: # Allow manual trigger permissions: contents: write jobs: update-wiki: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 1 - name: Checkout wiki repository uses: actions/checkout@v6 with: repository: ${{ github.repository }}.wiki path: wiki token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.14' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e . - name: Generate variable documentation run: | python3 .github/scripts/generate_wiki_docs.py wiki/ - name: Sync wiki pages from .wiki directory run: | # Copy all markdown files from .wiki/ to wiki/ (except Variables-*.md which are auto-generated) if [ -d ".wiki" ]; then echo "Syncing wiki pages from .wiki/ directory..." for file in .wiki/*.md; do filename=$(basename "$file") # Skip auto-generated variable documentation files if [[ ! "$filename" =~ ^Variables- ]]; then echo " Copying $filename" cp "$file" "wiki/$filename" fi done else echo "No .wiki directory found, skipping static wiki pages sync" fi - name: Check for changes id: changes working-directory: wiki run: | git add . if git diff --staged --quiet; then echo "has_changes=false" >> $GITHUB_OUTPUT echo "No changes detected in wiki documentation" else echo "has_changes=true" >> $GITHUB_OUTPUT echo "Changes detected in wiki documentation" fi - name: Commit and push changes if: steps.changes.outputs.has_changes == 'true' working-directory: wiki run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "Auto-update wiki pages" # Pull with rebase to handle any remote changes, then push # GitHub wikis use master as default branch git pull --rebase origin master git push origin master - name: Summary run: | if [ "${{ steps.changes.outputs.has_changes }}" == "true" ]; then echo "Wiki variable documentation updated successfully" echo "View at: https://github.com/${{ github.repository }}/wiki" else echo "No changes to wiki documentation" fi ================================================ FILE: .github/workflows/release-create-cli-release.yaml ================================================ --- name: Release - Create CLI GitHub Release 'on': push: tags: - 'v*.*.*' # Trigger on version tags like v1.0.0, v2.1.3, etc. permissions: contents: write jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Extract version from tag id: version run: | # Remove 'v' prefix if present (e.g., v1.0.0 -> 1.0.0) VERSION="${GITHUB_REF#refs/tags/}" VERSION="${VERSION#v}" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=$GITHUB_REF_NAME" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION from tag $GITHUB_REF_NAME" - name: Validate version consistency run: | TAG_VERSION="${{ steps.version.outputs.version }}" # Extract version from pyproject.toml PYPROJECT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') # Extract version from cli/__init__.py CLI_VERSION=$(grep '^__version__ = ' cli/__init__.py | sed 's/__version__ = "\(.*\)"/\1/') echo "Tag version: $TAG_VERSION" echo "pyproject.toml: $PYPROJECT_VERSION" echo "cli/__init__.py: $CLI_VERSION" echo "" # Check if all versions match if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then echo "Error: Tag version ($TAG_VERSION) does not match pyproject.toml version ($PYPROJECT_VERSION)" echo "Please update pyproject.toml to version $TAG_VERSION before creating the release." exit 1 fi if [ "$TAG_VERSION" != "$CLI_VERSION" ]; then echo "Error: Tag version ($TAG_VERSION) does not match cli/__init__.py version ($CLI_VERSION)" echo "Please update cli/__init__.py to version $TAG_VERSION before creating the release." exit 1 fi echo "Version consistency check passed" echo "All version strings match: $TAG_VERSION" - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.14' - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build - name: Check distribution run: | echo "Built packages:" ls -lh dist/ echo "" echo "Checking package integrity:" twine check dist/* # PyPI publishing disabled for now - install via GitHub releases # - name: Publish to PyPI # if: > # ${{ !contains(steps.version.outputs.version, 'alpha') && # !contains(steps.version.outputs.version, 'beta') && # !contains(steps.version.outputs.version, 'rc') }} # env: # TWINE_USERNAME: __token__ # TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} # run: | # echo "Publishing to PyPI..." # twine upload dist/* - name: Extract changelog for this version id: changelog run: | # Extract the changelog for this version from CHANGELOG.md VERSION="${{ steps.version.outputs.version }}" # First try to extract the section for this specific version CHANGELOG=$(awk -v ver="$VERSION" '/^## \[/{if($0 ~ "\\[" ver "\\]"){flag=1; next} else if(flag){exit}} flag' CHANGELOG.md) # If empty, fall back to [Unreleased] section if [ -z "$CHANGELOG" ]; then CHANGELOG=$(awk '/^## \[Unreleased\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md) fi if [ -z "$CHANGELOG" ]; then echo "No changelog entries found for this release" CHANGELOG="See commit history for details." fi # Save to output using heredoc to handle multiline { echo 'content<> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.version.outputs.tag }} name: Release ${{ steps.version.outputs.tag }} body: | ## Boilerplates CLI ${{ steps.version.outputs.tag }} ${{ steps.changelog.outputs.content }} --- ### Installation Install using the installation script: ```bash curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash ``` Or install a specific version: ```bash curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --version ${{ steps.version.outputs.tag }} ``` draft: false prerelease: > ${{ contains(steps.version.outputs.version, 'alpha') || contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'rc') }} files: | dist/*.whl dist/*.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/renovate-sync-versions.yaml ================================================ --- name: Renovate - Sync Template Versions 'on': pull_request: branches: - main paths: - 'library/**' permissions: contents: write pull-requests: write jobs: sync-versions: name: Sync Template Versions # Only run on Renovate PRs if: startsWith(github.head_ref, 'renovate/') runs-on: ubuntu-latest steps: - name: Checkout PR branch uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} fetch-depth: 0 # Fetch all history to compare with main token: ${{ secrets.GITHUB_TOKEN }} - name: Detect changed template files id: changes run: | # Fetch main branch for comparison git fetch origin main:main # Get list of changed files in library/ CHANGED_FILES=$(git diff --name-only main...HEAD | grep '^library/' | grep -E '\.(j2|yaml|yml)$' || true) if [ -n "$CHANGED_FILES" ]; then echo "Changed template files:" echo "$CHANGED_FILES" echo "files<> $GITHUB_OUTPUT echo "$CHANGED_FILES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT echo "has_files=true" >> $GITHUB_OUTPUT else echo "No template files changed" echo "has_files=false" >> $GITHUB_OUTPUT fi - name: Run template version sync id: sync if: steps.changes.outputs.has_files == 'true' run: | echo "Running template version sync script..." chmod +x .github/scripts/sync-template-version.sh # Pass changed files to the script .github/scripts/sync-template-version.sh ${{ steps.changes.outputs.files }} - name: Check for template.yaml changes id: template_changes run: | if [[ -n $(git status --porcelain library/**/template.yaml) ]]; then echo "has_changes=true" >> $GITHUB_OUTPUT echo "Template version changes detected" else echo "has_changes=false" >> $GITHUB_OUTPUT echo "No template version changes needed" fi - name: Commit and push changes if: steps.template_changes.outputs.has_changes == 'true' run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add library/**/template.yaml git commit -m "chore: sync template versions with image updates" git push ================================================ FILE: .gitignore ================================================ # Ignore local development files **/.vscode **/.DS_Store # Docker Secrets, Environment Files **/secret.* **/.env **/.envrc **/.direnv # Ignore Ansible **/.ansible # Python **/__pycache__/ **/*.py[cod] **/*.pyo **/*.pyd **/.venv **/venv/ **/.ruff_cache/ # Packaging *.egg-info/ build/ dist/ # Installation tracking .installed-version # Test outputs config.yaml ================================================ FILE: .wiki/Core-Concepts-Defaults.md ================================================ # Default Variables Save time by setting default values for variables you use frequently. This page explains how to manage your default variable configuration. ## What are Default Variables? **Default variables** are user-defined values that override module and template defaults. They allow you to: - Avoid repetitive typing during template generation - Standardize values across multiple templates - Customize your environment once, use everywhere ## Precedence Order Variables are resolved in this order (lowest to highest priority): 1. Module spec (module-wide defaults) 2. Template spec (template-specific defaults) 3. **User config** (your saved defaults) ← This page 4. CLI arguments (`--var` flags) Your defaults override module and template values but can be overridden by CLI arguments. ## Managing Defaults All default management commands follow this pattern: ```bash boilerplates defaults [args] ``` ### View Defaults List all saved defaults for a module: ```bash boilerplates compose defaults list ``` Output: ``` Default Variables (Compose): container_timezone: America/New_York restart_policy: unless-stopped traefik_network: traefik network_external: true ``` ### Set a Default Save a default value: ```bash boilerplates compose defaults set container_timezone="America/New_York" ``` Response: ``` Set default: container_timezone = America/New_York ``` **Tips:** - Use quotes for values with spaces - Booleans: `true` or `false` - Numbers: no quotes needed **Examples:** ```bash # String boilerplates compose defaults set restart_policy="unless-stopped" # Integer boilerplates compose defaults set user_uid=1000 # Boolean boilerplates compose defaults set traefik_enabled=true # String with spaces boilerplates compose defaults set container_hostname="my app server" ``` ### Get a Default View a single default value: ```bash boilerplates compose defaults get container_timezone ``` Output: ``` container_timezone: America/New_York ``` ### Remove a Default Delete a saved default: ```bash boilerplates compose defaults rm container_timezone ``` Response: ``` Removed default: container_timezone ``` The variable will now use module/template defaults again. ### Clear All Defaults Remove all saved defaults for a module: ```bash boilerplates compose defaults clear ``` Response: ``` Cleared all defaults for compose ``` **Warning:** This cannot be undone. Consider backing up your config first. ## Configuration Storage Defaults are stored in: ``` ~/.config/boilerplates/config.yaml ``` Example content: ```yaml libraries: - name: default type: git url: https://github.com/christianlempa/boilerplates branch: main directory: library defaults: compose: container_timezone: America/New_York restart_policy: unless-stopped traefik_network: traefik network_external: true terraform: region: us-east-1 instance_type: t3.micro ``` ### Manual Editing You can manually edit the config file: ```bash # Edit configuration nano ~/.config/boilerplates/config.yaml # Verify defaults boilerplates compose defaults list ``` ## Common Use Cases ### Timezone Configuration Set your local timezone once: ```bash boilerplates compose defaults set container_timezone="Europe/Berlin" ``` Now all Docker containers use your timezone by default. ### Network Configuration Standardize network settings: ```bash boilerplates compose defaults set network_external=true boilerplates compose defaults set network_name="docker-network" ``` ### Traefik Configuration Set common Traefik values: ```bash boilerplates compose defaults set traefik_network="traefik" boilerplates compose defaults set traefik_domain="example.com" boilerplates compose defaults set traefik_tls_certresolver="cloudflare" ``` ### User IDs Match your host user: ```bash boilerplates compose defaults set user_uid=$(id -u) boilerplates compose defaults set user_gid=$(id -g) ``` ### Restart Policy Standardize container behavior: ```bash boilerplates compose defaults set restart_policy="unless-stopped" ``` ## Overriding Defaults Even with defaults set, you can override them: ### Interactive Mode During interactive generation, defaults appear as pre-filled values. Press Enter to accept or type a new value: ``` Container timezone [America/New_York]: Europe/London ``` ### CLI Arguments Override with `--var`: ```bash boilerplates compose generate nginx \ --var container_timezone="UTC" \ --no-interactive ``` The CLI argument takes precedence over your saved default. ## Per-Module Defaults Each module has its own defaults: ```bash # Compose defaults boilerplates compose defaults set restart_policy="unless-stopped" # Terraform defaults (separate) boilerplates terraform defaults set region="us-east-1" # Ansible defaults (separate) boilerplates ansible defaults set become=true ``` Defaults don't transfer between modules—they're module-specific. ## Backup and Restore ### Backup Configuration Save your configuration: ```bash cp ~/.config/boilerplates/config.yaml ~/boilerplates-config-backup.yaml ``` ### Restore Configuration Restore from backup: ```bash cp ~/boilerplates-config-backup.yaml ~/.config/boilerplates/config.yaml ``` ### Share Configuration Share defaults with your team: ```bash # Export defaults cat ~/.config/boilerplates/config.yaml | grep -A 100 "defaults:" > team-defaults.yaml # Share file with team # Team members can merge into their config.yaml ``` ## Advanced Usage ### Environment-Specific Defaults Maintain multiple configurations: ```bash # Production defaults cp ~/.config/boilerplates/config.yaml ~/.config/boilerplates/config-prod.yaml # Development defaults cp ~/.config/boilerplates/config.yaml ~/.config/boilerplates/config-dev.yaml # Switch between them cp ~/.config/boilerplates/config-prod.yaml ~/.config/boilerplates/config.yaml ``` ### Scripted Configuration Set defaults programmatically: ```bash #!/bin/bash # Set common defaults boilerplates compose defaults set container_timezone="$(cat /etc/timezone)" boilerplates compose defaults set user_uid="$(id -u)" boilerplates compose defaults set user_gid="$(id -g)" boilerplates compose defaults set restart_policy="unless-stopped" ``` ## Troubleshooting ### Defaults Not Applied If defaults aren't being used: ```bash # Verify defaults are set boilerplates compose defaults list # Check config file cat ~/.config/boilerplates/config.yaml # Ensure module name matches # "compose" not "docker-compose" ``` ### Config File Errors If config file is corrupted: ```bash # Validate YAML syntax python3 -c "import yaml; yaml.safe_load(open('~/.config/boilerplates/config.yaml'))" # Or remove and recreate mv ~/.config/boilerplates/config.yaml ~/.config/boilerplates/config.yaml.bak boilerplates repo update # Recreates config ``` ### Wrong Values If wrong values appear: ```bash # Check precedence # 1. Module spec # 2. Template spec # 3. User defaults ← Check here # 4. CLI --var # Verify your defaults boilerplates compose defaults list # Check template spec boilerplates compose show ``` ## Best Practices ### Essential Defaults Set these common defaults: ```bash # System boilerplates compose defaults set container_timezone="$(cat /etc/timezone)" boilerplates compose defaults set user_uid=$(id -u) boilerplates compose defaults set user_gid=$(id -g) # Containers boilerplates compose defaults set restart_policy="unless-stopped" # Networking (if using external networks) boilerplates compose defaults set network_external=true boilerplates compose defaults set network_name="docker-network" ``` ### Don't Over-Configure Only set defaults for values you use consistently: **Good:** - Timezone (same everywhere) - User UID/GID (same everywhere) - Network settings (if standardized) **Bad:** - Service names (unique per service) - Hostnames (unique per service) - Port numbers (conflict-prone) ### Document Your Defaults Keep a list of your defaults for reference: ```bash # Save to file boilerplates compose defaults list > ~/my-defaults.txt ``` ### Review Periodically Check your defaults occasionally: ```bash boilerplates compose defaults list ``` Remove obsolete or unused values. ## Next Steps - [Libraries](Core-Concepts-Libraries) - Managing template libraries - [Variables](Core-Concepts-Variables) - Understanding variable types and behavior - [Getting Started](Getting-Started) - Using defaults in template generation ## See Also - [Installation](Installation) - CLI setup - [Concepts](Core-Concepts-Templates) - How templates work ================================================ FILE: .wiki/Core-Concepts-Libraries.md ================================================ # Libraries Libraries are collections of templates that can be synced from Git repositories or loaded from local directories. This page explains how to manage template libraries. ## What is a Library? A **library** is a collection of templates organized by module type (compose, terraform, ansible, etc.). Libraries can be: - **Git-based** - Synced from remote repositories - **Static** - Local directories on your filesystem ## Default Library By default, Boilerplates uses the official template library: ``` Name: default URL: https://github.com/christianlempa/boilerplates Branch: main Directory: library ``` This provides production-ready templates for various services and infrastructure. ## Library Location Libraries are stored locally at: ``` ~/.config/boilerplates/libraries/ └── default/ └── library/ ├── compose/ ├── terraform/ └── ansible/ ``` ## Managing Libraries ### List Libraries View all configured libraries: ```bash boilerplates repo list ``` Output: ``` Libraries: default (git) URL: https://github.com/christianlempa/boilerplates Branch: main Directory: library Status: Synced ``` ### Update Libraries Sync all Git-based libraries: ```bash boilerplates repo update ``` This: - Pulls latest changes from Git repositories - Uses sparse-checkout (only downloads template directories) - Updates metadata cache ### Add Custom Library Add your own template library: ```bash boilerplates repo add my-templates https://github.com/user/templates \ --directory library \ --branch main ``` Parameters: - **name** - Unique library identifier - **url** - Git repository URL - **--directory** - Path to templates within repository (default: `.`) - **--branch** - Git branch to use (default: `main`) ### Remove Library Remove a library from configuration: ```bash boilerplates repo remove my-templates ``` This removes the configuration but keeps downloaded files. To fully clean up: ```bash rm -rf ~/.config/boilerplates/libraries/my-templates ``` ## Library Types ### Git Libraries Synced from remote Git repositories: ```yaml libraries: - name: default type: git url: https://github.com/christianlempa/boilerplates branch: main directory: library ``` **Benefits:** - Always up-to-date - Version controlled - Easy to share - Automatic updates **Use cases:** - Official templates - Team-shared templates - Public template collections ### Static Libraries Local directories on your filesystem: ```yaml libraries: - name: local type: static path: ~/my-templates ``` **Benefits:** - No network required - Full control - Fast access - Development/testing **Use cases:** - Local development - Private templates - Custom modifications - Testing new templates ## Library Priority When multiple libraries contain the same template, **priority** determines which is used: ```yaml libraries: - name: local # Priority 1 (highest) type: static path: ~/my-templates - name: default # Priority 2 type: git url: https://github.com/christianlempa/boilerplates ``` ### Simple IDs Use the template name without qualification: ```bash boilerplates compose generate nginx ``` The CLI uses the first matching template (from `local` in the example above). ### Qualified IDs Target a specific library: ```bash boilerplates compose generate nginx.local # Uses local library boilerplates compose generate nginx.default # Uses default library ``` ## Configuration File Library configuration is stored in: ``` ~/.config/boilerplates/config.yaml ``` Example: ```yaml libraries: - name: default type: git url: https://github.com/christianlempa/boilerplates branch: main directory: library - name: local type: static path: /Users/me/my-templates ``` ### Manual Editing You can manually edit `config.yaml`: ```bash # Edit configuration nano ~/.config/boilerplates/config.yaml # Verify changes boilerplates repo list ``` ## Advanced Usage ### Multiple Git Branches Use different branches for stable vs. development templates: ```yaml libraries: - name: stable type: git url: https://github.com/user/templates branch: main directory: library - name: dev type: git url: https://github.com/user/templates branch: development directory: library ``` ### Sparse Checkout Git libraries use sparse-checkout to minimize disk usage: ``` # Only downloads: library/compose/ library/terraform/ library/ansible/ # Ignores: .github/ docs/ tests/ README.md ``` This keeps library downloads fast and disk usage low. ### Private Repositories For private Git repositories, ensure SSH or HTTPS authentication is configured: **SSH:** ```bash boilerplates repo add private git@github.com:user/private-templates.git \ --directory library \ --branch main ``` Requires SSH key configured with GitHub/GitLab. **HTTPS with credentials:** ```bash # Configure Git credential helper git config --global credential.helper store # Add library (will prompt for credentials on first sync) boilerplates repo add private https://github.com/user/private-templates.git \ --directory library \ --branch main ``` ## Template Discovery After adding libraries, templates are discovered automatically: ```bash # Sync libraries boilerplates repo update # List templates from all libraries boilerplates compose list # Show template details (uses priority order) boilerplates compose show nginx # Show from specific library boilerplates compose show nginx.local ``` ## Troubleshooting ### Library Not Syncing If `repo update` fails: ```bash # Check network connectivity ping github.com # Verify Git access git ls-remote https://github.com/christianlempa/boilerplates # Remove and re-add library boilerplates repo remove default boilerplates repo add default https://github.com/christianlempa/boilerplates \ --directory library \ --branch main ``` ### Templates Not Found If templates don't appear: ```bash # Verify library is configured boilerplates repo list # Update libraries boilerplates repo update # Check library directory structure ls -la ~/.config/boilerplates/libraries/default/library/compose/ ``` ### Duplicate Template Names If two libraries have the same template: ```bash # Check which library provides it boilerplates compose show nginx # Use qualified ID to target specific library boilerplates compose generate nginx.local ``` ## Best Practices ### Library Organization Structure your libraries consistently: ``` my-templates/ ├── library/ │ ├── compose/ │ │ ├── app1/ │ │ └── app2/ │ ├── terraform/ │ └── ansible/ └── README.md ``` ### Version Control For Git libraries: - Use semantic versioning tags - Maintain a CHANGELOG - Test templates before merging - Use branches for development ### Naming - Use descriptive library names - Avoid special characters - Keep names short but meaningful **Good:** `production`, `dev`, `team-infra` **Bad:** `my-lib-123`, `temp`, `new` ### Documentation Each library should have: - README.md with overview - Template documentation - Usage examples - Contribution guidelines ## Next Steps - [Default Variables](Core-Concepts-Defaults) - Managing variable defaults - [Templates](Core-Concepts-Templates) - Understanding template structure - [Developer Guide](Developers-Templates) - Creating templates for libraries ## See Also - [Getting Started](Getting-Started) - Your first template - [Installation](Installation) - Installing the CLI ================================================ FILE: .wiki/Core-Concepts-Templates.md ================================================ # Templates Templates are the core building blocks of the Boilerplates CLI. This page explains what templates are, how they work, and how to use them effectively. ## What is a Template? A template is a **directory-based configuration package** that contains: - **Metadata** - Name, description, version, author information - **Variable specifications** - Configurable parameters - **Template files** - Jinja2 templates that generate your configuration - **Static files** - Files copied as-is (optional) When you generate a template, the CLI: 1. Prompts you for variable values (or uses defaults/CLI overrides) 2. Renders template files using Jinja2 3. Writes the generated files to your specified directory ## Template Structure Every template is a directory containing at minimum a `template.yaml` file: ``` template-name/ ├── template.yaml # Template definition and metadata ├── docker-compose.yml.j2 # Jinja2 template files ├── .env.j2 # Environment configuration └── README.md # Static file (copied as-is) ``` ### The template.yaml File This file defines everything about your template: ```yaml --- kind: compose # Module type (compose, terraform, ansible, etc.) schema: "X.Y" # Schema version (affects available features) metadata: name: My Service description: Service description with Markdown support version: 1.0.0 # Application/service version author: Your Name date: '2025-01-12' tags: - docker - service spec: # Variable specifications (see Variables page) ``` ## Template Discovery Templates are organized in **libraries**. A library is a collection of templates for a specific module type. ### Default Library Structure ``` ~/.config/boilerplates/libraries/ └── default/ └── library/ ├── compose/ │ ├── nginx/ │ ├── traefik/ │ └── whoami/ ├── terraform/ └── ansible/ ``` ### Finding Templates ```bash # List all templates for a module boilerplates compose list # Search templates by name boilerplates compose search proxy # Show details about a template boilerplates compose show nginx ``` ## Template Metadata ### Required Fields ```yaml metadata: name: Template Name # Display name description: Description # What the template does version: 1.0.0 # Application version author: Your Name # Template author date: '2025-01-12' # Last update date ``` ### Optional Fields ```yaml metadata: tags: # Searchable tags - docker - web-server draft: false # Hide from listings if true next_steps: | # Post-generation instructions ## What's Next 1. Review the generated files 2. Customize as needed 3. Deploy! ``` ### Description Markdown The `description` field supports Markdown: ```yaml metadata: description: | A **powerful reverse proxy** and load balancer. ## Features - Automatic HTTPS - Load balancing - Let's Encrypt integration ## Resources - **Project**: https://traefik.io - **Documentation**: https://doc.traefik.io ``` This renders nicely when you run `boilerplates compose show