update 2025-04-06

Changes:
- fix README.md old json format usage
- rename "redirect" option to "proxy_pass"
- move docker-compose.yml and nginx.conf.j2 to examples and add to .gitignore
- fix situation when one domain from domains.txt and servers.yaml appeared twice in nginx.conf
This commit is contained in:
2025-04-06 22:16:20 +03:00
parent 16bc1c0db7
commit 7499e55ce6
10 changed files with 132 additions and 125 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,7 @@
/nginx/domains.txt /nginx/domains.txt
/nginx/servers.json
/nginx/servers.yaml /nginx/servers.yaml
/nginx/nginx.conf.j2
/nginx.conf
*.env *.env
/.venv
/docker-compose.y*ml

View File

@@ -2,19 +2,17 @@
## Preparation ## Preparation
Add your domains for _certbot_ to [domains.txt file at nginx directory](nginx/domains.txt) and servers configuration 1. Copy and edit (if needed) [nginx/nginx.conf.j2](nginx/nginx.conf.j2.example) file.
json file in format 2. Add your certified domains to nginx/domains.txt file ([example](nginx/domains.txt.example)).
```json
{ These domains will be used by _certbot_ to monitor and update (if possible) certificates. _Nginx_ will also setup
"domain": { http server for the given entries.
"redirect": "redirection_path",
"options": [ However if the domain is set both in domains.txt and servers.yaml (next step), _nginx_ will
"some_nginx_option_on_server_level here;" use https with certificates at the given path. That will fail nginx startup if the certificate
] to at least one domain is missing (--http-only option will skip domains.txt check).
}
} 3. Add your servers section configuration to nginx/servers.yaml ([example](nginx/servers.yaml.example)).
```
to [servers.json file at nginx directory](nginx/servers.json).
User email used for certbot can be set as environment variable at build process or in .env file. User email used for certbot can be set as environment variable at build process or in .env file.

View File

@@ -23,16 +23,10 @@ services:
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
networks:
- hosting_net
restart: unless-stopped restart: unless-stopped
volumes: volumes:
ssl: ssl:
name: ssl_nginx_ssl name: ssl_nginx_ssl
letsencrypt: letsencrypt:
name: ssl_nginx_letsencrypt name: ssl_nginx_letsencrypt
networks:
hosting_net:
external: true

View File

@@ -1,4 +1,4 @@
FROM python:3.11-alpine as builder FROM python:3.11-alpine AS builder
RUN pip3 install --no-cache-dir pyyaml jinja2 RUN pip3 install --no-cache-dir pyyaml jinja2
@@ -16,12 +16,11 @@ FROM nginx:alpine
COPY --from=builder /nginx.conf /etc/nginx/nginx.conf COPY --from=builder /nginx.conf /etc/nginx/nginx.conf
COPY domains.txt /domains.txt COPY domains.txt /domains.txt
RUN echo "16 2 */7 * * nginx -s reload" > /etc/crontabs/root && \ RUN echo "20 2 */7 * * nginx -s reload" > /etc/crontabs/root && \
\ \
echo "cp /domains.txt /ssl/domains.txt" > /entrypoint && \ echo "cp /domains.txt /ssl/domains.txt" > /entrypoint && \
echo "crond" >> /entrypoint && \ echo "crond" >> /entrypoint && \
echo "nginx -g 'daemon off;'" >> /entrypoint && \ echo "nginx -g 'daemon off;'" >> /entrypoint
echo "nginx" >> /entrypoint
ENTRYPOINT ["/bin/sh"] ENTRYPOINT ["/bin/sh"]
CMD ["/entrypoint"] CMD ["/entrypoint"]

View File

@@ -1,4 +1,5 @@
"""Add domain servers to nginx.conf executable script.""" """Add domain servers to nginx.conf executable script."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
@@ -16,7 +17,7 @@ class Server:
name: str name: str
all_names: str | None = None all_names: str | None = None
redirect: str | None = None proxy_pass: str | None = None
certificate_dir: str | None = None certificate_dir: str | None = None
port: int = None port: int = None
ssl_port: int = None ssl_port: int = None
@@ -42,7 +43,7 @@ class Server:
return { return {
"name": self.name, "name": self.name,
"all_names": self.all_names, "all_names": self.all_names,
"redirect": self.redirect, "proxy_pass": self.proxy_pass,
"server_options": self.server_options, "server_options": self.server_options,
"location_options": self.location_options, "location_options": self.location_options,
"certificate_dir": self.certificate_dir, "certificate_dir": self.certificate_dir,
@@ -105,19 +106,25 @@ def main() -> None:
domains_with_certs = [] domains_with_certs = []
nginx_servers: list[Server] = [] nginx_servers: list[Server] = []
resolver: str = "127.0.0.1"
acme_challenge_location: str | None = None
if args.servers_config is not None: if args.servers_config is not None:
with open(args.servers_config, "r", encoding="utf-8") as file: with open(args.servers_config, "r", encoding="utf-8") as file:
data: dict = yaml.safe_load(file) data: dict = yaml.safe_load(file)
resolver: str = data.get("resolver", "127.0.0.1") resolver: str = data.get("resolver", resolver)
acme_challenge_location: str | None = data.get("acme_challenge_location") acme_challenge_location = data.get("acme_challenge_location")
servers: dict[str, dict[str, Any]] = data["servers"] servers: dict[str, dict[str, Any]] = data["servers"]
for server_name, params in servers.items(): for server_name, params in servers.items():
nginx_servers.append( nginx_servers.append(
Server( Server(
name=(server_name if "*" not in server_name else f"{server_name} {server_name.replace('*.', '')}"), name=server_name,
all_names=params.get("all_names"), all_names=params.get(
redirect=params.get("redirect"), "all_names",
None if "*" not in server_name else f"{server_name} {server_name.replace('*.', '', 1)}",
),
proxy_pass=params.get("proxy_pass"),
certificate_dir=_get_certificate_path( certificate_dir=_get_certificate_path(
args.http_only, domains_with_certs, args.certificates_path, server_name args.http_only, domains_with_certs, args.certificates_path, server_name
), ),
@@ -130,8 +137,12 @@ def main() -> None:
for domain in domains_with_certs: for domain in domains_with_certs:
if not any( if not any(
( (
domain == server.name (server.all_names is None and domain == server.name)
or (domain == f"*.{server.name[server.name.find('.') + 1:]}" if "." in server.name else False) or (
server.all_names is not None
and f" {domain}" in server.all_names
or server.all_names.startswith(domain)
)
) )
for server in nginx_servers for server in nginx_servers
): ):

View File

@@ -1,5 +1,5 @@
your.domain.to_redirect your.domain.to_listen
your_other.doma.in your_other.doma.in
*.doma.in *.doma.in
#commented.domain #commented.domain
domain.without.redirect domain.without.proxy

View File

@@ -1,4 +1,4 @@
FROM python:3.10-alpine as builder FROM python:3.10-alpine AS builder
COPY add_servers.py /add_servers.py COPY add_servers.py /add_servers.py
COPY domains.txt /domains.txt COPY domains.txt /domains.txt
@@ -18,8 +18,7 @@ COPY domains.txt /domains.txt
RUN echo "(sleep 120 && killall nginx) &" > /entrypoint && \ RUN echo "(sleep 120 && killall nginx) &" > /entrypoint && \
echo "cp /domains.txt /ssl/domains.txt" >> /entrypoint && \ echo "cp /domains.txt /ssl/domains.txt" >> /entrypoint && \
echo "nginx -g 'daemon off;'" >> /entrypoint && \ echo "nginx -g 'daemon off;'" >> /entrypoint
echo "nginx" >> /entrypoint
ENTRYPOINT ["/bin/sh"] ENTRYPOINT ["/bin/sh"]
CMD ["/entrypoint"] CMD ["/entrypoint"]

View File

@@ -1,82 +1,83 @@
{#- variables ~ examples: #} {#- variables ~ examples: #}
{#- acme_challenge_location ~ /ssl/: #} {#- acme_challenge_location ~ /ssl/: #}
{#- resolver ~ 127.0.0.11: #} {#- resolver ~ 127.0.0.11: #}
{#- servers ~ ["name": ..., ("redirect": ..., "server_options": ..., "location_options": ..., "all_names": ..., "port": ..., "ssl_port": ...)]: #} {#- servers ~ ["name": ..., ("proxy_pass": ..., "server_options": ..., "location_options": ..., "all_names": ..., "port": ..., "ssl_port": ...)]: #}
{#- #}user nginx; {#- #}user nginx;
worker_processes auto; worker_processes auto;
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m; ssl_session_timeout 10m;
server_tokens off; server_tokens off;
gzip on; gzip on;
proxy_connect_timeout 300; proxy_connect_timeout 300;
proxy_send_timeout 600; proxy_send_timeout 600;
proxy_read_timeout 600; proxy_read_timeout 600;
send_timeout 600; send_timeout 600;
client_max_body_size 500M; client_max_body_size 500M;
server { server {
return 404; return 404;
} }
{%- for server in servers %} {%- for server in servers %}
server { {# #}
listen {{ server["port"] or 80 }}; server {
{%- if server["certificate_dir"] is not none %} listen {{ server["port"] or 80 }};
listen {{ server["ssl_port"] or 443 }} ssl; {%- if server["certificate_dir"] is not none %}
{%- endif %} listen {{ server["ssl_port"] or 443 }} ssl;
keepalive_timeout 70; ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem;
ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem;
server_name {{ server["all_names"] or server["name"] }};
if ($scheme = 'http') {
{%- if acme_challenge_location is defined %} return 302 https://$host$request_uri;
{# #} }
location /.well-known/acme-challenge { {%- endif %}
root {{ acme_challenge_location }}; keepalive_timeout 70;
}
{%- endif %} server_name {{ server["all_names"] or server["name"] }};
{%- if server["server_options"]|length > 0 %} {%- if acme_challenge_location is not none %}
{# #} {# #}
{%- for server_option in server["server_options"] %} location /.well-known/acme-challenge {
{{ server_option }} root {{ acme_challenge_location }};
{%- endfor %} }
{%- endif %} {%- endif %}
{%- if server["certificate_dir"] is not none %} {%- if server["server_options"]|length > 0 %}
{# #} {# #}
ssl_certificate {{ server["certificate_dir"] }}/fullchain.pem; {%- for server_option in server["server_options"] %}
ssl_certificate_key {{ server["certificate_dir"] }}/privkey.pem; {{ server_option }}
{%- endif %} {%- endfor %}
{%- endif %}
{%- if server["redirect"] is not none %}
{# #} {%- if server["proxy_pass"] is not none %}
location / { {# #}
resolver {{ resolver }}; location / {
set $host_{{ loop.index }} {{ server["redirect"] }}; resolver {{ resolver }};
proxy_pass $host_{{ loop.index }}; set $host_{{ loop.index }} {{ server["proxy_pass"] }};
proxy_pass $host_{{ loop.index }};
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
{%- if server["location_options"]|length > 0 %}
{# #} {%- if server["location_options"]|length > 0 %}
{%- for location_option in server["location_options"] %} {# #}
{{ location_option }} {%- for location_option in server["location_options"] %}
{%- endfor %} {{ location_option }}
{%- endif %} {%- endfor %}
} {%- endif %}
{%- endif %} }
} {%- endif %}
{%- endfor %} }
} {%- endfor %}
}

View File

@@ -1,11 +1,12 @@
resolver: 127.0.0.1 resolver: 127.0.0.1
acme_challenge_location: /etc/nginx/acme/ acme_challenge_location: /etc/nginx/acme/
servers: servers:
your.domain.to_redirect: your.domain.to_redirect:
redirect: "http://redirection.address" proxy_pass: "http://redirection.address"
your_other.doma.in: your_other.doma.in:
redirect: "http://redirection-other.address" proxy_pass: "http://redirection-other.address"
server_options: server_options:
- "proxy_buffering off;" - "proxy_buffering off;"
- "proxy_request_buffering off;" - "proxy_request_buffering off;"
@@ -16,7 +17,7 @@ servers:
- "proxy_read_timeout 86400;" - "proxy_read_timeout 86400;"
"*.doma.in": "*.doma.in":
all_names: "*.doma.in doma.in" all_names: "*.doma.in doma.in"
redirect: "http://full.subdomain.redirect" proxy_pass: "http://full.subdomain.proxy"
server_options: server_options:
- "proxy_buffering off;" - "proxy_buffering off;"
- "proxy_request_buffering off;" - "proxy_request_buffering off;"

View File

@@ -1,5 +1,6 @@
# dummy pyproject file for linting
[tool.pylint] [tool.pylint]
max-line-length = 120 max-line-length = 120
[tool.black] [tool.black]
line-length = 120 line-length = 120