Explorar el Código

renovate updates and template sanity checks

xcad hace 4 meses
padre
commit
8f2b257a30
Se han modificado 59 ficheros con 691 adiciones y 187 borrados
  1. 94 0
      .renovate/README.md
  2. 42 0
      .renovate/sync-template-version.sh
  3. 62 5
      AGENTS.md
  4. 10 1
      cli/core/display.py
  5. 44 25
      cli/core/module.py
  6. 11 0
      cli/core/prompt.py
  7. 54 1
      cli/core/template.py
  8. 185 9
      cli/core/variables.py
  9. 8 0
      cli/modules/compose.py
  10. 8 7
      library/compose/alloy/compose.yaml.j2
  11. 1 1
      library/compose/alloy/template.yaml
  12. 1 1
      library/compose/ansiblesemaphore/template.yaml
  13. 6 6
      library/compose/authentik/compose.yaml.j2
  14. 1 1
      library/compose/authentik/template.yaml
  15. 1 1
      library/compose/bind9/template.yaml
  16. 1 1
      library/compose/cadvisor/template.yaml
  17. 1 1
      library/compose/checkmk/template.yaml
  18. 1 1
      library/compose/clamav/template.yaml
  19. 1 1
      library/compose/dockge/template.yaml
  20. 6 6
      library/compose/gitea/compose.yaml.j2
  21. 1 1
      library/compose/gitea/template.yaml
  22. 1 1
      library/compose/gitlab-runner/template.yaml
  23. 20 10
      library/compose/gitlab/compose.yaml.j2
  24. 1 1
      library/compose/gitlab/template.yaml
  25. 6 6
      library/compose/grafana/compose.yaml.j2
  26. 1 1
      library/compose/grafana/template.yaml
  27. 1 1
      library/compose/heimdall/template.yaml
  28. 1 1
      library/compose/homeassistant/template.yaml
  29. 1 1
      library/compose/homepage/template.yaml
  30. 6 6
      library/compose/homer/compose.yaml.j2
  31. 1 1
      library/compose/homer/template.yaml
  32. 6 6
      library/compose/influxdb/compose.yaml.j2
  33. 1 1
      library/compose/influxdb/template.yaml
  34. 1 1
      library/compose/loki/template.yaml
  35. 1 1
      library/compose/mariadb/template.yaml
  36. 6 4
      library/compose/n8n/compose.yaml.j2
  37. 1 1
      library/compose/n8n/template.yaml
  38. 6 6
      library/compose/nextcloud/compose.yaml.j2
  39. 1 1
      library/compose/nextcloud/template.yaml
  40. 18 12
      library/compose/nginx/compose.yaml.j2
  41. 1 1
      library/compose/nginx/template.yaml
  42. 1 1
      library/compose/nginxproxymanager/template.yaml
  43. 1 1
      library/compose/nodeexporter/template.yaml
  44. 1 1
      library/compose/openwebui/template.yaml
  45. 1 1
      library/compose/passbolt/template.yaml
  46. 9 8
      library/compose/pihole/compose.yaml.j2
  47. 1 1
      library/compose/pihole/template.yaml
  48. 8 7
      library/compose/portainer/compose.yaml.j2
  49. 1 1
      library/compose/portainer/template.yaml
  50. 1 1
      library/compose/postgres/template.yaml
  51. 1 1
      library/compose/prometheus/template.yaml
  52. 1 1
      library/compose/promtail/template.yaml
  53. 1 1
      library/compose/teleport/template.yaml
  54. 3 1
      library/compose/traefik/config/traefik.yaml.j2
  55. 32 20
      library/compose/traefik/template.yaml
  56. 1 1
      library/compose/twingate-connector/template.yaml
  57. 1 1
      library/compose/uptimekuma/template.yaml
  58. 1 1
      library/compose/wazuh/template.yaml
  59. 6 6
      library/compose/whoami/compose.yaml.j2

+ 94 - 0
.renovate/README.md

@@ -0,0 +1,94 @@
+# Renovate Configuration
+
+This directory contains helper scripts and configuration for Renovate bot automation.
+
+## Template Version Sync
+
+### Overview
+
+The `sync-template-version.sh` script automatically syncs Docker image versions from `compose.yaml.j2` files to their corresponding `template.yaml` metadata files.
+
+### How It Works
+
+1. **Renovate detects updates**: The custom regex manager in `renovate.json` detects Docker image versions in `.j2` template files
+2. **Updates are applied**: When Renovate creates a PR, it updates the Docker image version in `compose.yaml.j2`
+3. **Post-upgrade task runs**: After the update, the `sync-template-version.sh` script runs automatically
+4. **Metadata synced**: The script extracts the first Docker image version from each `compose.yaml.j2` and updates the `version` field in the corresponding `template.yaml`
+
+### Configuration
+
+In `renovate.json`, the following configuration enables this feature:
+
+```json
+{
+  "customManagers": [
+    {
+      "customType": "regex",
+      "description": "Update Docker images in Jinja2 compose templates",
+      "managerFilePatterns": [
+        "/^library/compose/.+/compose\\.ya?ml\\.j2$/"
+      ],
+      "matchStrings": [
+        "image:\\s*(?<depName>[^:\\s]+):(?<currentValue>[^\\s\\n{]+)"
+      ],
+      "datasourceTemplate": "docker"
+    }
+  ],
+  "postUpgradeTasks": {
+    "commands": [
+      ".renovate/sync-template-version.sh"
+    ],
+    "fileFilters": [
+      "library/compose/**/template.yaml"
+    ],
+    "executionMode": "update"
+  }
+}
+```
+
+### Manual Execution
+
+You can run the script manually at any time:
+
+```bash
+./.renovate/sync-template-version.sh
+```
+
+This will scan all compose templates and update their metadata versions to match the Docker image versions.
+
+### Limitations
+
+- Only updates templates that have a Docker image with a version tag (e.g., `image: name:1.2.3`)
+- Skips templates using Jinja2 variables for versions (e.g., `image: name:{{ version }}`)
+- Uses the **first** image found in the `compose.yaml.j2` file (typically the main application image)
+- Templates without `template.yaml` files are skipped
+
+### Template Structure
+
+Expected directory structure for each template:
+
+```
+library/compose/<template-name>/
+├── compose.yaml.j2     # Jinja2 template with Docker Compose config
+├── template.yaml       # Template metadata (includes version field)
+└── ... (other files)
+```
+
+The `template.yaml` should have a `version` field in the metadata section:
+
+```yaml
+---
+kind: compose
+metadata:
+  name: Application Name
+  description: Description
+  version: 0.1.0  # This will be auto-updated
+  author: Christian Lempa
+  date: '2025-10-02'
+```
+
+### Benefits
+
+- **Consistency**: Template versions automatically track Docker image versions
+- **Automation**: No manual version updates needed when Docker images are updated
+- **Traceability**: Easy to see which Docker image version a template was designed for

+ 42 - 0
.renovate/sync-template-version.sh

@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# Sync the first Docker image version from compose.yaml.j2 to template.yaml
+# This script is called by Renovate as a post-upgrade task
+
+set -euo pipefail
+
+# Find all template directories
+find library/compose -type f -name "compose.yaml.j2" | while read -r compose_file; do
+    template_dir=$(dirname "$compose_file")
+    template_file="$template_dir/template.yaml"
+    
+    # Skip if template.yaml doesn't exist
+    [ ! -f "$template_file" ] && continue
+    
+    # Extract the first image version from compose.yaml.j2
+    # This matches: image: repo/name:version or image: name:version
+    # Ignores Jinja2 variables like {{ variable }}
+    version=$(grep -E '^\s*image:\s*[^{]*:[^{}\s]+' "$compose_file" | head -n1 | sed -E 's/.*:([^:]+)$/\1/' | tr -d ' ' || true)
+    
+    # Skip if no version found or if it's a Jinja2 variable
+    if [ -z "$version" ] || [[ "$version" =~ \{\{ ]]; then
+        continue
+    fi
+    
+    # Get current template version and trim whitespace
+    current_version=$(grep -E '^\s*version:\s*' "$template_file" | sed -E 's/.*version:\s*['\''"]?([^'\''"]+)['\''"]?/\1/' | tr -d ' ')
+    
+    # Only update if versions are different
+    if [ -n "$current_version" ] && [ "$version" != "$current_version" ]; then
+        echo "Updating $template_file: $current_version -> $version"
+        
+        # Use sed to update the version in template.yaml
+        # Works on both macOS and Linux
+        if [[ "$OSTYPE" == "darwin"* ]]; then
+            sed -i '' "s/version: .*/version: $version/" "$template_file"
+        else
+            sed -i "s/version: .*/version: $version/" "$template_file"
+        fi
+    fi
+done
+
+echo "Template version sync complete"

+ 62 - 5
AGENTS.md

@@ -36,24 +36,29 @@ python3 -m cli compose list
 # Debugging commands
 python3 -m cli --log-level DEBUG compose list
 
+# Generate template to directory named after template (default)
+python3 -m cli compose generate nginx
+
+# Generate template to custom directory
+python3 -m cli compose generate nginx my-nginx-server
+
 # Generate template interactively (default - prompts for variables)
-python3 -m cli compose generate authentik --out /tmp/my-project
+python3 -m cli compose generate authentik
 
 # Generate template non-interactively (skips prompts, uses defaults and CLI variables)
-python3 -m cli compose generate authentik --out /tmp/my-project --no-interactive
+python3 -m cli compose generate authentik my-auth --no-interactive
 
 # Generate with variable overrides (non-interactive)
-python3 -m cli compose generate authentik \
+python3 -m cli compose generate authentik my-auth \
   --var service_name=auth \
   --var ports_enabled=false \
   --var database_type=postgres \
-  --out /tmp/my-project \
   --no-interactive
 
 # Show template details
 python3 -m cli compose show authentik
 
-# Managing default values (renamed from 'config' to 'defaults')
+# Managing default values
 python3 -m cli compose defaults set service_name my-app
 python3 -m cli compose defaults get
 python3 -m cli compose defaults list
@@ -128,6 +133,13 @@ spec:
 
 -   **Static Files**: Any file without a `.j2` extension is treated as a static file and will be copied to the output directory as-is, preserving its relative path and filename.
 
+-   **Content Sanitization**: All rendered Jinja2 templates are automatically sanitized to improve output quality:
+    - Multiple consecutive blank lines are reduced to a single blank line
+    - Leading blank lines are removed
+    - Trailing whitespace is stripped from each line
+    - Files are ensured to end with exactly one newline character
+    - This prevents common formatting issues from conditional Jinja2 blocks
+
 #### Example Directory Structure
 
 ```
@@ -168,6 +180,51 @@ The `Variable.origin` attribute is updated to reflect this chain (e.g., `module
 - During an interactive session, the CLI will first ask the user to enable or disable the section by prompting for the toggle variable (e.g., "Enable advanced settings?").
 - If the section is disabled (the toggle is `false`), all other variables within that section are skipped, and the section is visually dimmed in the summary table. This provides a clean way to manage optional or advanced configurations.
 
+**4. Section Dependencies:**
+
+- Sections can declare dependencies on other sections using the `needs` property.
+- **Example**: `needs: "traefik"` or `needs: ["database", "redis"]` for multiple dependencies.
+- **Purpose**: Ensures that dependent sections are only shown/processed when their dependency sections are enabled.
+- **Behavior**:
+  - During interactive prompting: If a dependency is not satisfied (disabled or not enabled), the dependent section is automatically skipped with a message like `⊘ Section Name (skipped - requires dependency_name to be enabled)`.
+  - During non-interactive generation: Variables from sections with unsatisfied dependencies are excluded from the Jinja2 rendering context.
+  - During validation: Sections with unsatisfied dependencies are skipped.
+- **Validation**: Dependencies are validated at template load time:
+  - Circular dependencies are detected and cause an error (e.g., A needs B, B needs A).
+  - Missing dependencies cause an error (e.g., A needs B, but B doesn't exist).
+  - Self-dependencies cause an error (e.g., A needs A).
+- **Sorting**: Sections are automatically sorted using topological sort to ensure dependencies come before dependents, while preserving the original order within priority groups (required, enabled, disabled).
+- **Use Cases**:
+  - Split complex configurations: e.g., `traefik` (basic) and `traefik_tls` (needs traefik) sections.
+  - Conditional features: e.g., `database_backup` (needs database) or `monitoring_alerts` (needs monitoring).
+  - Hierarchical settings: e.g., `email` (basic) and `email_advanced` (needs email) sections.
+
+**Example Section with Dependencies:**
+
+```yaml
+spec:
+  traefik:
+    title: "Traefik"
+    toggle: "traefik_enabled"
+    vars:
+      traefik_enabled:
+        type: "bool"
+        default: false
+      traefik_host:
+        type: "hostname"
+  
+  traefik_tls:
+    title: "Traefik TLS/SSL"
+    needs: "traefik"  # Only shown if traefik is enabled
+    toggle: "traefik_tls_enabled"
+    vars:
+      traefik_tls_enabled:
+        type: "bool"
+        default: true
+      traefik_tls_certresolver:
+        type: "str"
+```
+
 ## Future Improvements
 
 ### Managing TODOs as GitHub Issues

+ 10 - 1
cli/core/display.py

@@ -158,7 +158,12 @@ class DisplayManager:
 
             disabled_text = " (disabled)" if is_dimmed else ""
             required_text = " [yellow](required)[/yellow]" if section.required else ""
-            header_text = f"[bold dim]{section.title}{required_text}{disabled_text}[/bold dim]" if is_dimmed else f"[bold]{section.title}{required_text}{disabled_text}[/bold]"
+            # Add dependency information
+            needs_text = ""
+            if section.needs:
+              needs_list = ", ".join(section.needs)
+              needs_text = f" [dim](needs: {needs_list})[/dim]"
+            header_text = f"[bold dim]{section.title}{required_text}{needs_text}{disabled_text}[/bold dim]" if is_dimmed else f"[bold]{section.title}{required_text}{needs_text}{disabled_text}[/bold]"
             variables_table.add_row(header_text, "", "", "", "")
 
             for var_name, variable in section.variables.items():
@@ -255,6 +260,7 @@ class DisplayManager:
             section_desc = section_data.get("description", "")
             section_required = section_data.get("required", False)
             section_toggle = section_data.get("toggle", None)
+            section_needs = section_data.get("needs", None)
 
             # Build section label
             section_label = f"[cyan]{section_name}[/cyan]"
@@ -262,6 +268,9 @@ class DisplayManager:
                 section_label += " [yellow](required)[/yellow]"
             if section_toggle:
                 section_label += f" [dim](toggle: {section_toggle})[/dim]"
+            if section_needs:
+                needs_str = ", ".join(section_needs) if isinstance(section_needs, list) else section_needs
+                section_label += f" [dim](needs: {needs_str})[/dim]"
             
             if show_all and section_desc:
                 section_label += f"\n  [dim]{section_desc}[/dim]"

+ 44 - 25
cli/core/module.py

@@ -171,7 +171,7 @@ class Module(ABC):
   def generate(
     self,
     id: str = Argument(..., help="Template ID"),
-    out: Optional[Path] = Option(None, "--out", "-o", help="Output directory"),
+    directory: Optional[str] = Argument(None, help="Output directory (defaults to template ID)"),
     interactive: bool = Option(True, "--interactive/--no-interactive", "-i/-n", help="Enable interactive prompting for variables"),
     var: Optional[list[str]] = Option(None, "--var", "-v", help="Variable override (repeatable). Use KEY=VALUE or --var KEY VALUE"),
     ctx: Context = None,
@@ -183,6 +183,16 @@ class Module(ABC):
     2. Template spec (from template.yaml)
     3. Config defaults (from ~/.config/boilerplates/config.yaml)
     4. CLI overrides (--var flags)
+    
+    Examples:
+        # Generate to directory named after template
+        cli compose generate traefik
+        
+        # Generate to custom directory
+        cli compose generate traefik my-proxy
+        
+        # Generate with variables
+        cli compose generate traefik --var traefik_enabled=false
     """
 
     logger.info(f"Starting generation for template '{id}' from module '{self.name}'")
@@ -224,7 +234,8 @@ class Module(ABC):
         logger.info(f"Collected {len(collected_values)} variable values from user input")
 
     if template.variables:
-      variable_values.update(template.variables.get_all_values())
+      # Use get_satisfied_values() to exclude variables from sections with unsatisfied dependencies
+      variable_values.update(template.variables.get_satisfied_values())
 
     try:
       # Validate all variables before rendering
@@ -239,17 +250,39 @@ class Module(ABC):
         raise Exit(code=1)
       
       logger.info(f"Successfully rendered template '{id}'")
-      output_dir = out or Path(".")
+      
+      # Determine output directory (default to template ID)
+      output_dir = Path(directory) if directory else Path(id)
+      
+      # Check if directory exists and is not empty
+      dir_exists = output_dir.exists()
+      dir_not_empty = dir_exists and any(output_dir.iterdir())
       
       # Check which files already exist
       existing_files = []
-      if output_dir.exists():
+      if dir_exists:
         for file_path in rendered_files.keys():
           full_path = output_dir / file_path
           if full_path.exists():
             existing_files.append(full_path)
       
-      # Display file generation confirmation
+      # Warn if directory is not empty (both interactive and non-interactive)
+      if dir_not_empty:
+        if interactive:
+          console.print(f"\n[yellow]⚠ Warning: Directory '{output_dir}' is not empty.[/yellow]")
+          if existing_files:
+            console.print(f"[yellow]  {len(existing_files)} file(s) will be overwritten.[/yellow]")
+          
+          if not Confirm.ask(f"Continue and potentially overwrite files in '{output_dir}'?", default=False):
+            console.print("[yellow]Generation cancelled.[/yellow]")
+            return
+        else:
+          # Non-interactive mode: show warning but continue
+          logger.warning(f"Directory '{output_dir}' is not empty")
+          if existing_files:
+            logger.warning(f"{len(existing_files)} file(s) will be overwritten")
+      
+      # Display file generation confirmation in interactive mode
       if interactive:
         self.display.display_file_generation_confirmation(
           output_dir, 
@@ -257,19 +290,11 @@ class Module(ABC):
           existing_files if existing_files else None
         )
         
-        # Ask for confirmation
-        if existing_files:
-          prompt_msg = f"[yellow][/yellow]  {len(existing_files)} file(s) will be overwritten. Continue?"
-        else:
-          prompt_msg = "Generate these files?"
-        
-        if not Confirm.ask(prompt_msg, default=True):
-          console.print("[yellow]Generation cancelled.[/yellow]")
-          return
-      else:
-        # Non-interactive mode: warn if files will be overwritten
-        if existing_files:
-          logger.warning(f"{len(existing_files)} file(s) will be overwritten in '{output_dir}'")
+        # Final confirmation (only if we didn't already ask about overwriting)
+        if not dir_not_empty:
+          if not Confirm.ask("Generate these files?", default=True):
+            console.print("[yellow]Generation cancelled.[/yellow]")
+            return
       
       # Create the output directory if it doesn't exist
       output_dir.mkdir(parents=True, exist_ok=True)
@@ -282,15 +307,9 @@ class Module(ABC):
           f.write(content)
         console.print(f"[green]Generated file: {file_path}[/green]")
       
+      console.print(f"\n[green]✓ Template generated successfully in '{output_dir}'[/green]")
       logger.info(f"Template written to directory: {output_dir}")
 
-      # If no output directory was specified, print the masked content to the console
-      if not out:
-        console.print("\n[bold]Rendered output (sensitive values masked):[/bold]")
-        masked_files = template.mask_sensitive_values(rendered_files, template.variables)
-        for file_path, content in masked_files.items():
-          console.print(Panel(content, title=file_path, border_style="green"))
-
     except Exception as e:
       logger.error(f"Error rendering template '{id}': {e}")
       console.print(f"[red]Error generating template: {e}[/red]")

+ 11 - 0
cli/core/prompt.py

@@ -47,6 +47,17 @@ class PromptHandler:
       if not section.variables:
         continue
 
+      # Check if dependencies are satisfied
+      if not variables.is_section_satisfied(section_key):
+        # Get list of unsatisfied dependencies for better user feedback
+        unsatisfied = [dep for dep in section.needs if not variables.is_section_satisfied(dep)]
+        dep_names = ", ".join(unsatisfied) if unsatisfied else "unknown"
+        self.console.print(
+          f"\n[dim]⊘ {section.title} (skipped - requires {dep_names} to be enabled)[/dim]"
+        )
+        logger.debug(f"Skipping section '{section_key}' - dependencies not satisfied: {dep_names}")
+        continue
+
       # Always show section header first
       self.display.display_section_header(section.title, section.description)
 

+ 54 - 1
cli/core/template.py

@@ -470,7 +470,8 @@ class Template:
 
   def render(self, variables: VariableCollection) -> Dict[str, str]:
     """Render all .j2 files in the template directory."""
-    variable_values = variables.get_all_values()
+    # Use get_satisfied_values() to exclude variables from sections with unsatisfied dependencies
+    variable_values = variables.get_satisfied_values()
     logger.debug(f"Rendering template '{self.id}' with variables: {variable_values}")
     rendered_files = {}
     for template_file in self.template_files: # Iterate over TemplateFile objects
@@ -478,6 +479,8 @@ class Template:
         try:
           template = self.jinja_env.get_template(str(template_file.relative_path)) # Use lazy-loaded jinja_env
           rendered_content = template.render(**variable_values)
+          # Sanitize the rendered content to remove excessive blank lines
+          rendered_content = self._sanitize_content(rendered_content, template_file.output_path)
           rendered_files[str(template_file.output_path)] = rendered_content
         except Exception as e:
           logger.error(f"Error rendering template file {template_file.relative_path}: {e}")
@@ -495,6 +498,56 @@ class Template:
               raise
           
     return rendered_files
+  
+  def _sanitize_content(self, content: str, file_path: Path) -> str:
+    """Sanitize rendered content by removing excessive blank lines.
+    
+    This function:
+    - Reduces multiple consecutive blank lines to a maximum of one blank line
+    - Preserves file structure and readability
+    - Removes trailing whitespace from lines
+    - Ensures file ends with a single newline
+    
+    Args:
+        content: The rendered content to sanitize
+        file_path: Path to the output file (used for file-type detection)
+        
+    Returns:
+        Sanitized content with cleaned up blank lines
+    """
+    if not content:
+      return content
+    
+    # Split content into lines
+    lines = content.split('\n')
+    sanitized_lines = []
+    blank_line_count = 0
+    
+    for line in lines:
+      # Remove trailing whitespace from the line
+      cleaned_line = line.rstrip()
+      
+      # Check if this is a blank line
+      if not cleaned_line:
+        blank_line_count += 1
+        # Only keep the first blank line in a sequence
+        if blank_line_count == 1:
+          sanitized_lines.append('')
+      else:
+        # Reset counter when we hit a non-blank line
+        blank_line_count = 0
+        sanitized_lines.append(cleaned_line)
+    
+    # Join lines back together
+    result = '\n'.join(sanitized_lines)
+    
+    # Remove leading blank lines
+    result = result.lstrip('\n')
+    
+    # Ensure file ends with exactly one newline
+    result = result.rstrip('\n') + '\n'
+    
+    return result
 
   def mask_sensitive_values(self, rendered_files: Dict[str, str], variables: VariableCollection) -> Dict[str, str]:
     """Mask sensitive values in rendered files using Variable's native masking."""

+ 185 - 9
cli/core/variables.py

@@ -412,6 +412,17 @@ class VariableSection:
     self.toggle: Optional[str] = data.get("toggle")
     # Default "general" section to required=True, all others to required=False
     self.required: bool = data.get("required", data["key"] == "general")
+    # Section dependencies - can be string or list of strings
+    needs_value = data.get("needs")
+    if needs_value:
+      if isinstance(needs_value, str):
+        self.needs: List[str] = [needs_value]
+      elif isinstance(needs_value, list):
+        self.needs: List[str] = needs_value
+      else:
+        raise ValueError(f"Section '{self.key}' has invalid 'needs' value: must be string or list")
+    else:
+      self.needs: List[str] = []
 
   def variable_names(self) -> list[str]:
     return list(self.variables.keys())
@@ -436,6 +447,10 @@ class VariableSection:
     # Always store required flag
     section_dict["required"] = self.required
     
+    # Store dependencies if any
+    if self.needs:
+      section_dict["needs"] = self.needs if len(self.needs) > 1 else self.needs[0]
+    
     # Serialize all variables using their own to_dict method
     section_dict["vars"] = {}
     for var_name, variable in self.variables.items():
@@ -504,6 +519,7 @@ class VariableSection:
       'description': self.description,
       'toggle': self.toggle,
       'required': self.required,
+      'needs': self.needs.copy() if self.needs else None,
     })
     
     # Deep copy all variables
@@ -558,6 +574,8 @@ class VariableCollection:
     # Variable objects contained in the _set structure.
     self._variable_map: Dict[str, Variable] = {}
     self._initialize_sections(spec)
+    # Validate dependencies after all sections are loaded
+    self._validate_dependencies()
 
   def _initialize_sections(self, spec: dict[str, Any]) -> None:
     """Initialize sections from the spec."""
@@ -578,7 +596,8 @@ class VariableCollection:
       "title": data.get("title", key.replace("_", " ").title()),
       "description": data.get("description"),
       "toggle": data.get("toggle"),
-      "required": data.get("required", key == "general")
+      "required": data.get("required", key == "general"),
+      "needs": data.get("needs")
     }
     return VariableSection(section_init_data)
 
@@ -629,19 +648,99 @@ class VariableCollection:
         f"Section '{section.key}' toggle variable '{section.toggle}' must be type 'bool', "
         f"but is type '{toggle_var.type}'"
       )
+  
+  def _validate_dependencies(self) -> None:
+    """Validate section dependencies for cycles and missing references.
+    
+    Raises:
+        ValueError: If circular dependencies or missing section references are found
+    """
+    # Check for missing dependencies
+    for section_key, section in self._sections.items():
+      for dep in section.needs:
+        if dep not in self._sections:
+          raise ValueError(
+            f"Section '{section_key}' depends on '{dep}', but '{dep}' does not exist"
+          )
+    
+    # Check for circular dependencies using depth-first search
+    visited = set()
+    rec_stack = set()
+    
+    def has_cycle(section_key: str) -> bool:
+      visited.add(section_key)
+      rec_stack.add(section_key)
+      
+      section = self._sections[section_key]
+      for dep in section.needs:
+        if dep not in visited:
+          if has_cycle(dep):
+            return True
+        elif dep in rec_stack:
+          raise ValueError(
+            f"Circular dependency detected: '{section_key}' depends on '{dep}', "
+            f"which creates a cycle"
+          )
+      
+      rec_stack.remove(section_key)
+      return False
+    
+    for section_key in self._sections:
+      if section_key not in visited:
+        has_cycle(section_key)
+  
+  def is_section_satisfied(self, section_key: str) -> bool:
+    """Check if all dependencies for a section are satisfied.
+    
+    A dependency is satisfied if:
+    1. The dependency section exists
+    2. The dependency section is enabled (if it has a toggle)
+    
+    Args:
+        section_key: The key of the section to check
+        
+    Returns:
+        True if all dependencies are satisfied, False otherwise
+    """
+    section = self._sections.get(section_key)
+    if not section:
+      return False
+    
+    # No dependencies = always satisfied
+    if not section.needs:
+      return True
+    
+    # Check each dependency
+    for dep_key in section.needs:
+      dep_section = self._sections.get(dep_key)
+      if not dep_section:
+        logger.warning(f"Section '{section_key}' depends on missing section '{dep_key}'")
+        return False
+      
+      # Check if dependency is enabled
+      if not dep_section.is_enabled():
+        logger.debug(f"Section '{section_key}' dependency '{dep_key}' is disabled")
+        return False
+    
+    return True
 
   def sort_sections(self) -> None:
     """Sort sections with the following priority:
     
-    1. Required sections first (in their original order)
-    2. Enabled sections next (in their original order)
-    3. Disabled sections last (in their original order)
+    1. Dependencies come before dependents (topological sort)
+    2. Required sections first (in their original order)
+    3. Enabled sections next (in their original order)
+    4. Disabled sections last (in their original order)
     
     This maintains the original ordering within each group while organizing
-    sections logically for display and user interaction.
+    sections logically for display and user interaction, and ensures that
+    sections are prompted in the correct dependency order.
     """
-    # Convert to list to maintain order during sorting
-    section_items = list(self._sections.items())
+    # First, perform topological sort to respect dependencies
+    sorted_keys = self._topological_sort()
+    
+    # Then apply priority sorting within dependency groups
+    section_items = [(key, self._sections[key]) for key in sorted_keys]
     
     # Define sort key: (priority, original_index)
     # Priority: 0 = required, 1 = enabled, 2 = disabled
@@ -656,6 +755,7 @@ class VariableCollection:
       return (priority, index)
     
     # Sort with original index to maintain order within each priority group
+    # Note: This preserves the topological order from earlier
     sorted_items = sorted(
       enumerate(section_items),
       key=get_sort_key
@@ -663,6 +763,44 @@ class VariableCollection:
     
     # Rebuild _sections dict in new order
     self._sections = {key: section for _, (key, section) in sorted_items}
+  
+  def _topological_sort(self) -> List[str]:
+    """Perform topological sort on sections based on dependencies.
+    
+    Uses Kahn's algorithm to ensure dependencies come before dependents.
+    Preserves original order when no dependencies exist.
+    
+    Returns:
+        List of section keys in topologically sorted order
+    """
+    # Calculate in-degree (number of dependencies) for each section
+    in_degree = {key: len(section.needs) for key, section in self._sections.items()}
+    
+    # Find all sections with no dependencies
+    queue = [key for key, degree in in_degree.items() if degree == 0]
+    result = []
+    
+    # Process sections in order
+    while queue:
+      # Sort queue to preserve original order when possible
+      queue.sort(key=lambda k: list(self._sections.keys()).index(k))
+      
+      current = queue.pop(0)
+      result.append(current)
+      
+      # Find sections that depend on current
+      for key, section in self._sections.items():
+        if current in section.needs:
+          in_degree[key] -= 1
+          if in_degree[key] == 0:
+            queue.append(key)
+    
+    # If not all sections processed, there's a cycle (shouldn't happen due to validation)
+    if len(result) != len(self._sections):
+      logger.warning("Topological sort incomplete - possible dependency cycle")
+      return list(self._sections.keys())
+    
+    return result
 
   # -------------------------
   # SECTION: Public API Methods
@@ -688,6 +826,35 @@ class VariableCollection:
     for var_name, variable in self._variable_map.items():
       all_values[var_name] = variable.get_typed_value()
     return all_values
+  
+  def get_satisfied_values(self) -> dict[str, Any]:
+    """Get variable values only from sections with satisfied dependencies.
+    
+    This respects both toggle states and section dependencies, ensuring that:
+    - Variables from disabled sections (toggle=false) are excluded
+    - Variables from sections with unsatisfied dependencies are excluded
+    
+    Returns:
+        Dictionary of variable names to values for satisfied sections only
+    """
+    satisfied_values = {}
+    
+    for section_key, section in self._sections.items():
+      # Skip sections with unsatisfied dependencies
+      if not self.is_section_satisfied(section_key):
+        logger.debug(f"Excluding variables from section '{section_key}' - dependencies not satisfied")
+        continue
+      
+      # Skip disabled sections (toggle check)
+      if not section.is_enabled():
+        logger.debug(f"Excluding variables from section '{section_key}' - section is disabled")
+        continue
+      
+      # Include all variables from this satisfied section
+      for var_name, variable in section.variables.items():
+        satisfied_values[var_name] = variable.get_typed_value()
+    
+    return satisfied_values
 
   def get_sensitive_variables(self) -> Dict[str, Any]:
     """Get only the sensitive variables with their values."""
@@ -745,10 +912,15 @@ class VariableCollection:
     return successful
   
   def validate_all(self) -> None:
-    """Validate all variables in the collection, skipping disabled sections."""
+    """Validate all variables in the collection, skipping disabled and unsatisfied sections."""
     errors: list[str] = []
 
-    for section in self._sections.values():
+    for section_key, section in self._sections.items():
+      # Skip sections with unsatisfied dependencies
+      if not self.is_section_satisfied(section_key):
+        logger.debug(f"Skipping validation for section '{section_key}' - dependencies not satisfied")
+        continue
+      
       # Check if the section is disabled by a toggle
       if section.toggle:
         toggle_var = section.variables.get(section.toggle)
@@ -873,6 +1045,9 @@ class VariableCollection:
       merged_section.toggle = other_section.toggle
     # Required flag always updated
     merged_section.required = other_section.required
+    # Needs/dependencies always updated
+    if other_section.needs:
+      merged_section.needs = other_section.needs.copy()
     
     # Merge variables
     for var_name, other_var in other_section.variables.items():
@@ -942,6 +1117,7 @@ class VariableCollection:
         'description': section.description,
         'toggle': section.toggle,
         'required': section.required,
+        'needs': section.needs.copy() if section.needs else None,
       })
       
       # Clone only the variables that should be included

+ 8 - 0
cli/modules/compose.py

@@ -95,6 +95,14 @@ spec = OrderedDict(
             "type": "str",
             "default": "web",
           },
+        },
+      },
+      "traefik_tls": {
+        "title": "Traefik TLS/SSL",
+        "toggle": "traefik_tls_enabled",
+        "needs": "traefik",
+        "description": "Enable HTTPS/TLS for Traefik with certificate management.",
+        "vars": {
           "traefik_tls_enabled": {
             "description": "Enable HTTPS/TLS",
             "type": "bool",

+ 8 - 7
library/compose/alloy/compose.yaml.j2

@@ -38,14 +38,15 @@ services:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default("alloy") }}.loadbalancer.server.port=12345
       - traefik.http.services.{{ service_name | default("alloy") }}.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ service_name | default("alloy") }}.service={{ service_name | default("alloy") }}
-      - traefik.http.routers.{{ service_name | default("alloy") }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("alloy") }}-http.service={{ service_name | default("alloy") }}
+      - traefik.http.routers.{{ service_name | default("alloy") }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("alloy") }}-http.entrypoints={{ traefik_entrypoint | default("web") }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default("alloy") }}.tls=true
-      - traefik.http.routers.{{ service_name | default("alloy") }}.entrypoints={{ traefik_tls_entrypoint | default("websecure") }}
-      - traefik.http.routers.{{ service_name | default("alloy") }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default("alloy") }}.entrypoints={{ traefik_entrypoint | default("web") }}
+      - traefik.http.routers.{{ service_name | default("alloy") }}-https.service={{ service_name | default("alloy") }}
+      - traefik.http.routers.{{ service_name | default("alloy") }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("alloy") }}-https.entrypoints={{ traefik_tls_entrypoint | default("websecure") }}
+      - traefik.http.routers.{{ service_name | default("alloy") }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default("alloy") }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default("unless-stopped") }}

+ 1 - 1
library/compose/alloy/template.yaml

@@ -14,7 +14,7 @@ metadata:
     Source: https://github.com/grafana/alloy
 
     Documentation: https://grafana.com/docs/alloy/latest/
-  version: 0.1.0
+  version: v1.10.2
   author: Christian Lempa
   date: '2025-10-02'
   tags:

+ 1 - 1
library/compose/ansiblesemaphore/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: 8.4
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/authentik/compose.yaml.j2

@@ -19,13 +19,13 @@ services:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('authentik') }}.loadbalancer.server.port=9000
       - traefik.http.services.{{ service_name | default('authentik') }}.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ service_name | default('authentik') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('authentik') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('authentik') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('authentik') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('authentik') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('authentik') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('authentik') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('authentik') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('authentik') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('authentik') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('authentik') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     volumes:

+ 1 - 1
library/compose/authentik/template.yaml

@@ -14,7 +14,7 @@ metadata:
     Documentation: https://goauthentik.io/docs/
 
     GitHub: https://github.com/goauthentik/authentik
-  version: 0.1.0
+  version: 2025.6.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/bind9/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Bind9
   description: Docker compose setup for bind9
-  version: 0.1.0
+  version: 9.20-24.10_edge
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/cadvisor/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Cadvisor
   description: Docker compose setup for cadvisor
-  version: 0.1.0
+  version: v0.52.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/checkmk/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Monitoring
   description: Docker compose setup for monitoring
-  version: 0.1.0
+  version: 2.4.0-latest
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/clamav/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Clamav
   description: Docker compose setup for clamav
-  version: 0.1.0
+  version: 1.4.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/dockge/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Dockge
   description: Docker compose setup for dockge
-  version: 0.1.0
+  version: 1.5.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/gitea/compose.yaml.j2

@@ -18,13 +18,13 @@ services:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('gitea') }}.loadbalancer.server.port=3000
       - traefik.http.services.{{ service_name | default('gitea') }}.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ service_name | default('gitea') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('gitea') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('gitea') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('gitea') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('gitea') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('gitea') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('gitea') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('gitea') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('gitea') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('gitea') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('gitea') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     volumes:

+ 1 - 1
library/compose/gitea/template.yaml

@@ -13,7 +13,7 @@ metadata:
     Documentation: https://docs.gitea.io/
 
     GitHub: https://github.com/go-gitea/gitea
-  version: 0.1.0
+  version: 1.24.5
   author: Christian Lempa
   date: '2025-10-02'
   tags:

+ 1 - 1
library/compose/gitlab-runner/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Gitlab-Runner
   description: Docker compose setup for gitlab-runner
-  version: 0.1.0
+  version: alpine-v17.9.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 20 - 10
library/compose/gitlab/compose.yaml.j2

@@ -26,19 +26,29 @@ services:
       - traefik.enable=true
       - traefik.http.services.{{ container_name }}.loadbalancer.server.port=80
       - traefik.http.services.{{ container_name }}.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ container_name }}.service={{ container_name }}
-      - traefik.http.routers.{{ container_name }}.rule=Host(`{{ traefik_host }}`)
-      - traefik.http.routers.{{ container_name }}.entrypoints={{ traefik_tls_entrypoint }}
-      - traefik.http.routers.{{ container_name }}.tls=true
-      - traefik.http.routers.{{ container_name }}.tls.certresolver={{ traefik_tls_certresolver }}
+      - traefik.http.routers.{{ container_name }}-http.service={{ container_name }}
+      - traefik.http.routers.{{ container_name }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ container_name }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
+      {% if traefik_tls_enabled %}
+      - traefik.http.routers.{{ container_name }}-https.service={{ container_name }}
+      - traefik.http.routers.{{ container_name }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ container_name }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ container_name }}-https.tls=true
+      - traefik.http.routers.{{ container_name }}-https.tls.certresolver={{ traefik_tls_certresolver }}
+      {% endif %}
 {% if registry_enabled %}
       - traefik.http.services.{{ container_name }}-registry.loadbalancer.server.port={{ registry_port }}
       - traefik.http.services.{{ container_name }}-registry.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ container_name }}-registry.service={{ container_name }}-registry
-      - traefik.http.routers.{{ container_name }}-registry.rule=Host(`{{ registry_hostname }}`)
-      - traefik.http.routers.{{ container_name }}-registry.entrypoints={{ traefik_tls_entrypoint }}
-      - traefik.http.routers.{{ container_name }}-registry.tls=true
-      - traefik.http.routers.{{ container_name }}-registry.tls.certresolver={{ traefik_tls_certresolver }}
+      - traefik.http.routers.{{ container_name }}-registry-http.service={{ container_name }}-registry
+      - traefik.http.routers.{{ container_name }}-registry-http.rule=Host(`{{ registry_hostname }}`)
+      - traefik.http.routers.{{ container_name }}-registry-http.entrypoints={{ traefik_entrypoint | default('web') }}
+      {% if traefik_tls_enabled %}
+      - traefik.http.routers.{{ container_name }}-registry-https.service={{ container_name }}-registry
+      - traefik.http.routers.{{ container_name }}-registry-https.rule=Host(`{{ registry_hostname }}`)
+      - traefik.http.routers.{{ container_name }}-registry-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ container_name }}-registry-https.tls=true
+      - traefik.http.routers.{{ container_name }}-registry-https.tls.certresolver={{ traefik_tls_certresolver }}
+      {% endif %}
 {% endif %}
 {% endif %}
     restart: {{ restart_policy }}

+ 1 - 1
library/compose/gitlab/template.yaml

@@ -13,7 +13,7 @@ metadata:
     Source: https://gitlab.com/gitlab-org/gitlab
 
     Documentation: https://docs.gitlab.com/
-  version: 0.1.0
+  version: 18.3.1-ce.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/grafana/compose.yaml.j2

@@ -21,13 +21,13 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('grafana') }}.loadbalancer.server.port=3000
-      - traefik.http.routers.{{ service_name | default('grafana') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('grafana') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('grafana') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('grafana') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('grafana') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('grafana') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('grafana') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('grafana') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('grafana') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('grafana') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('grafana') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default('unless-stopped') }}

+ 1 - 1
library/compose/grafana/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Grafana
   description: Open-source platform for monitoring and observability
-  version: 0.1.0
+  version: 12.1.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/heimdall/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Heimdall
   description: Docker compose setup for heimdall
-  version: 0.1.0
+  version: 2.7.4
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/homeassistant/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Homeassistant
   description: Docker compose setup for homeassistant
-  version: 0.1.0
+  version: 2025.8.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/homepage/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Homepage
   description: Docker compose setup for homepage
-  version: 0.1.0
+  version: v1.4.6
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/homer/compose.yaml.j2

@@ -18,13 +18,13 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('homer') }}.loadbalancer.server.port=8080
-      - traefik.http.routers.{{ service_name | default('homer') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('homer') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('homer') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('homer') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('homer') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('homer') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('homer') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('homer') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('homer') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('homer') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('homer') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default('unless-stopped') }}

+ 1 - 1
library/compose/homer/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Homer
   description: Docker compose setup for homer
-  version: 0.1.0
+  version: v25.08.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/influxdb/compose.yaml.j2

@@ -31,13 +31,13 @@ services:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('influxdb') }}.loadbalancer.server.port=8086
       - traefik.http.services.{{ service_name | default('influxdb') }}.loadbalancer.server.scheme=http
-      - traefik.http.routers.{{ service_name | default('influxdb') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('influxdb') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('influxdb') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('influxdb') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('influxdb') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('influxdb') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default('unless-stopped') }}

+ 1 - 1
library/compose/influxdb/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Influxdb
   description: Docker compose setup for influxdb
-  version: 0.1.0
+  version: 2.7.12-alpine
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/loki/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Loki
   description: Docker compose setup for loki
-  version: 0.1.0
+  version: 3.5.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/mariadb/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: 12.0.2
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 4
library/compose/n8n/compose.yaml.j2

@@ -42,11 +42,13 @@ services:
     {% if traefik_enabled %}
     labels:
       - traefik.enable=true
-      - traefik.http.routers.{{ service_name | default('n8n') }}.rule=Host(`{{ traefik_host | default('n8n.home.arpa') }}`)
+      - traefik.http.routers.{{ service_name | default('n8n') }}-http.rule=Host(`{{ traefik_host | default('n8n.home.arpa') }}`)
+      - traefik.http.routers.{{ service_name | default('n8n') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('n8n') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('n8n') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('n8n') }}.tls.certresolver={{ traefik_tls_certresolver | default('default') }}
+      - traefik.http.routers.{{ service_name | default('n8n') }}-https.rule=Host(`{{ traefik_host | default('n8n.home.arpa') }}`)
+      - traefik.http.routers.{{ service_name | default('n8n') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('n8n') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('n8n') }}-https.tls.certresolver={{ traefik_tls_certresolver | default('default') }}
       {% else %}
       - traefik.http.routers.{{ service_name | default('n8n') }}.entrypoints={{ traefik_entrypoint | default('web') }}
       {% endif %}

+ 1 - 1
library/compose/n8n/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: N8N
   description: Docker compose setup for n8n
-  version: 0.1.0
+  version: 1.110.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/nextcloud/compose.yaml.j2

@@ -29,13 +29,13 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('nextcloud') }}.loadbalancer.server.port=80
-      - traefik.http.routers.{{ service_name | default('nextcloud') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('nextcloud') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('nextcloud') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('nextcloud') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('nextcloud') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('nextcloud') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     depends_on:

+ 1 - 1
library/compose/nextcloud/template.yaml

@@ -13,7 +13,7 @@ metadata:
     Documentation: https://docs.nextcloud.com/
 
     GitHub: https://github.com/nextcloud/server
-  version: 0.1.0
+  version: 31.0.8-apache
   author: Christian Lempa
   date: '2025-10-02'
   tags:

+ 18 - 12
library/compose/nginx/compose.yaml.j2

@@ -11,11 +11,16 @@ services:
       labels:
         - traefik.enable=true
         - traefik.http.services.{{ container_name | default('nginx') }}.loadbalancer.server.port=80
-        - traefik.http.routers.{{ container_name | default('nginx') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-        - traefik.http.routers.{{ container_name | default('nginx') }}.rule=Host(`{{ traefik_host }}`)
-        - traefik.http.routers.{{ container_name | default('nginx') }}.tls={{ traefik_tls_enabled | default(true) }}
-        - traefik.http.routers.{{ container_name | default('nginx') }}.tls.certresolver={{ traefik_tls_certresolver }}
-        - traefik.http.routers.{{ container_name | default('nginx') }}.service={{ container_name | default('nginx') }}
+        - traefik.http.routers.{{ container_name | default('nginx') }}-http.rule=Host(`{{ traefik_host }}`)
+        - traefik.http.routers.{{ container_name | default('nginx') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
+        - traefik.http.routers.{{ container_name | default('nginx') }}-http.service={{ container_name | default('nginx') }}
+        {% if traefik_tls_enabled %}
+        - traefik.http.routers.{{ container_name | default('nginx') }}-https.rule=Host(`{{ traefik_host }}`)
+        - traefik.http.routers.{{ container_name | default('nginx') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+        - traefik.http.routers.{{ container_name | default('nginx') }}-https.tls=true
+        - traefik.http.routers.{{ container_name | default('nginx') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
+        - traefik.http.routers.{{ container_name | default('nginx') }}-https.service={{ container_name | default('nginx') }}
+        {% endif %}
       {% endif %}
     {% endif %}
     {% if ports_enabled %}
@@ -30,15 +35,16 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ container_name | default('nginx') }}.loadbalancer.server.port=80
-      - traefik.http.routers.{{ container_name | default('nginx') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ container_name | default('nginx') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ container_name | default('nginx') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ container_name | default('nginx') }}-http.service={{ container_name | default('nginx') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ container_name | default('nginx') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ container_name | default('nginx') }}.tls=true
-      - traefik.http.routers.{{ container_name | default('nginx') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ container_name | default('nginx') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ container_name | default('nginx') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ container_name | default('nginx') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ container_name | default('nginx') }}-https.tls=true
+      - traefik.http.routers.{{ container_name | default('nginx') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
+      - traefik.http.routers.{{ container_name | default('nginx') }}-https.service={{ container_name | default('nginx') }}
       {% endif %}
-      - traefik.http.routers.{{ container_name | default('nginx') }}.service={{ container_name | default('nginx') }}
     {% endif %}
     {% if network_enabled %}
     networks:

+ 1 - 1
library/compose/nginx/template.yaml

@@ -3,7 +3,7 @@ kind: "compose"
 metadata:
   name: "Nginx"
   description: "An open-source web server"
-  version: "0.0.1"
+  version: 1.28.0-alpine
   date: "2023-10-01"
   author: "Christian Lempa"
   tags:

+ 1 - 1
library/compose/nginxproxymanager/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: 2.12.6
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/nodeexporter/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Node_Exporter
   description: Docker compose setup for node_exporter
-  version: 0.1.0
+  version: v1.9.1
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/openwebui/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Openwebui
   description: Docker compose setup for openwebui
-  version: 0.1.0
+  version: v0.6.26
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/passbolt/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: 11.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 9 - 8
library/compose/pihole/compose.yaml.j2

@@ -26,16 +26,17 @@ services:
     {% if traefik_enabled %}
     labels:
       - traefik.enable=true
-      - traefik.http.routers.{{ service_name | default('pihole') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.services.{{ service_name | default('pihole') }}.loadBalancer.server.port=80
+      - traefik.http.routers.{{ service_name | default('pihole') }}-http.service={{ service_name | default('pihole') }}
+      - traefik.http.routers.{{ service_name | default('pihole') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('pihole') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('pihole') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('pihole') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('pihole') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('pihole') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('pihole') }}-https.service={{ service_name | default('pihole') }}
+      - traefik.http.routers.{{ service_name | default('pihole') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('pihole') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('pihole') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('pihole') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
-      - traefik.http.routers.{{ service_name | default('pihole') }}.service={{ service_name | default('pihole') }}
-      - traefik.http.services.{{ service_name | default('pihole') }}.loadBalancer.server.port=80
     {% endif %}
     restart: {{ restart_policy | default('unless-stopped') }}
 

+ 1 - 1
library/compose/pihole/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Pihole
   description: Docker compose setup for pihole
-  version: 0.1.0
+  version: 2025.08.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 8 - 7
library/compose/portainer/compose.yaml.j2

@@ -21,14 +21,15 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default('portainer') }}.loadbalancer.server.port=9000
-      - traefik.http.routers.{{ service_name | default('portainer') }}.service={{ service_name | default('portainer') }}
-      - traefik.http.routers.{{ service_name | default('portainer') }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('portainer') }}-http.service={{ service_name | default('portainer') }}
+      - traefik.http.routers.{{ service_name | default('portainer') }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('portainer') }}-http.entrypoints={{ traefik_entrypoint | default('web') }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default('portainer') }}.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
-      - traefik.http.routers.{{ service_name | default('portainer') }}.tls=true
-      - traefik.http.routers.{{ service_name | default('portainer') }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default('portainer') }}.entrypoints={{ traefik_entrypoint | default('web') }}
+      - traefik.http.routers.{{ service_name | default('portainer') }}-https.service={{ service_name | default('portainer') }}
+      - traefik.http.routers.{{ service_name | default('portainer') }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default('portainer') }}-https.entrypoints={{ traefik_tls_entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name | default('portainer') }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default('portainer') }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default('unless-stopped') }}

+ 1 - 1
library/compose/portainer/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Portainer
   description: Docker compose setup for portainer
-  version: 0.1.0
+  version: 2.33.1-alpine
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/postgres/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: PostgreSQL
   description: Advanced open-source relational database
-  version: 0.1.0
+  version: 17.6
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/prometheus/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: v3.5.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/promtail/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Promtail
   description: Docker compose setup for promtail
-  version: 0.1.0
+  version: 3.5.3
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/teleport/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Teleport
   description: Docker compose setup for teleport
-  version: 0.1.0
+  version: 18.0.2
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 3 - 1
library/compose/traefik/config/traefik.yaml.j2

@@ -21,15 +21,17 @@ api:
 entryPoints:
   {{ traefik_entrypoint }}:
     address: :80
-    {% if traefik_tls_redirect %}
+    {% if traefik_tls_enabled and traefik_tls_redirect %}
     http:
       redirections:
         entryPoint:
           to: {{ traefik_tls_entrypoint }}
           scheme: https
     {% endif %}
+  {% if traefik_tls_enabled %}
   {{ traefik_tls_entrypoint }}:
     address: :443
+  {% endif %}
 
 {% if traefik_tls_enabled %}
 certificatesResolvers:

+ 32 - 20
library/compose/traefik/template.yaml

@@ -1,10 +1,16 @@
-
 ---
-kind: "compose"
+kind: compose
 metadata:
-  name: "Traefik"
-  description: "Modern reverse proxy and load balancer for microservices"
-  version: "0.2.0"
+  name: Traefik
+  description: >
+    Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
+    This template sets up Traefik with automatic HTTPS using Let's Encrypt and can be integrated with Authentik for SSO.
+
+
+    Project: https://traefik.io/
+
+    Documentation: https://doc.traefik.io/traefik/
+  version: v3.2
   author: "Christian Lempa"
   date: "2025-10-02"
   tags:
@@ -14,7 +20,7 @@ metadata:
     - edge-router
 spec:
   general:
-    name: "General"
+    title: "General"
     required: true
     vars:
       accesslog_enabled:
@@ -23,17 +29,17 @@ spec:
         default: false
   traefik:
     title: "Traefik Settings"
-    description: "Configure Traefik as a reverse proxy with TLS/ACME support"
+    description: "Configure Traefik as a reverse proxy"
+    required: true
+  traefik_tls:
+    title: "Traefik TLS Settings"
+    description: "Configure TLS/SSL with Let's Encrypt ACME"
+    needs: "traefik"
     vars:
-      traefik_tls_acme_email:
-        type: "str"
-        description: "Email address for ACME (Let's Encrypt) registration"
-        default: "admin@example.com"
-        extra: "Required when traefik_tls_enabled is true"
-      traefik_tls_redirect:
+      traefik_tls_enabled:
         type: "bool"
-        description: "Redirect all HTTP traffic to HTTPS"
-        default: true
+        description: "Enable HTTPS/TLS with ACME"
+        default: false
       traefik_tls_acme_provider:
         type: "enum"
         description: "ACME DNS challenge provider"
@@ -47,6 +53,15 @@ spec:
         default: "your-api-token-here"
         sensitive: true
         extra: "For Cloudflare, create an API token with Zone:DNS:Edit permissions"
+      traefik_tls_acme_email:
+        type: "str"
+        description: "Email address for ACME (Let's Encrypt) registration"
+        default: "admin@example.com"
+        extra: "Required for Let's Encrypt certificate requests"
+      traefik_tls_redirect:
+        type: "bool"
+        description: "Redirect all HTTP traffic to HTTPS"
+        default: true
   ports:
     name: "Ports"
     prompt: "Expose ports via 'ports' mapping?"
@@ -68,11 +83,8 @@ spec:
       network_name:
         default: "proxy"
   authentik:
-    title: "Authentik Middleware"
-    description: >
-      Configure Authentik forward auth middleware for Traefik.
-      This creates a middleware that can be referenced in your service labels
-      as 'authentik@file' (or with your custom middleware name).
+    title: Authentik Middleware
+    description: Enable Authentik SSO integration for Traefik
     vars:
       authentik_outpost_url:
         type: "url"

+ 1 - 1
library/compose/twingate-connector/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Twingate_Connector
   description: Docker compose setup for twingate_connector
-  version: 0.1.0
+  version: 1.77.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/uptimekuma/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Volumes
   description: Docker compose setup for volumes
-  version: 0.1.0
+  version: 1.23.16
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 1 - 1
library/compose/wazuh/template.yaml

@@ -3,7 +3,7 @@ kind: compose
 metadata:
   name: Wazuh.Manager
   description: Docker compose setup for wazuh.manager
-  version: 0.1.0
+  version: 4.12.0
   author: Christian Lempa
   date: '2025-09-28'
   tags:

+ 6 - 6
library/compose/whoami/compose.yaml.j2

@@ -21,13 +21,13 @@ services:
     labels:
       - traefik.enable=true
       - traefik.http.services.{{ service_name | default("whoami") }}.loadbalancer.server.port=80
-      - traefik.http.routers.{{ service_name | default("whoami") }}.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("whoami") }}-http.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("whoami") }}-http.entrypoints={{ traefik_entrypoint | default("web") }}
       {% if traefik_tls_enabled %}
-      - traefik.http.routers.{{ service_name | default("whoami") }}.entrypoints={{ traefik_tls_entrypoint | default("websecure") }}
-      - traefik.http.routers.{{ service_name | default("whoami") }}.tls=true
-      - traefik.http.routers.{{ service_name | default("whoami") }}.tls.certresolver={{ traefik_tls_certresolver }}
-      {% else %}
-      - traefik.http.routers.{{ service_name | default("whoami") }}.entrypoints={{ traefik_entrypoint | default("web") }}
+      - traefik.http.routers.{{ service_name | default("whoami") }}-https.rule=Host(`{{ traefik_host }}`)
+      - traefik.http.routers.{{ service_name | default("whoami") }}-https.entrypoints={{ traefik_tls_entrypoint | default("websecure") }}
+      - traefik.http.routers.{{ service_name | default("whoami") }}-https.tls=true
+      - traefik.http.routers.{{ service_name | default("whoami") }}-https.tls.certresolver={{ traefik_tls_certresolver }}
       {% endif %}
     {% endif %}
     restart: {{ restart_policy | default("unless-stopped") }}