#!/bin/bash # generate-configs.sh — генератор конфигов из sites.yml # Генерирует: nginx-http.conf, nginx-ssl.conf, certbot/domains.txt, обновляет .env set -euo pipefail DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" SITES_YML="$DIR/sites.yml" NGINX_DIR="$DIR/nginx" ENV_FILE="$DIR/.env" if [ ! -f "$SITES_YML" ]; then echo "Ошибка: $SITES_YML не найден" exit 1 fi echo "=== Генерация конфигов из sites.yml ===" # Используем python3 с quoted heredoc — предотвращает интерпретацию $ переменных bash python3 - "$DIR" "$NGINX_DIR" "$ENV_FILE" << 'PYEOF' import yaml, os, sys BASE_DIR = sys.argv[1] NGINX_DIR = sys.argv[2] ENV_FILE = sys.argv[3] SITES_YML = os.path.join(BASE_DIR, "sites.yml") with open(SITES_YML) as f: data = yaml.safe_load(f) sites = data.get("sites", {}) if not sites: print("Ошибка: в sites.yml нет сайтов") sys.exit(1) # собираем данные all_domains = [] env_domains = {} site_list = [] for name, cfg in sites.items(): domain = cfg["domain"] aliases = cfg.get("aliases", []) all_domains.append(domain) all_domains.extend(aliases) env_key = f"DOMAINS_{name}" env_val = ",".join([domain] + aliases) env_domains[env_key] = env_val site_list.append({ "name": name, "domain": domain, "aliases": aliases, "type": cfg.get("type", "upstream"), "upstream": cfg.get("upstream", ""), "root": cfg.get("root", ""), "api": cfg.get("api", {}), }) env_domains["ALL_DOMAINS"] = ",".join(all_domains) def all_server_names(): """Возвращает строку со всеми доменами и алиасами через пробел""" parts = [] for s in site_list: parts.append(s["domain"]) parts.extend(s["aliases"]) return " ".join(parts) def all_server_names_multiline(): """Возвращает строку с переносами для nginx server_name""" lines = [] for s in site_list: lines.append(s["domain"]) for a in s["aliases"]: lines.append(a) return " \\\n ".join(lines) # ────────────────────────────────────────────── # 2. Генерация nginx-http.conf # ────────────────────────────────────────────── http_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную # HTTP-only конфигурация (работает когда нет сертификатов) server {{ listen 80; server_name {all_server_names_multiline()}; location / {{ root /usr/share/nginx/stub/html; index index.html; }} location /.well-known/acme-challenge/ {{ root /var/www/certbot; }} }} # Блок для HTTPS → HTTP редиректа (порт 443) server {{ listen 443 ssl; server_name {all_server_names_multiline()}; ssl_certificate /etc/nginx/ssl/dummy.crt; ssl_certificate_key /etc/nginx/ssl/dummy.key; return 301 http://$host$request_uri; }} """ http_conf_path = os.path.join(NGINX_DIR, "nginx-http.conf") with open(http_conf_path, "w") as f: f.write(http_conf.lstrip()) print(f" ✓ {http_conf_path}") # ────────────────────────────────────────────── # 3. Генерация nginx-ssl.conf # ────────────────────────────────────────────── ssl_server_blocks = [] for s in site_list: server_names = " ".join([s["domain"]] + s["aliases"]) block = f""" server {{ listen 443 ssl; server_name {server_names}; ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; """ if s["type"] == "upstream": block += f""" location / {{ proxy_pass {s["upstream"]}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; }} """ elif s["type"] == "static": block += f""" location / {{ root {s["root"]}; index index.html; try_files $uri $uri/ /index.html; }} """ # API routes for path, target in s["api"].items(): cors_block = "" if "/api/" in path: cors_block = """ if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain charset=UTF-8'; return 204; } """ block += f""" location {path} {{ proxy_pass {target}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; {cors_block} }} """ if s["type"] == "static": block += f""" location /uploads/ {{ alias /uploads/; expires 1y; add_header Cache-Control "public, immutable"; }} """ block += "}" ssl_server_blocks.append(block) ssl_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную # Полная HTTPS конфигурация # --- HTTP → HTTPS редирект --- server {{ listen 80; server_name {all_server_names()}; location /.well-known/acme-challenge/ {{ root /var/www/certbot; }} location /uploads/ {{ alias /uploads/; expires 1y; add_header Cache-Control "public, immutable"; }} location / {{ return 301 https://$host$request_uri; }} }} # --- HTTPS серверные блоки --- {''.join(ssl_server_blocks)} """ ssl_conf_path = os.path.join(NGINX_DIR, "nginx-ssl.conf") with open(ssl_conf_path, "w") as f: f.write(ssl_conf.lstrip()) print(f" ✓ {ssl_conf_path}") # ────────────────────────────────────────────── # 4. Генерация per-domain конфигов (conf.available/) # ────────────────────────────────────────────── CONF_AVAILABLE = os.path.join(NGINX_DIR, "conf.available") os.makedirs(CONF_AVAILABLE, exist_ok=True) # 00-http.conf — базовый HTTP catch-all (всегда активен) base_http = f"""# Автоматически сгенерировано generate-configs.sh server {{ listen 80 default_server; server_name _; location / {{ root /usr/share/nginx/stub/html; index index.html; }} location /.well-known/acme-challenge/ {{ root /var/www/certbot; }} }} """ path = os.path.join(CONF_AVAILABLE, "00-http.conf") with open(path, "w") as f: f.write(base_http.lstrip()) print(f" ✓ conf.available/00-http.conf") # per-domain: SSL + HTTP fallback ORDER = ["10", "20", "30", "40", "50", "60", "70", "80", "90"] for idx, s in enumerate(site_list): prefix = ORDER[idx] if idx < len(ORDER) else f"{90 + idx}" safe_name = s["name"] server_names = " ".join([s["domain"]] + s["aliases"]) # --- SSL variant --- ssl_block = f"""# CERT_DOMAIN={s["domain"]} # Автоматически сгенерировано generate-configs.sh server {{ listen 443 ssl; server_name {server_names}; ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; """ if s["type"] == "upstream": ssl_block += f""" location / {{ proxy_pass {s["upstream"]}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; }} """ elif s["type"] == "static": ssl_block += f""" location / {{ root {s["root"]}; index index.html; try_files $uri $uri/ /index.html; }} """ for path, target in s["api"].items(): cors = "" if "/api/" in path: cors = """ if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain charset=UTF-8'; return 204; } """ ssl_block += f""" location {path} {{ proxy_pass {target}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; {cors} }} """ if s["type"] == "static": ssl_block += f""" location /uploads/ {{ alias /uploads/; expires 1y; add_header Cache-Control "public, immutable"; }} """ ssl_block += "}" ssl_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.ssl.conf") with open(ssl_path, "w") as f: f.write(ssl_block.lstrip()) # --- HTTP fallback variant --- http_block = f"""# HTTP fallback for {s["domain"]} (no SSL cert) server {{ listen 80; server_name {server_names}; """ if s["type"] == "upstream": http_block += f""" location / {{ proxy_pass {s["upstream"]}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; }} """ elif s["type"] == "static": http_block += f""" location / {{ root {s["root"]}; index index.html; try_files $uri $uri/ /index.html; }} """ for path, target in s["api"].items(): cors = "" if "/api/" in path: cors = """ if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain charset=UTF-8'; return 204; } """ http_block += f""" location {path} {{ proxy_pass {target}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; {cors} }} """ if s["type"] == "static": http_block += f""" location /uploads/ {{ alias /uploads/; expires 1y; add_header Cache-Control "public, immutable"; }} """ http_block += "}" http_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.http.conf") with open(http_path, "w") as f: f.write(http_block.lstrip()) print(f" ✓ conf.available/{prefix}-{safe_name}.ssl.conf + .http.conf") # ────────────────────────────────────────────── # 5. Генерация certbot/domains.txt # ────────────────────────────────────────────── domains_txt_path = os.path.join(BASE_DIR, "certbot", "domains.txt") with open(domains_txt_path, "w") as f: for d in all_domains: f.write(d + "\n") print(f" ✓ {domains_txt_path}") # ────────────────────────────────────────────── # 6. Обновление .env # ────────────────────────────────────────────── if os.path.exists(ENV_FILE): with open(ENV_FILE) as f: env_lines = f.readlines() else: env_lines = [] new_env = [] for line in env_lines: stripped = line.strip() if stripped.startswith("DOMAINS_") or stripped.startswith("ALL_DOMAINS") or "CERTBOT NGINX VARIABLES" in stripped: continue new_env.append(line) # удаляем пустые строки в начале while new_env and not new_env[0].strip(): new_env.pop(0) domain_keys = {k: v for k, v in env_domains.items()} insert_idx = None for i, line in enumerate(new_env): if line.strip().startswith("EMAIL="): insert_idx = i + 1 break env_header = "#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную\n" domain_lines = [f"{k}={v}\n" for k, v in sorted(domain_keys.items())] if insert_idx is not None: new_env.insert(insert_idx, env_header) for dl in reversed(domain_lines): new_env.insert(insert_idx + 1, dl) else: new_env = [env_header] + domain_lines + new_env with open(ENV_FILE, "w") as f: f.writelines(new_env) print(f" ✓ {ENV_FILE} (обновлён)") print() print("=== Генерация завершена ===") print(f"Сгенерировано {len(site_list)} сайтов:") for s in site_list: print(f" • {s['domain']} ({s['type']})") print() print("Не забудь перезапустить nginx: docker compose restart nginx") PYEOF