From 90977abda70447cf52c17c8997ce8bc36d453d1d Mon Sep 17 00:00:00 2001 From: q Date: Sun, 12 Apr 2026 12:53:01 +0300 Subject: [PATCH] upd nginx with tls --- README.md | 12 +++++ docker-compose.yaml | 16 +++++- webapp/frontend/Dockerfile | 20 +++++-- .../01-render-nginx-conf.sh | 20 +++++++ .../05-init-self-signed.sh | 16 ++++++ .../docker-entrypoint.d/10-certbot-loop.sh | 47 ++++++++++++++++ webapp/frontend/nginx.conf | 53 ++++++++++++++----- 7 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 webapp/frontend/docker-entrypoint.d/01-render-nginx-conf.sh create mode 100644 webapp/frontend/docker-entrypoint.d/05-init-self-signed.sh create mode 100644 webapp/frontend/docker-entrypoint.d/10-certbot-loop.sh diff --git a/README.md b/README.md index 8acf370..a84b397 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,18 @@ docker compose down - В образ копируется весь проект, а runtime-данные исключены через `.dockerignore`. - Если меняешь код или картинки, достаточно пересобрать: `docker compose up --build -d`. +## HTTPS / Let's Encrypt + +- Фронтендовый `nginx` теперь слушает `80` и `443`, отдает ACME challenge и сам запускает `certbot` внутри контейнера. +- Для автоматической выдачи сертификата задай в `.env`: + - `APP_DOMAIN=bot.example.com` + - `APP_WWW_DOMAIN=www.bot.example.com` (опционально) + - `ACME_EMAIL=admin@example.com` +- Домен должен уже смотреть на сервер, а порты `80` и `443` должны быть доступны снаружи. +- Для безопасной первой проверки можно временно включить `CERTBOT_STAGING=1`, потом вернуть `0`. +- Сертификаты и webroot challenge хранятся в docker volumes `certbot_etc` и `certbot_www`, обновление запускается по кругу с интервалом `CERTBOT_RENEW_INTERVAL` (по умолчанию `12h`). +- Если нужен старый локальный маппинг портов, можно переопределить `WEBAPP_HTTP_PORT` и `WEBAPP_HTTPS_PORT`, но для реального Let's Encrypt внешний `80` должен оставаться доступным. + ## Кратко о логике - `/maket` подбирает размер текста, чтобы уместить его в область макета. diff --git a/docker-compose.yaml b/docker-compose.yaml index 3dd4ddd..03caef6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -36,7 +36,21 @@ services: build: context: ./webapp/frontend ports: - - "3000:80" + - "${WEBAPP_HTTP_PORT:-80}:80" + - "${WEBAPP_HTTPS_PORT:-443}:443" + environment: + APP_DOMAIN: ${APP_DOMAIN:-} + APP_WWW_DOMAIN: ${APP_WWW_DOMAIN:-} + ACME_EMAIL: ${ACME_EMAIL:-} + CERTBOT_STAGING: ${CERTBOT_STAGING:-0} + CERTBOT_RENEW_INTERVAL: ${CERTBOT_RENEW_INTERVAL:-12h} + volumes: + - certbot_etc:/etc/letsencrypt + - certbot_www:/var/www/certbot depends_on: - webapp-api restart: unless-stopped + +volumes: + certbot_etc: + certbot_www: diff --git a/webapp/frontend/Dockerfile b/webapp/frontend/Dockerfile index 01bb247..ac01973 100644 --- a/webapp/frontend/Dockerfile +++ b/webapp/frontend/Dockerfile @@ -1,4 +1,18 @@ FROM nginx:alpine -COPY . /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 + +RUN apk add --no-cache certbot openssl + +COPY index.html /usr/share/nginx/html/index.html +COPY css /usr/share/nginx/html/css +COPY js /usr/share/nginx/html/js +COPY nginx.conf /etc/nginx/default.conf.template +COPY docker-entrypoint.d/01-render-nginx-conf.sh /docker-entrypoint.d/01-render-nginx-conf.sh +COPY docker-entrypoint.d/05-init-self-signed.sh /docker-entrypoint.d/05-init-self-signed.sh +COPY docker-entrypoint.d/10-certbot-loop.sh /docker-entrypoint.d/10-certbot-loop.sh + +RUN chmod +x /docker-entrypoint.d/01-render-nginx-conf.sh \ + /docker-entrypoint.d/05-init-self-signed.sh \ + /docker-entrypoint.d/10-certbot-loop.sh \ + && rm -f /etc/nginx/conf.d/default.conf + +EXPOSE 80 443 diff --git a/webapp/frontend/docker-entrypoint.d/01-render-nginx-conf.sh b/webapp/frontend/docker-entrypoint.d/01-render-nginx-conf.sh new file mode 100644 index 0000000..3707b65 --- /dev/null +++ b/webapp/frontend/docker-entrypoint.d/01-render-nginx-conf.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +APP_DOMAIN="${APP_DOMAIN:-}" +APP_WWW_DOMAIN="${APP_WWW_DOMAIN:-}" + +PRIMARY_DOMAIN="${APP_DOMAIN:-localhost}" +SERVER_NAMES="_" + +if [ -n "$APP_DOMAIN" ]; then + SERVER_NAMES="$APP_DOMAIN" + if [ -n "$APP_WWW_DOMAIN" ]; then + SERVER_NAMES="$SERVER_NAMES $APP_WWW_DOMAIN" + fi +fi + +sed \ + -e "s|__SERVER_NAMES__|$SERVER_NAMES|g" \ + -e "s|__PRIMARY_DOMAIN__|$PRIMARY_DOMAIN|g" \ + /etc/nginx/default.conf.template > /etc/nginx/conf.d/default.conf diff --git a/webapp/frontend/docker-entrypoint.d/05-init-self-signed.sh b/webapp/frontend/docker-entrypoint.d/05-init-self-signed.sh new file mode 100644 index 0000000..8aa0a2e --- /dev/null +++ b/webapp/frontend/docker-entrypoint.d/05-init-self-signed.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +PRIMARY_DOMAIN="${APP_DOMAIN:-localhost}" +CERT_DIR="/etc/letsencrypt/live/$PRIMARY_DOMAIN" +FULLCHAIN="$CERT_DIR/fullchain.pem" +PRIVKEY="$CERT_DIR/privkey.pem" + +if [ ! -s "$FULLCHAIN" ] || [ ! -s "$PRIVKEY" ]; then + echo "No TLS certificate for $PRIMARY_DOMAIN. Generating temporary self-signed cert." + mkdir -p "$CERT_DIR" + openssl req -x509 -nodes -newkey rsa:2048 -days 1 \ + -keyout "$PRIVKEY" \ + -out "$FULLCHAIN" \ + -subj "/CN=$PRIMARY_DOMAIN" >/dev/null 2>&1 +fi diff --git a/webapp/frontend/docker-entrypoint.d/10-certbot-loop.sh b/webapp/frontend/docker-entrypoint.d/10-certbot-loop.sh new file mode 100644 index 0000000..87bec3f --- /dev/null +++ b/webapp/frontend/docker-entrypoint.d/10-certbot-loop.sh @@ -0,0 +1,47 @@ +#!/bin/sh +set -eu + +APP_DOMAIN="${APP_DOMAIN:-}" +APP_WWW_DOMAIN="${APP_WWW_DOMAIN:-}" +ACME_EMAIL="${ACME_EMAIL:-}" +CERTBOT_STAGING="${CERTBOT_STAGING:-0}" +CERTBOT_RENEW_INTERVAL="${CERTBOT_RENEW_INTERVAL:-12h}" + +if [ -z "$APP_DOMAIN" ] || [ -z "$ACME_EMAIL" ]; then + echo "Skipping certbot: APP_DOMAIN or ACME_EMAIL is empty." + exit 0 +fi + +( + sleep 20 + + if [ ! -f "/etc/letsencrypt/renewal/$APP_DOMAIN.conf" ]; then + rm -rf "/etc/letsencrypt/live/$APP_DOMAIN" "/etc/letsencrypt/archive/$APP_DOMAIN" + fi + + while :; do + DOMAINS="-d $APP_DOMAIN" + if [ -n "$APP_WWW_DOMAIN" ]; then + DOMAINS="$DOMAINS -d $APP_WWW_DOMAIN" + fi + + STAGING_FLAG="" + if [ "$CERTBOT_STAGING" = "1" ]; then + STAGING_FLAG="--staging" + fi + + # shellcheck disable=SC2086 + certbot certonly \ + --webroot \ + -w /var/www/certbot \ + --agree-tos \ + --non-interactive \ + --email "$ACME_EMAIL" \ + --keep-until-expiring \ + $STAGING_FLAG \ + $DOMAINS || true + + nginx -s reload || true + sleep "$CERTBOT_RENEW_INTERVAL" + done +) & diff --git a/webapp/frontend/nginx.conf b/webapp/frontend/nginx.conf index 0f3c80f..17c883b 100644 --- a/webapp/frontend/nginx.conf +++ b/webapp/frontend/nginx.conf @@ -1,25 +1,52 @@ server { listen 80; - server_name _; + listen [::]:80; + server_name __SERVER_NAMES__; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name __SERVER_NAMES__; + + ssl_certificate /etc/letsencrypt/live/__PRIMARY_DOMAIN__/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/__PRIMARY_DOMAIN__/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + root /usr/share/nginx/html; index index.html; - # SPA fallback + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location /api/ { + proxy_pass http://webapp-api:8080; + proxy_http_version 1.1; + proxy_set_header Connection ""; + 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-Telegram-Init-Data $http_x_telegram_init_data; + } + location / { try_files $uri $uri/ /index.html; } - # Proxy API requests to backend - location /api/ { - proxy_pass http://webapp-api:8080; - 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-Telegram-Init-Data $http_x_telegram_init_data; - } - - # Cache static assets - location ~* \.(css|js|png|jpg|svg|woff2?)$ { + location ~* \.(css|js|png|jpg|jpeg|svg|woff2?)$ { expires 7d; add_header Cache-Control "public, immutable"; }