Version 0.6.0 (2026-03-25)

Changes:
- add config validation container to docker-compose
- add Make file with `up` and `down` commands
- add `default-http{,s}-port` commands to add_servers.py
- add nginx_conf.d directory usage for more than one custom nginx configurations
- rename `port` and `ssl_port` to `http{,s}_port` for templates
- add `https_custom_params` to templates
This commit is contained in:
2026-03-25 16:16:01 +03:00
parent 94259c5f99
commit 2ec44ea292
13 changed files with 266 additions and 55 deletions

View File

@@ -7,13 +7,16 @@ COPY servers.yaml /servers.yaml
COPY domains.txt /domains.txt
COPY nginx.conf.j2 /nginx.conf.j2
RUN python /add_servers.py --nginx-template /nginx.conf.j2 -o /nginx.conf --domains_list_txt /domains.txt --servers-config servers.yaml --certificates-path /ssl
RUN python /add_servers.py --nginx-template /nginx.conf.j2 -o /nginx.conf --domains-list-txt /domains.txt --servers-config servers.yaml --certificates-path /ssl
# service
FROM nginx:alpine
RUN apk add curl bash
COPY --from=builder /nginx.conf /etc/nginx/nginx.conf
COPY nginx_conf.d /etc/nginx/conf.d
COPY domains.txt /domains.txt
RUN echo "20 2 */7 * * nginx -s reload" > /etc/crontabs/root && \

View File

@@ -4,13 +4,17 @@ from __future__ import annotations
import argparse
import os
from dataclasses import dataclass, field
from dataclasses import dataclass, field, fields
from typing import Any
import jinja2
import yaml
DEFAULT_HTTP_PORT = 80
DEFAULT_HTTPS_PORT = 80
@dataclass
class Server:
"""Server entry for nginx.conf file."""
@@ -19,37 +23,25 @@ class Server:
all_names: str | None = None
proxy_pass: str | None = None
certificate_dir: str | None = None
port: int = None
ssl_port: int = None
http_port: int = None
https_port: int = None
https_custom_params: str = None
server_options: list[str] = field(default_factory=list)
location_options: list[str] = field(default_factory=list)
certificates_path: str = "/etc/letsencrypt/live"
def __post_init__(self) -> None:
if self.server_options is None:
self.server_options = []
if self.location_options is None:
self.location_options = []
if self.all_names is None:
self.all_names = self.name
if self.port is None:
self.port = 80
if self.ssl_port is None:
self.ssl_port = 443
if self.http_port is None:
self.http_port = DEFAULT_HTTP_PORT
if self.https_port is None:
self.https_port = DEFAULT_HTTPS_PORT
if self.https_custom_params is None:
self.https_custom_params = ""
def to_dict(self) -> dict:
"""Convert server class to dict for nginx.conf.j2 jinja2-template"""
return {
"name": self.name,
"all_names": self.all_names,
"proxy_pass": self.proxy_pass,
"server_options": self.server_options,
"location_options": self.location_options,
"certificate_dir": self.certificate_dir,
"port": self.port,
"ssl_port": self.ssl_port,
}
return {field.name: getattr(self, field.name) for field in fields(self)}
@dataclass
@@ -59,6 +51,8 @@ class CLIParams:
nginx_template: str
domains_list_txt: str
servers_config: str
default_http_port: str
default_https_port: str
certificates_path: str
http_only: bool
output: str | None
@@ -69,7 +63,7 @@ def main() -> None:
parser = argparse.ArgumentParser("add-servers", description="Add domain servers to a given nginx.conf file")
parser.add_argument("--nginx-template", "-f", required=True, help="Path to nginx.conf.j2 template file")
parser.add_argument(
"--domains_list_txt",
"--domains-list-txt",
"-d",
required=True,
help="Path to file with domains which have ssl certificates",
@@ -81,6 +75,18 @@ def main() -> None:
default=None,
help="Path to servers configuration yaml file",
)
parser.add_argument(
"--default-http-port",
required=False,
default=80,
help="Default port for https protocol",
)
parser.add_argument(
"--default-https-port",
required=False,
default=443,
help="Default port for https protocol",
)
parser.add_argument(
"--certificates-path",
"-c",
@@ -97,6 +103,11 @@ def main() -> None:
args: CLIParams = parser.parse_args()
global DEFAULT_HTTP_PORT
DEFAULT_HTTP_PORT = args.default_http_port
global DEFAULT_HTTPS_PORT
DEFAULT_HTTPS_PORT = args.default_https_port
if args.domains_list_txt is not None:
with open(args.domains_list_txt, "r", encoding="utf-8") as file:
domains_with_certs = [
@@ -126,12 +137,16 @@ def main() -> None:
),
proxy_pass=params.get("proxy_pass"),
certificate_dir=_get_certificate_path(
args.http_only, domains_with_certs, args.certificates_path, params.get("certificate_name") or server_name
http_only=args.http_only,
domains_with_certs=domains_with_certs,
base_certs_path=args.certificates_path,
server_name=params.get("certificate_name") or server_name,
),
port=params.get("port"),
ssl_port=params.get("ssl_port"),
server_options=params.get("server_options"),
location_options=params.get("location_options"),
https_custom_params=params.get("https_custom_params"),
http_port=params.get("http_port"),
https_port=params.get("https_port"),
server_options=params.get("server_options", []),
location_options=params.get("location_options", []),
)
)
for domain in domains_with_certs:

View File

@@ -1,8 +1,23 @@
{#- variables ~ examples: #}
{#- acme_challenge_location ~ /ssl/: #}
{#- resolver ~ 127.0.0.11: #}
{#- servers ~ ["name": ..., ("proxy_pass": ..., "server_options": ..., "location_options": ..., "all_names": ..., "port": ..., "ssl_port": ...)]: #}
{#- #}user nginx;
{# input variables ~ examples:
- acme_challenge_location ~ /ssl/:
- resolver ~ 127.0.0.11:
- servers
- name ~ doma.in,
- all_names ~ doma.in testing.doma.in
- proxy_pass ~ http://localhost:3333
- certificate_dir ~ other.doma.in
- server_options
- opt_1;
...
- location_options
- opt_1;
...
- http_port ~ 80
- https_port ~ 443
- https_custom_params ~ http2
-#}
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
@@ -31,9 +46,9 @@ http {
{%- for server in servers %}
{# #}
server {
listen {{ server["port"] or 80 }};
listen {{ server["http_port"] }};
{%- if server["certificate_dir"] is not none %}
listen {{ server["ssl_port"] or 443 }} ssl;
listen {{ server["https_port"] }} ssl {{- "" if server["ssl_custom_params"]|length == 0 else " " + server["ssl_custom_params"]}};
ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem;
ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem;
@@ -68,6 +83,9 @@ http {
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
{%- if server["location_options"]|length > 0 %}

View File

@@ -0,0 +1,126 @@
{# input variables ~ examples:
- acme_challenge_location ~ /ssl/:
- resolver ~ 127.0.0.11:
- servers
- name ~ doma.in,
- all_names ~ doma.in testing.doma.in
- proxy_pass ~ http://localhost:3333
- certificate_dir ~ other.doma.in
- server_options
- opt_1;
...
- location_options
- opt_1;
...
- http_port ~ 80
- https_port ~ 443
- https_custom_params ~ http2
-#}
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
server_tokens off;
gzip on;
proxy_connect_timeout 300;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
client_max_body_size 500M;
server {
return 404;
}
map $http_host $proxied_host {
"" $host;
default $http_host;
}
map $http_x_forwarded_port $proxied_port {
"" $server_port;
default $http_x_forwarded_port;
}
map $http_x_forwarded_host $proxied_x_host {
"" $host:$proxied_port;
default $http_x_forwarded_host;
}
map $http_x_forwarded_proto $proxied_proto {
"" $scheme;
default $http_x_forwarded_proto;
}
map $http_x_real_ip $proxied_remote_addr {
"" $remote_addr;
default $http_x_real_ip;
}
map $http_x_forwarded_for $proxied_forwarded_for {
"" $proxy_add_x_forwarded_for;
default $http_x_forwarded_for;
}
{%- for server in servers %}
{# #}
server {
listen {{ server["http_port"] or 80 }};
{%- if server["certificate_dir"] is not none %}
listen {{ server["https_port"] }} ssl {{- "" if server["ssl_custom_params"]|length == 0 else " " + server["ssl_custom_params"]}};
ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem;
ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem;
{#if ($scheme = 'http') {
return 302 https://$host$request_uri;
}#}
{%- endif %}
keepalive_timeout 70;
server_name {{ server["all_names"] or server["name"] }};
{%- if acme_challenge_location is not none %}
{# #}
location /.well-known/acme-challenge {
root {{ acme_challenge_location }};
}
{%- endif %}
{%- if server["server_options"]|length > 0 %}
{# #}
{%- for server_option in server["server_options"] %}
{{ server_option }}
{%- endfor %}
{%- endif %}
{%- if server["proxy_pass"] is not none %}
{# #}
location / {
resolver {{ resolver }};
set $host_{{ loop.index }} {{ server["proxy_pass"] }};
proxy_pass $host_{{ loop.index }};
proxy_set_header HOST $proxied_host;
proxy_set_header X-Forwarded-Host $proxied_x_host;
proxy_set_header X-Forwarded-Port $proxied_port;
proxy_set_header X-Forwarded-Proto $proxied_proto;
proxy_set_header X-Forwarded-For $proxied_forwarded_for;
proxy_set_header X-Real-IP $proxied_remote_addr;
{%- if server["location_options"]|length > 0 %}
{# #}
{%- for location_option in server["location_options"] %}
{{ location_option }}
{%- endfor %}
{%- endif %}
}
{%- endif %}
}
{%- endfor %}
}

View File

@@ -1,5 +1,5 @@
resolver: 127.0.0.1
acme_challenge_location: /etc/nginx/acme/
resolver: 127.0.0.1 # 127.0.0.11 for docker
acme_challenge_location: /etc/nginx/acme/ # /ssl for docker volume
servers:
@@ -18,8 +18,14 @@ servers:
"*.doma.in":
all_names: "*.doma.in doma.in"
certificate_name: doma.in
proxy_pass: "http://full.subdomain.proxy"
proxy_pass: "http://full.subdomain.proxy"
server_options:
- "proxy_buffering off;"
- "proxy_request_buffering off;"
- "client_max_body_size 0;"
- "http2 on;"
"custom.doma.in":
proxy_pass: "http://custom"
http_port: 4480
https_port: 4443
https_custom_params: "http2 proxy_protocol"

View File

@@ -0,0 +1,2 @@
/.gitignore
/.dockerignore

3
nginx/nginx_conf.d/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!/.gitignore
!/.dockerignore