Skip to content

Commit 5942cc2

Browse files
committed
initial env var ingestion for rev proxy configs
1 parent fb4ba0d commit 5942cc2

8 files changed

Lines changed: 584 additions & 0 deletions

File tree

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ RUN \
7979
php84-tokenizer \
8080
php84-xmlreader \
8181
php84-xsl \
82+
python3 \
83+
py3-jinja2 \
8284
whois && \
8385
echo "**** install certbot plugins ****" && \
8486
if [ -z ${CERTBOT_VERSION+x} ]; then \

Dockerfile.aarch64

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ RUN \
7979
php84-tokenizer \
8080
php84-xmlreader \
8181
php84-xsl \
82+
python3 \
83+
py3-jinja2 \
8284
whois && \
8385
echo "**** install certbot plugins ****" && \
8486
if [ -z ${CERTBOT_VERSION+x} ]; then \

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,88 @@ INSTALL_PIP_PACKAGES=certbot-dns-<plugin>
8585
Set the required credentials (usually found in the plugin documentation) in `/config/dns-conf/<plugin>.ini`.
8686
It is recommended to attempt obtaining a certificate with `STAGING=true` first to make sure the plugin is working as expected.
8787

88+
89+
### Dynamic Reverse Proxy Configuration via Environment Variables
90+
91+
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.
92+
93+
**Service Definition**
94+
95+
Each reverse proxy service is defined by an environment variable following the format `PROXY_CONFIG_<SERVICE_NAME>`. 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.
96+
97+
```yaml
98+
environment:
99+
# Configure the default site (root domain) to proxy to a dashboard service
100+
- 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "authelia", "quic": true}'
101+
102+
# Simple subdomain service
103+
- 'PROXY_CONFIG_HOMARR={"port": 7575, "auth": "authelia"}'
104+
105+
# Service with a boolean flag for HTTPS backend and QUIC enabled
106+
- 'PROXY_CONFIG_HEIMDALL={"port": 443, "https": true, "quic": true}'
107+
108+
# Complex service with nested objects and lists (incomplete example for syntax)
109+
- 'PROXY_CONFIG_PLEX={
110+
"port": 32400,
111+
"proxy_redirect_off": true,
112+
"buffering_off": true,
113+
"proxy_set_headers": [
114+
{"key": "X-Plex-Client-Identifier", "value": "$$http_x_plex_client_identifier"},
115+
{"key": "X-Plex-Device", "value": "$$http_x_plex_device"}
116+
],
117+
"extra_locations": [
118+
{"path": "/library/streams/", "custom_directives": ["proxy_pass_request_headers off"]}
119+
]
120+
}'
121+
```
122+
123+
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`.
124+
125+
**Configuring the Default Site (Root Domain)**
126+
127+
To configure the service that responds on your root domain (e.g., `https://yourdomain.com`), use the special service name `DEFAULT`.
128+
129+
* The environment variable is `PROXY_CONFIG_DEFAULT`.
130+
* 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.
131+
* If `PROXY_CONFIG_DEFAULT` is not set, the container will serve the standard SWAG welcome page on the root domain.
132+
133+
Example:
134+
```yaml
135+
environment:
136+
# This will proxy https://yourdomain.com to the 'dashboard' container on port 80
137+
- 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "none"}'
138+
```
139+
140+
**Authentication Management**
141+
142+
Authentication can be managed globally or per-service with a clear order of precedence.
143+
144+
1. **Per-Service Override (Highest Priority):** Add an `auth` key directly inside the service's JSON configuration.
145+
* `"auth": "authelia"`: Enables Authelia for this service.
146+
* `"auth": "basic"`: Enables Basic Authentication for this service (see below).
147+
* `"auth": "none"`: Explicitly disables authentication for this service.
148+
149+
2. **Global Exclusions:** A comma-separated list of service names to exclude from the global authenticator.
150+
* `PROXY_AUTH_EXCLUDE=ntfy,public-dashboard`
151+
152+
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.
153+
* `PROXY_AUTH_PROVIDER=authelia` (can be `ldap`, `authentik`, etc.)
154+
155+
**Basic Authentication**
156+
157+
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.
158+
159+
* `PROXY_AUTH_BASIC_USER`: The username for basic authentication.
160+
* `PROXY_AUTH_BASIC_PASS`: The password for basic authentication.
161+
162+
Example:
163+
```yaml
164+
environment:
165+
- 'PROXY_CONFIG_PORTAINER={"port": 9000, "auth": "basic"}'
166+
- PROXY_AUTH_BASIC_USER=myadmin
167+
- PROXY_AUTH_BASIC_PASS=supersecretpassword
168+
```
169+
88170
### Security and password protection
89171

90172
* 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
433515

434516
## Versions
435517

518+
* **02.09.25:** - Add ability to define proxy configurations via environment variables.
436519
* **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.
437520
* **05.05.25:** - Disable Certbot's built in log rotation.
438521
* **19.01.25:** - Add [Auto Reload](https://github.com/linuxserver/docker-mods/tree/swag-auto-reload) functionality to SWAG.

readme-vars.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,88 @@ app_setup_block: |
8282
Set the required credentials (usually found in the plugin documentation) in `/config/dns-conf/<plugin>.ini`.
8383
It is recommended to attempt obtaining a certificate with `STAGING=true` first to make sure the plugin is working as expected.
8484
85+
86+
### Dynamic Reverse Proxy Configuration via Environment Variables
87+
88+
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.
89+
90+
**Service Definition**
91+
92+
Each reverse proxy service is defined by an environment variable following the format `PROXY_CONFIG_<SERVICE_NAME>`. 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.
93+
94+
```yaml
95+
environment:
96+
# Configure the default site (root domain) to proxy to a dashboard service
97+
- 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "authelia", "quic": true}'
98+
99+
# Simple subdomain service
100+
- 'PROXY_CONFIG_HOMARR={"port": 7575, "auth": "authelia"}'
101+
102+
# Service with a boolean flag for HTTPS backend and QUIC enabled
103+
- 'PROXY_CONFIG_HEIMDALL={"port": 443, "https": true, "quic": true}'
104+
105+
# Complex service with nested objects and lists (incomplete example for syntax)
106+
- 'PROXY_CONFIG_PLEX={
107+
"port": 32400,
108+
"proxy_redirect_off": true,
109+
"buffering_off": true,
110+
"proxy_set_headers": [
111+
{"key": "X-Plex-Client-Identifier", "value": "$$http_x_plex_client_identifier"},
112+
{"key": "X-Plex-Device", "value": "$$http_x_plex_device"}
113+
],
114+
"extra_locations": [
115+
{"path": "/library/streams/", "custom_directives": ["proxy_pass_request_headers off"]}
116+
]
117+
}'
118+
```
119+
120+
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`.
121+
122+
**Configuring the Default Site (Root Domain)**
123+
124+
To configure the service that responds on your root domain (e.g., `https://yourdomain.com`), use the special service name `DEFAULT`.
125+
126+
* The environment variable is `PROXY_CONFIG_DEFAULT`.
127+
* 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.
128+
* If `PROXY_CONFIG_DEFAULT` is not set, the container will serve the standard SWAG welcome page on the root domain.
129+
130+
Example:
131+
```yaml
132+
environment:
133+
# This will proxy https://yourdomain.com to the 'dashboard' container on port 80
134+
- 'PROXY_CONFIG_DEFAULT={"name": "dashboard", "port": 80, "auth": "none"}'
135+
```
136+
137+
**Authentication Management**
138+
139+
Authentication can be managed globally or per-service with a clear order of precedence.
140+
141+
1. **Per-Service Override (Highest Priority):** Add an `auth` key directly inside the service's JSON configuration.
142+
* `"auth": "authelia"`: Enables Authelia for this service.
143+
* `"auth": "basic"`: Enables Basic Authentication for this service (see below).
144+
* `"auth": "none"`: Explicitly disables authentication for this service.
145+
146+
2. **Global Exclusions:** A comma-separated list of service names to exclude from the global authenticator.
147+
* `PROXY_AUTH_EXCLUDE=ntfy,public-dashboard`
148+
149+
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.
150+
* `PROXY_AUTH_PROVIDER=authelia` (can be `ldap`, `authentik`, etc.)
151+
152+
**Basic Authentication**
153+
154+
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.
155+
156+
* `PROXY_AUTH_BASIC_USER`: The username for basic authentication.
157+
* `PROXY_AUTH_BASIC_PASS`: The password for basic authentication.
158+
159+
Example:
160+
```yaml
161+
environment:
162+
- 'PROXY_CONFIG_PORTAINER={"port": 9000, "auth": "basic"}'
163+
- PROXY_AUTH_BASIC_USER=myadmin
164+
- PROXY_AUTH_BASIC_PASS=supersecretpassword
165+
```
166+
85167
### Security and password protection
86168
87169
* The container detects changes to url and subdomains, revokes existing certs and generates new ones during start.
@@ -218,6 +300,7 @@ init_diagram: |
218300
"swag:latest" <- Base Images
219301
# changelog
220302
changelogs:
303+
- {date: "02.09.25:", desc: "Add ability to define proxy configurations via environment variables."}
221304
- {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."}
222305
- {date: "05.05.25:", desc: "Disable Certbot's built in log rotation."}
223306
- {date: "19.01.25:", desc: "Add [Auto Reload](https://github.com/linuxserver/docker-mods/tree/swag-auto-reload) functionality to SWAG."}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import os
2+
import json
3+
import subprocess
4+
from jinja2 import Environment, FileSystemLoader
5+
6+
# --- Configuration ---
7+
TEMPLATE_DIR = '/app/config-generator/templates'
8+
PROXY_OUTPUT_DIR = '/config/nginx/proxy-confs'
9+
DEFAULT_CONF_OUTPUT = '/config/nginx/site-confs/default.conf'
10+
HTPASSWD_FILE = '/config/nginx/.htpasswd'
11+
# ---------------------
12+
13+
def process_service_config(service_name, service_config_json, global_auth_provider, auth_exclude_list):
14+
"""Processes a single service configuration, including auth logic."""
15+
service_config = json.loads(service_config_json)
16+
17+
# The default service doesn't have a subdomain name in the traditional sense
18+
if service_name.lower() == 'default':
19+
# We still need a target container name, let the user define it or raise an error
20+
if 'name' not in service_config:
21+
raise ValueError("PROXY_CONFIG_DEFAULT must contain a 'name' key specifying the target container name.")
22+
else:
23+
service_config['name'] = service_name
24+
25+
# --- Authentication Logic ---
26+
auth_provider = 'none' # Default
27+
# 1. Per-service override
28+
if 'auth' in service_config:
29+
auth_provider = service_config['auth']
30+
print(f" - Found per-service auth override: '{auth_provider}'")
31+
# 2. Global provider check
32+
elif global_auth_provider and service_name not in auth_exclude_list:
33+
auth_provider = global_auth_provider
34+
print(f" - Applying global auth provider: '{auth_provider}'")
35+
# 3. Otherwise, no auth
36+
else:
37+
if service_name in auth_exclude_list:
38+
print(f" - Service is in global exclude list. No auth.")
39+
else:
40+
print(f" - No auth provider specified.")
41+
42+
service_config['auth_provider'] = auth_provider
43+
return service_config
44+
45+
def generate_configs():
46+
"""
47+
Generates Nginx config files from PROXY_CONFIG environment variables and a Jinja2 template.
48+
"""
49+
print("--- Starting Nginx Config Generation from Environment Variables ---")
50+
51+
# Ensure output directories exist
52+
os.makedirs(PROXY_OUTPUT_DIR, exist_ok=True)
53+
os.makedirs(os.path.dirname(DEFAULT_CONF_OUTPUT), exist_ok=True)
54+
print(f"Output directories are ready.")
55+
56+
# Get global auth settings from environment variables
57+
global_auth_provider = os.environ.get('PROXY_AUTH_PROVIDER')
58+
auth_exclude_list = os.environ.get('PROXY_AUTH_EXCLUDE', '').split(',')
59+
auth_exclude_list = [name.strip() for name in auth_exclude_list if name.strip()]
60+
61+
# Get basic auth credentials
62+
basic_auth_user = os.environ.get('PROXY_AUTH_BASIC_USER')
63+
basic_auth_pass = os.environ.get('PROXY_AUTH_BASIC_PASS')
64+
basic_auth_configured = False
65+
66+
print(f"Global Auth Provider: {global_auth_provider}")
67+
print(f"Auth Exclude List: {auth_exclude_list}")
68+
69+
# Collect and process service configurations
70+
subdomain_services = []
71+
default_service = None
72+
73+
for key, value in os.environ.items():
74+
if key.startswith('PROXY_CONFIG_'):
75+
service_name = key.replace('PROXY_CONFIG_', '').lower()
76+
print(f" Processing service: {service_name}")
77+
print(value)
78+
try:
79+
service_config = process_service_config(service_name, value, global_auth_provider, auth_exclude_list)
80+
81+
# Handle Basic Auth File Creation
82+
if service_config['auth_provider'] == 'basic' and not basic_auth_configured:
83+
if basic_auth_user and basic_auth_pass:
84+
print(f" - Configuring Basic Auth with user '{basic_auth_user}'.")
85+
try:
86+
os.makedirs(os.path.dirname(HTPASSWD_FILE), exist_ok=True)
87+
command = ['htpasswd', '-bc', HTPASSWD_FILE, basic_auth_user, basic_auth_pass]
88+
subprocess.run(command, check=True, capture_output=True, text=True)
89+
print(f" - Successfully created '{HTPASSWD_FILE}'.")
90+
basic_auth_configured = True
91+
except subprocess.CalledProcessError as e:
92+
print(f" [!!] ERROR: 'htpasswd' command failed: {e.stderr}. Basic auth will not be enabled.")
93+
service_config['auth_provider'] = 'none'
94+
except FileNotFoundError:
95+
print(f" [!!] ERROR: 'htpasswd' command not found. Basic auth will not be enabled.")
96+
service_config['auth_provider'] = 'none'
97+
else:
98+
print(f" [!!] WARNING: 'auth: basic' is set, but PROXY_AUTH_BASIC_USER or PROXY_AUTH_BASIC_PASS is missing. Skipping auth.")
99+
service_config['auth_provider'] = 'none'
100+
101+
if service_name == 'default':
102+
default_service = service_config
103+
else:
104+
subdomain_services.append(service_config)
105+
106+
except (json.JSONDecodeError, ValueError) as e:
107+
print(f" [!!] ERROR: Could not parse or validate config for {service_name}: {e}. Skipping.")
108+
except Exception as e:
109+
print(f" [!!] ERROR: An unexpected error occurred processing {service_name}: {e}. Skipping.")
110+
111+
# Set up Jinja2 environment
112+
try:
113+
env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), trim_blocks=True, lstrip_blocks=True)
114+
proxy_template = env.get_template('proxy.conf.j2')
115+
default_template = env.get_template('default.conf.j2')
116+
print("\nJinja2 templates loaded successfully.")
117+
except Exception as e:
118+
print(f"ERROR: Failed to load Jinja2 templates from '{TEMPLATE_DIR}': {e}. Exiting.")
119+
return
120+
121+
# Generate default site config if specified
122+
if default_service:
123+
print("\n--- Generating Default Site Config ---")
124+
try:
125+
rendered_content = default_template.render(item=default_service)
126+
with open(DEFAULT_CONF_OUTPUT, 'w') as f:
127+
f.write(rendered_content)
128+
print(f" [OK] Generated {os.path.basename(DEFAULT_CONF_OUTPUT)}")
129+
except Exception as e:
130+
print(f" [!!] ERROR: Failed to render or write default config: {e}")
131+
else:
132+
print("\n--- PROXY_CONFIG_DEFAULT not set, default site config will not be generated. ---")
133+
134+
135+
# Generate subdomain proxy configs
136+
print("\n--- Generating Subdomain Proxy Configs ---")
137+
if not subdomain_services:
138+
print("No subdomain services found to configure.")
139+
for service in subdomain_services:
140+
filename = f"{service['name']}.subdomain.conf"
141+
output_path = os.path.join(PROXY_OUTPUT_DIR, filename)
142+
try:
143+
rendered_content = proxy_template.render(item=service)
144+
with open(output_path, 'w') as f:
145+
f.write(rendered_content)
146+
print(f" [OK] Generated {filename}")
147+
except Exception as e:
148+
print(f" [!!] ERROR: Failed to render or write config for {service['name']}: {e}")
149+
150+
print("\n--- Generation Complete ---")
151+
152+
if __name__ == "__main__":
153+
generate_configs()

0 commit comments

Comments
 (0)