From 5942cc225357eee4a4960e7ac82c6914d9f2f568 Mon Sep 17 00:00:00 2001 From: thelamer Date: Tue, 2 Sep 2025 15:16:05 -0400 Subject: [PATCH 1/2] initial env var ingestion for rev proxy configs --- Dockerfile | 2 + Dockerfile.aarch64 | 2 + README.md | 83 ++++++++++ readme-vars.yml | 83 ++++++++++ root/app/config-generator/generate_configs.py | 153 ++++++++++++++++++ .../templates/default.conf.j2 | 103 ++++++++++++ .../config-generator/templates/proxy.conf.j2 | 141 ++++++++++++++++ .../s6-overlay/s6-rc.d/init-swag-config/run | 17 ++ 8 files changed, 584 insertions(+) create mode 100644 root/app/config-generator/generate_configs.py create mode 100644 root/app/config-generator/templates/default.conf.j2 create mode 100644 root/app/config-generator/templates/proxy.conf.j2 diff --git a/Dockerfile b/Dockerfile index ebb6cae3..77cf3b39 100755 --- a/Dockerfile +++ b/Dockerfile @@ -79,6 +79,8 @@ RUN \ php84-tokenizer \ php84-xmlreader \ php84-xsl \ + python3 \ + py3-jinja2 \ whois && \ echo "**** install certbot plugins ****" && \ if [ -z ${CERTBOT_VERSION+x} ]; then \ diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 index 81987892..f5f4ebc1 100755 --- a/Dockerfile.aarch64 +++ b/Dockerfile.aarch64 @@ -79,6 +79,8 @@ RUN \ php84-tokenizer \ php84-xmlreader \ php84-xsl \ + python3 \ + py3-jinja2 \ whois && \ echo "**** install certbot plugins ****" && \ if [ -z ${CERTBOT_VERSION+x} ]; then \ diff --git a/README.md b/README.md index e2c13619..4994199e 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,88 @@ INSTALL_PIP_PACKAGES=certbot-dns- Set the required credentials (usually found in the plugin documentation) in `/config/dns-conf/.ini`. It is recommended to attempt obtaining a certificate with `STAGING=true` first to make sure the plugin is working as expected. + +### Dynamic Reverse Proxy Configuration via Environment Variables + +SWAG can dynamically generate reverse proxy configuration files directly from environment variables, bypassing the need to manage individual `.conf` files. When any `PROXY_CONFIG_*` variable is detected, this mode is activated, and any existing `.conf` files in `/config/nginx/proxy-confs/` will be removed at startup. + +**Service Definition** + +Each reverse proxy service is defined by an environment variable following the format `PROXY_CONFIG_`. The service name will be used as the subdomain (e.g., `SERVICE_NAME.yourdomain.com`), with the special exception of `DEFAULT` (see below). The value of the variable must be a valid JSON object. + +```yaml +environment: + # Configure the default site (root domain) to proxy to a dashboard service + - 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "authelia", "quic": true}' + + # Simple subdomain service + - 'PROXY_CONFIG_HOMARR={"port": 7575, "auth": "authelia"}' + + # Service with a boolean flag for HTTPS backend and QUIC enabled + - 'PROXY_CONFIG_HEIMDALL={"port": 443, "https": true, "quic": true}' + + # Complex service with nested objects and lists (incomplete example for syntax) + - 'PROXY_CONFIG_PLEX={ + "port": 32400, + "proxy_redirect_off": true, + "buffering_off": true, + "proxy_set_headers": [ + {"key": "X-Plex-Client-Identifier", "value": "$$http_x_plex_client_identifier"}, + {"key": "X-Plex-Device", "value": "$$http_x_plex_device"} + ], + "extra_locations": [ + {"path": "/library/streams/", "custom_directives": ["proxy_pass_request_headers off"]} + ] + }' +``` + +The available keys in the JSON object correspond to the options in the underlying Nginx template. Common keys include `port`, `https`, `quic`, `auth`, `buffering_off`, `proxy_set_headers`, and `extra_locations`. + +**Configuring the Default Site (Root Domain)** + +To configure the service that responds on your root domain (e.g., `https://yourdomain.com`), use the special service name `DEFAULT`. + +* The environment variable is `PROXY_CONFIG_DEFAULT`. +* Unlike subdomain services, the `DEFAULT` configuration **must** include a `"name"` key in its JSON value. This key specifies the name of the container that SWAG should proxy traffic to. +* If `PROXY_CONFIG_DEFAULT` is not set, the container will serve the standard SWAG welcome page on the root domain. + +Example: +```yaml +environment: + # This will proxy https://yourdomain.com to the 'dashboard' container on port 80 + - 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "none"}' +``` + +**Authentication Management** + +Authentication can be managed globally or per-service with a clear order of precedence. + +1. **Per-Service Override (Highest Priority):** Add an `auth` key directly inside the service's JSON configuration. + * `"auth": "authelia"`: Enables Authelia for this service. + * `"auth": "basic"`: Enables Basic Authentication for this service (see below). + * `"auth": "none"`: Explicitly disables authentication for this service. + +2. **Global Exclusions:** A comma-separated list of service names to exclude from the global authenticator. + * `PROXY_AUTH_EXCLUDE=ntfy,public-dashboard` + +3. **Global Default (Lowest Priority):** A single variable sets the default authentication provider for all services that don't have a per-service override and are not in the exclusion list. + * `PROXY_AUTH_PROVIDER=authelia` (can be `ldap`, `authentik`, etc.) + +**Basic Authentication** + +If you set `"auth": "basic"` for any service, you must also provide the credentials using these two environment variables. The container will automatically create the necessary `.htpasswd` file. + +* `PROXY_AUTH_BASIC_USER`: The username for basic authentication. +* `PROXY_AUTH_BASIC_PASS`: The password for basic authentication. + +Example: +```yaml +environment: + - 'PROXY_CONFIG_PORTAINER={"port": 9000, "auth": "basic"}' + - PROXY_AUTH_BASIC_USER=myadmin + - PROXY_AUTH_BASIC_PASS=supersecretpassword +``` + ### Security and password protection * The container detects changes to url and subdomains, revokes existing certs and generates new ones during start. @@ -433,6 +515,7 @@ Once registered you can define the dockerfile to use with `-f Dockerfile.aarch64 ## Versions +* **02.09.25:** - Add ability to define proxy configurations via environment variables. * **18.07.25:** - Rebase to Alpine 3.22 with PHP 8.4. Add QUIC support. Drop PHP bindings for mcrypt as it is no longer maintained. * **05.05.25:** - Disable Certbot's built in log rotation. * **19.01.25:** - Add [Auto Reload](https://github.com/linuxserver/docker-mods/tree/swag-auto-reload) functionality to SWAG. diff --git a/readme-vars.yml b/readme-vars.yml index 22c5fefb..ec6e5b03 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -82,6 +82,88 @@ app_setup_block: | Set the required credentials (usually found in the plugin documentation) in `/config/dns-conf/.ini`. It is recommended to attempt obtaining a certificate with `STAGING=true` first to make sure the plugin is working as expected. + + ### Dynamic Reverse Proxy Configuration via Environment Variables + + SWAG can dynamically generate reverse proxy configuration files directly from environment variables, bypassing the need to manage individual `.conf` files. When any `PROXY_CONFIG_*` variable is detected, this mode is activated, and any existing `.conf` files in `/config/nginx/proxy-confs/` will be removed at startup. + + **Service Definition** + + Each reverse proxy service is defined by an environment variable following the format `PROXY_CONFIG_`. The service name will be used as the subdomain (e.g., `SERVICE_NAME.yourdomain.com`), with the special exception of `DEFAULT` (see below). The value of the variable must be a valid JSON object. + + ```yaml + environment: + # Configure the default site (root domain) to proxy to a dashboard service + - 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "authelia", "quic": true}' + + # Simple subdomain service + - 'PROXY_CONFIG_HOMARR={"port": 7575, "auth": "authelia"}' + + # Service with a boolean flag for HTTPS backend and QUIC enabled + - 'PROXY_CONFIG_HEIMDALL={"port": 443, "https": true, "quic": true}' + + # Complex service with nested objects and lists (incomplete example for syntax) + - 'PROXY_CONFIG_PLEX={ + "port": 32400, + "proxy_redirect_off": true, + "buffering_off": true, + "proxy_set_headers": [ + {"key": "X-Plex-Client-Identifier", "value": "$$http_x_plex_client_identifier"}, + {"key": "X-Plex-Device", "value": "$$http_x_plex_device"} + ], + "extra_locations": [ + {"path": "/library/streams/", "custom_directives": ["proxy_pass_request_headers off"]} + ] + }' + ``` + + The available keys in the JSON object correspond to the options in the underlying Nginx template. Common keys include `port`, `https`, `quic`, `auth`, `buffering_off`, `proxy_set_headers`, and `extra_locations`. + + **Configuring the Default Site (Root Domain)** + + To configure the service that responds on your root domain (e.g., `https://yourdomain.com`), use the special service name `DEFAULT`. + + * The environment variable is `PROXY_CONFIG_DEFAULT`. + * Unlike subdomain services, the `DEFAULT` configuration **must** include a `"name"` key in its JSON value. This key specifies the name of the container that SWAG should proxy traffic to. + * If `PROXY_CONFIG_DEFAULT` is not set, the container will serve the standard SWAG welcome page on the root domain. + + Example: + ```yaml + environment: + # This will proxy https://yourdomain.com to the 'dashboard' container on port 80 + - 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "none"}' + ``` + + **Authentication Management** + + Authentication can be managed globally or per-service with a clear order of precedence. + + 1. **Per-Service Override (Highest Priority):** Add an `auth` key directly inside the service's JSON configuration. + * `"auth": "authelia"`: Enables Authelia for this service. + * `"auth": "basic"`: Enables Basic Authentication for this service (see below). + * `"auth": "none"`: Explicitly disables authentication for this service. + + 2. **Global Exclusions:** A comma-separated list of service names to exclude from the global authenticator. + * `PROXY_AUTH_EXCLUDE=ntfy,public-dashboard` + + 3. **Global Default (Lowest Priority):** A single variable sets the default authentication provider for all services that don't have a per-service override and are not in the exclusion list. + * `PROXY_AUTH_PROVIDER=authelia` (can be `ldap`, `authentik`, etc.) + + **Basic Authentication** + + If you set `"auth": "basic"` for any service, you must also provide the credentials using these two environment variables. The container will automatically create the necessary `.htpasswd` file. + + * `PROXY_AUTH_BASIC_USER`: The username for basic authentication. + * `PROXY_AUTH_BASIC_PASS`: The password for basic authentication. + + Example: + ```yaml + environment: + - 'PROXY_CONFIG_PORTAINER={"port": 9000, "auth": "basic"}' + - PROXY_AUTH_BASIC_USER=myadmin + - PROXY_AUTH_BASIC_PASS=supersecretpassword + ``` + ### Security and password protection * The container detects changes to url and subdomains, revokes existing certs and generates new ones during start. @@ -218,6 +300,7 @@ init_diagram: | "swag:latest" <- Base Images # changelog changelogs: + - {date: "02.09.25:", desc: "Add ability to define proxy configurations via environment variables."} - {date: "18.07.25:", desc: "Rebase to Alpine 3.22 with PHP 8.4. Add QUIC support. Drop PHP bindings for mcrypt as it is no longer maintained."} - {date: "05.05.25:", desc: "Disable Certbot's built in log rotation."} - {date: "19.01.25:", desc: "Add [Auto Reload](https://github.com/linuxserver/docker-mods/tree/swag-auto-reload) functionality to SWAG."} diff --git a/root/app/config-generator/generate_configs.py b/root/app/config-generator/generate_configs.py new file mode 100644 index 00000000..6b0b6434 --- /dev/null +++ b/root/app/config-generator/generate_configs.py @@ -0,0 +1,153 @@ +import os +import json +import subprocess +from jinja2 import Environment, FileSystemLoader + +# --- Configuration --- +TEMPLATE_DIR = '/app/config-generator/templates' +PROXY_OUTPUT_DIR = '/config/nginx/proxy-confs' +DEFAULT_CONF_OUTPUT = '/config/nginx/site-confs/default.conf' +HTPASSWD_FILE = '/config/nginx/.htpasswd' +# --------------------- + +def process_service_config(service_name, service_config_json, global_auth_provider, auth_exclude_list): + """Processes a single service configuration, including auth logic.""" + service_config = json.loads(service_config_json) + + # The default service doesn't have a subdomain name in the traditional sense + if service_name.lower() == 'default': + # We still need a target container name, let the user define it or raise an error + if 'name' not in service_config: + raise ValueError("PROXY_CONFIG_DEFAULT must contain a 'name' key specifying the target container name.") + else: + service_config['name'] = service_name + + # --- Authentication Logic --- + auth_provider = 'none' # Default + # 1. Per-service override + if 'auth' in service_config: + auth_provider = service_config['auth'] + print(f" - Found per-service auth override: '{auth_provider}'") + # 2. Global provider check + elif global_auth_provider and service_name not in auth_exclude_list: + auth_provider = global_auth_provider + print(f" - Applying global auth provider: '{auth_provider}'") + # 3. Otherwise, no auth + else: + if service_name in auth_exclude_list: + print(f" - Service is in global exclude list. No auth.") + else: + print(f" - No auth provider specified.") + + service_config['auth_provider'] = auth_provider + return service_config + +def generate_configs(): + """ + Generates Nginx config files from PROXY_CONFIG environment variables and a Jinja2 template. + """ + print("--- Starting Nginx Config Generation from Environment Variables ---") + + # Ensure output directories exist + os.makedirs(PROXY_OUTPUT_DIR, exist_ok=True) + os.makedirs(os.path.dirname(DEFAULT_CONF_OUTPUT), exist_ok=True) + print(f"Output directories are ready.") + + # Get global auth settings from environment variables + global_auth_provider = os.environ.get('PROXY_AUTH_PROVIDER') + auth_exclude_list = os.environ.get('PROXY_AUTH_EXCLUDE', '').split(',') + auth_exclude_list = [name.strip() for name in auth_exclude_list if name.strip()] + + # Get basic auth credentials + basic_auth_user = os.environ.get('PROXY_AUTH_BASIC_USER') + basic_auth_pass = os.environ.get('PROXY_AUTH_BASIC_PASS') + basic_auth_configured = False + + print(f"Global Auth Provider: {global_auth_provider}") + print(f"Auth Exclude List: {auth_exclude_list}") + + # Collect and process service configurations + subdomain_services = [] + default_service = None + + for key, value in os.environ.items(): + if key.startswith('PROXY_CONFIG_'): + service_name = key.replace('PROXY_CONFIG_', '').lower() + print(f" Processing service: {service_name}") + print(value) + try: + service_config = process_service_config(service_name, value, global_auth_provider, auth_exclude_list) + + # Handle Basic Auth File Creation + if service_config['auth_provider'] == 'basic' and not basic_auth_configured: + if basic_auth_user and basic_auth_pass: + print(f" - Configuring Basic Auth with user '{basic_auth_user}'.") + try: + os.makedirs(os.path.dirname(HTPASSWD_FILE), exist_ok=True) + command = ['htpasswd', '-bc', HTPASSWD_FILE, basic_auth_user, basic_auth_pass] + subprocess.run(command, check=True, capture_output=True, text=True) + print(f" - Successfully created '{HTPASSWD_FILE}'.") + basic_auth_configured = True + except subprocess.CalledProcessError as e: + print(f" [!!] ERROR: 'htpasswd' command failed: {e.stderr}. Basic auth will not be enabled.") + service_config['auth_provider'] = 'none' + except FileNotFoundError: + print(f" [!!] ERROR: 'htpasswd' command not found. Basic auth will not be enabled.") + service_config['auth_provider'] = 'none' + else: + print(f" [!!] WARNING: 'auth: basic' is set, but PROXY_AUTH_BASIC_USER or PROXY_AUTH_BASIC_PASS is missing. Skipping auth.") + service_config['auth_provider'] = 'none' + + if service_name == 'default': + default_service = service_config + else: + subdomain_services.append(service_config) + + except (json.JSONDecodeError, ValueError) as e: + print(f" [!!] ERROR: Could not parse or validate config for {service_name}: {e}. Skipping.") + except Exception as e: + print(f" [!!] ERROR: An unexpected error occurred processing {service_name}: {e}. Skipping.") + + # Set up Jinja2 environment + try: + env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), trim_blocks=True, lstrip_blocks=True) + proxy_template = env.get_template('proxy.conf.j2') + default_template = env.get_template('default.conf.j2') + print("\nJinja2 templates loaded successfully.") + except Exception as e: + print(f"ERROR: Failed to load Jinja2 templates from '{TEMPLATE_DIR}': {e}. Exiting.") + return + + # Generate default site config if specified + if default_service: + print("\n--- Generating Default Site Config ---") + try: + rendered_content = default_template.render(item=default_service) + with open(DEFAULT_CONF_OUTPUT, 'w') as f: + f.write(rendered_content) + print(f" [OK] Generated {os.path.basename(DEFAULT_CONF_OUTPUT)}") + except Exception as e: + print(f" [!!] ERROR: Failed to render or write default config: {e}") + else: + print("\n--- PROXY_CONFIG_DEFAULT not set, default site config will not be generated. ---") + + + # Generate subdomain proxy configs + print("\n--- Generating Subdomain Proxy Configs ---") + if not subdomain_services: + print("No subdomain services found to configure.") + for service in subdomain_services: + filename = f"{service['name']}.subdomain.conf" + output_path = os.path.join(PROXY_OUTPUT_DIR, filename) + try: + rendered_content = proxy_template.render(item=service) + with open(output_path, 'w') as f: + f.write(rendered_content) + print(f" [OK] Generated {filename}") + except Exception as e: + print(f" [!!] ERROR: Failed to render or write config for {service['name']}: {e}") + + print("\n--- Generation Complete ---") + +if __name__ == "__main__": + generate_configs() diff --git a/root/app/config-generator/templates/default.conf.j2 b/root/app/config-generator/templates/default.conf.j2 new file mode 100644 index 00000000..156f6135 --- /dev/null +++ b/root/app/config-generator/templates/default.conf.j2 @@ -0,0 +1,103 @@ +## Version 2025/08/28 +# THIS FILE IS AUTO-GENERATED BY THE CONTAINER. DO NOT EDIT. +# +# This is the default server block, handling requests to the root domain. + +# redirect all traffic to https +server { + listen 80 default_server; + listen [::]:80 default_server; + + location / { + return 301 https://$host$request_uri; + } +} + +# main server block +server { + listen 443 ssl default_server; +{% if item.quic %} + listen 443 quic reuseport default_server; +{% else %} +# listen 443 quic reuseport default_server; +{% endif %} + listen [::]:443 ssl default_server; +{% if item.quic %} + listen [::]:443 quic reuseport default_server; +{% else %} +# listen [::]:443 quic reuseport default_server; +{% endif %} + + server_name _; + + include /config/nginx/ssl.conf; + + client_max_body_size {{ item.client_max_body_size | default('0') }}; +{% if item.proxy_redirect_off %} + proxy_redirect off; +{% endif %} +{% if item.buffering_off %} + proxy_buffering off; +{% endif %} + +{% if item.auth_provider and item.auth_provider not in ['none', 'basic'] %} + # enable for {{ item.auth_provider }} + include /config/nginx/{{ item.auth_provider }}-server.conf; +{% endif %} + + location / { +{% if item.auth_provider == 'basic' %} + # enable for basic auth + auth_basic "Restricted"; + auth_basic_user_file /config/nginx/.htpasswd; +{% elif item.auth_provider and item.auth_provider != 'none' %} + # enable for {{ item.auth_provider }} + include /config/nginx/{{ item.auth_provider }}-location.conf; +{% else %} + # No authentication enabled for this service. +{% endif %} + + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + set $upstream_app {{ item.name }}; + set $upstream_port {{ item.port }}; + set $upstream_proto {% if item.https %}https{% else %}http{% endif %}; + proxy_pass $upstream_proto://$upstream_app:$upstream_port; +{% if item.proxy_set_headers %} +{% for header in item.proxy_set_headers %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if item.custom_directives %} +{% for directive in item.custom_directives %} + {{ directive }}; +{% endfor %} +{% endif %} + } + +{% if item.extra_locations %} +{% for loc in item.extra_locations %} + location {{ loc.path }} { + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + set $upstream_app {{ loc.app | default(item.name) }}; + set $upstream_port {{ loc.port | default(item.port) }}; + set $upstream_proto {% if loc.https %}https{% elif item.https and loc.https is not defined %}https{% else %}http{% endif %}; + proxy_pass $upstream_proto://$upstream_app:$upstream_port{% if loc.proxy_pass_path %}{{ loc.proxy_pass_path }}{% endif %}; +{% if loc.proxy_set_headers %} +{% for header in loc.proxy_set_headers %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if loc.custom_directives %} +{% for directive in loc.custom_directives %} + {{ directive }}; +{% endfor %} +{% endif %} + } +{% endfor %} +{% endif %} +} + +# enable subdomain method reverse proxy confs +include /config/nginx/proxy-confs/*.conf; diff --git a/root/app/config-generator/templates/proxy.conf.j2 b/root/app/config-generator/templates/proxy.conf.j2 new file mode 100644 index 00000000..0b7c66c2 --- /dev/null +++ b/root/app/config-generator/templates/proxy.conf.j2 @@ -0,0 +1,141 @@ +## Version 2025/08/28 +# THIS FILE IS AUTO-GENERATED BY THE CONTAINER. DO NOT EDIT. +# +# make sure that your {{ item.name }} container is named {{ item.name }} +# make sure that your dns has a cname set for {{ item.name }} + +server { + listen 443 ssl; +{% if item.quic %} + listen 443 quic reuseport; +{% else %} +# listen 443 quic reuseport; +{% endif %} + listen [::]:443 ssl; +{% if item.quic %} + listen [::]:443 quic reuseport; +{% else %} +# listen [::]:443 quic reuseport; +{% endif %} + + server_name {{ item.name }}.*; + + include /config/nginx/ssl.conf; + + client_max_body_size {{ item.client_max_body_size | default('0') }}; +{% if item.proxy_redirect_off %} + proxy_redirect off; +{% endif %} +{% if item.buffering_off %} + proxy_buffering off; +{% endif %} + +{% if item.auth_provider and item.auth_provider not in ['none', 'basic'] %} + # enable for {{ item.auth_provider }} + include /config/nginx/{{ item.auth_provider }}-server.conf; +{% endif %} + + location / { +{% if item.auth_provider == 'basic' %} + # enable for basic auth + auth_basic "Restricted"; + auth_basic_user_file /config/nginx/.htpasswd; +{% elif item.auth_provider and item.auth_provider != 'none' %} + # enable for {{ item.auth_provider }} + include /config/nginx/{{ item.auth_provider }}-location.conf; +{% else %} + # No authentication enabled for this service. +{% endif %} + + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + set $upstream_app {{ item.name }}; + set $upstream_port {{ item.port }}; + set $upstream_proto {% if item.https %}https{% else %}http{% endif %}; + proxy_pass $upstream_proto://$upstream_app:$upstream_port; +{% if item.hide_xframe %} + proxy_hide_header X-Frame-Options; +{% endif %} +{% if item.iframe_friendly %} + # Uncomment to allow loading in an iframe (i.e. Organizr) + #proxy_hide_header X-Frame-Options; +{% endif %} +{% if item.hide_x_forwarded_port %} + # Hide proxy port to prevent CSRF errors + proxy_hide_header X-Forwarded-Port; +{% endif %} +{% if item.set_x_scheme %} + proxy_set_header X-Scheme https; +{% endif %} +{% if item.websockets %} + proxy_buffering off; + proxy_socket_keepalive on; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions; + proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key; + proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version; +{% endif %} +{% if item.proxy_pass_headers %} +{% for header in item.proxy_pass_headers %} + proxy_pass_header {{ header }}; +{% endfor %} +{% endif %} +{% if item.proxy_set_headers %} +{% for header in item.proxy_set_headers %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if item.proxy_hide_headers %} +{% for header in item.proxy_hide_headers %} + proxy_hide_header {{ header }}; +{% endfor %} +{% endif %} +{% if item.add_headers %} +{% for header in item.add_headers %} + add_header {{ header.key }} "{{ header.value }}"; +{% endfor %} +{% endif %} +{% if item.custom_directives %} +{% for directive in item.custom_directives %} + {{ directive }}; +{% endfor %} +{% endif %} + } +{% if item.api %} + location ~ (?:/{{ item.name }})?/api { + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + set $upstream_app {{ item.name }}; + set $upstream_port {{ item.port }}; + set $upstream_proto {% if item.https %}https{% else %}http{% endif %}; + proxy_pass $upstream_proto://$upstream_app:$upstream_port; + } +{% endif %} +{% if item.extra_locations %} +{% for loc in item.extra_locations %} + location ~ (?:/{{ item.name }})?{{ loc.path }} { + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + set $upstream_app {{ loc.app | default(item.name) }}; + set $upstream_port {{ loc.port | default(item.port) }}; + set $upstream_proto {% if loc.https %}https{% elif item.https and loc.https is not defined %}https{% else %}http{% endif %}; + proxy_pass $upstream_proto://$upstream_app:$upstream_port{% if loc.proxy_pass_path %}{{ loc.proxy_pass_path }}{% endif %}; +{% if loc.proxy_set_headers %} +{% for header in loc.proxy_set_headers %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if loc.proxy_hide_headers %} +{% for header in loc.proxy_hide_headers %} + proxy_hide_header {{ header }}; +{% endfor %} +{% endif %} +{% if loc.custom_directives %} +{% for directive in loc.custom_directives %} + {{ directive }}; +{% endfor %} +{% endif %} + } +{% endfor %} +{% endif %} +} diff --git a/root/etc/s6-overlay/s6-rc.d/init-swag-config/run b/root/etc/s6-overlay/s6-rc.d/init-swag-config/run index b28d279b..cb90a31c 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-swag-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-swag-config/run @@ -42,3 +42,20 @@ fi if [[ ! -f /config/nginx/ldap-server.conf ]]; then cp /defaults/nginx/ldap-server.conf.sample /config/nginx/ldap-server.conf fi + +# check if any PROXY_CONFIG environment variables are set +if env | grep -q "^PROXY_CONFIG_"; then + echo "INFO: Found PROXY_CONFIG environment variables. Generating Nginx configs from environment..." + # clean the target directory to ensure a fresh start + echo "INFO: Cleaning /config/nginx/proxy-confs/ of existing files..." + rm -f /config/nginx/proxy-confs/* + # run the Python generator script + echo "INFO: Running python config generator..." + if ! python3 /app/config-generator/generate_configs.py; then + echo "ERROR: The python config generator script failed. Please check the logs above. Container will not start." + exit 1 + fi + echo "INFO: Config generation complete." +else + echo "INFO: No PROXY_CONFIG variables found. User is expected to manage /config/nginx/proxy-confs/ manually." +fi From 8c28cb7a40a41890a5ae1081ab456ebcce31d335 Mon Sep 17 00:00:00 2001 From: thelamer Date: Thu, 4 Sep 2025 15:28:25 -0400 Subject: [PATCH 2/2] make the env proxy confs their own isolated folder to include --- root/app/config-generator/generate_configs.py | 2 +- root/app/config-generator/templates/default.conf.j2 | 4 +++- root/defaults/nginx/site-confs/default.conf.sample | 2 ++ root/etc/s6-overlay/s6-rc.d/init-swag-config/run | 5 ++--- root/etc/s6-overlay/s6-rc.d/init-swag-folders/run | 1 + 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/root/app/config-generator/generate_configs.py b/root/app/config-generator/generate_configs.py index 6b0b6434..f0858ff4 100644 --- a/root/app/config-generator/generate_configs.py +++ b/root/app/config-generator/generate_configs.py @@ -5,7 +5,7 @@ # --- Configuration --- TEMPLATE_DIR = '/app/config-generator/templates' -PROXY_OUTPUT_DIR = '/config/nginx/proxy-confs' +PROXY_OUTPUT_DIR = '/config/nginx/env-proxy-confs' DEFAULT_CONF_OUTPUT = '/config/nginx/site-confs/default.conf' HTPASSWD_FILE = '/config/nginx/.htpasswd' # --------------------- diff --git a/root/app/config-generator/templates/default.conf.j2 b/root/app/config-generator/templates/default.conf.j2 index 156f6135..f318aec0 100644 --- a/root/app/config-generator/templates/default.conf.j2 +++ b/root/app/config-generator/templates/default.conf.j2 @@ -100,4 +100,6 @@ server { } # enable subdomain method reverse proxy confs -include /config/nginx/proxy-confs/*.conf; +include /config/nginx/proxy-confs/*.subdomain.conf; +# enable env var subdomain method reverse proxy confs +include /config/nginx/env-proxy-confs/*.subdomain.conf; diff --git a/root/defaults/nginx/site-confs/default.conf.sample b/root/defaults/nginx/site-confs/default.conf.sample index e2404967..5b9d20ae 100644 --- a/root/defaults/nginx/site-confs/default.conf.sample +++ b/root/defaults/nginx/site-confs/default.conf.sample @@ -82,3 +82,5 @@ server { # enable subdomain method reverse proxy confs include /config/nginx/proxy-confs/*.subdomain.conf; +# enable env var subdomain method reverse proxy confs +include /config/nginx/env-proxy-confs/*.subdomain.conf; diff --git a/root/etc/s6-overlay/s6-rc.d/init-swag-config/run b/root/etc/s6-overlay/s6-rc.d/init-swag-config/run index cb90a31c..73d34d45 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-swag-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-swag-config/run @@ -43,12 +43,11 @@ if [[ ! -f /config/nginx/ldap-server.conf ]]; then cp /defaults/nginx/ldap-server.conf.sample /config/nginx/ldap-server.conf fi +# clean the env target directory to ensure a fresh start +rm -f /config/nginx/env-proxy-confs/* # check if any PROXY_CONFIG environment variables are set if env | grep -q "^PROXY_CONFIG_"; then echo "INFO: Found PROXY_CONFIG environment variables. Generating Nginx configs from environment..." - # clean the target directory to ensure a fresh start - echo "INFO: Cleaning /config/nginx/proxy-confs/ of existing files..." - rm -f /config/nginx/proxy-confs/* # run the Python generator script echo "INFO: Running python config generator..." if ! python3 /app/config-generator/generate_configs.py; then diff --git a/root/etc/s6-overlay/s6-rc.d/init-swag-folders/run b/root/etc/s6-overlay/s6-rc.d/init-swag-folders/run index 9de179dd..5ab7594b 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-swag-folders/run +++ b/root/etc/s6-overlay/s6-rc.d/init-swag-folders/run @@ -6,6 +6,7 @@ mkdir -p \ /config/{fail2ban,dns-conf} \ /config/etc/letsencrypt/renewal-hooks \ /config/log/{fail2ban,letsencrypt,nginx} \ + /config/nginx/env-proxy-confs \ /config/nginx/proxy-confs \ /run/fail2ban \ /tmp/letsencrypt