From 8922895ab765b8555a793dca16225203b4ee5901 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 15 Jun 2026 13:02:41 +0900 Subject: [PATCH] =?UTF-8?q?https=20=EC=A0=91=EC=86=8D=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 + .gitignore | 7 + deploy-home.sh | 3 +- deploy_server_guide.md | 42 ++++- docker-compose.yml | 5 + docker/nginx/certbot-webroot/.gitkeep | 0 docker/nginx/ssl/.gitkeep | 0 frontend/Dockerfile | 11 +- .../docker-entrypoint.d/40-enable-https.sh | 13 ++ frontend/nginx-https.conf | 22 +++ frontend/nginx-locations.inc | 135 ++++++++++++++++ frontend/nginx.conf | 144 +----------------- scripts/setup-exdev-https.sh | 70 +++++++++ 13 files changed, 306 insertions(+), 151 deletions(-) create mode 100644 docker/nginx/certbot-webroot/.gitkeep create mode 100644 docker/nginx/ssl/.gitkeep create mode 100755 frontend/docker-entrypoint.d/40-enable-https.sh create mode 100644 frontend/nginx-https.conf create mode 100644 frontend/nginx-locations.inc create mode 100755 scripts/setup-exdev-https.sh diff --git a/.env.example b/.env.example index 0989369..fac7b11 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,11 @@ BACKEND_PORT=8080 # Frontend (외부 노출 포트) FRONTEND_PORT=80 +FRONTEND_HTTPS_PORT=443 + +# HTTPS (선택) — scripts/setup-exdev-https.sh 로 발급 후 frontend 재시작 +# SSL_CERT_DIR=./docker/nginx/ssl +# CERTBOT_WEBROOT_DIR=./docker/nginx/certbot-webroot # Desktop Jenkins (웹 UI 「앱 빌드」 — exdev 서버 .env에 설정) # GC_DESKTOP_JENKINS_URL=http://host.docker.internal:8090 diff --git a/.gitignore b/.gitignore index fabfe5a..dbfee65 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,11 @@ desktop/updater.key **/.tauri/ data/mobile-releases/*.apk frontend/public/desktop/updates/latest.json +frontend/public/desktop/updates/*.tar.gz +frontend/public/desktop/updates/*.exe +frontend/public/desktop/updates/*.dmg + +# TLS (서버에서 발급 — git 제외) +docker/nginx/ssl/*.pem +docker/nginx/letsencrypt/ scripts/deploy-target.local.env diff --git a/deploy-home.sh b/deploy-home.sh index 2709251..c1986aa 100755 --- a/deploy-home.sh +++ b/deploy-home.sh @@ -305,7 +305,8 @@ echo -e "${GREEN}${BOLD}╔═════════════════ echo -e "${GREEN}${BOLD}║ 배포 완료! 🎉 ║${NC}" echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════╝${NC}" echo "" -echo -e " 접속 URL: ${CYAN}${BOLD}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}" +echo -e " HTTP: ${CYAN}${BOLD}http://${REMOTE_HOST}:${FRONTEND_PORT}${NC}" +echo -e " HTTPS: ${CYAN}${BOLD}https://${REMOTE_HOST}/${NC} ${YELLOW}(인증서: ./scripts/setup-exdev-https.sh)${NC}" echo "" echo -e "${YELLOW}자주 쓰는 명령어:${NC}" echo " ./deploy-home.sh # 전체 재빌드 배포 (기본)" diff --git a/deploy_server_guide.md b/deploy_server_guide.md index 2e76090..f3ae07b 100644 --- a/deploy_server_guide.md +++ b/deploy_server_guide.md @@ -90,7 +90,7 @@ GoldenChart 배포는 **공통 스크립트 `deploy.sh`** 를 중심으로 두 | 서비스 | 컨테이너 | 포트 (기본) | URL | |--------|---------|------------|-----| -| Frontend (Nginx) | `gc-frontend` | 80 | http://exdev.co.kr/ | +| Frontend (Nginx) | `gc-frontend` | 80, **443** | http://exdev.co.kr/ · https://exdev.co.kr/ | | Backend (Spring Boot) | `gc-backend` | 8080 | http://exdev.co.kr:8080 (직접) 또는 `/api` (Nginx 경유) | | MySQL | `gc-mysql` | 3306 | localhost:3306 (서버 내부) | @@ -602,6 +602,46 @@ docker system prune -f --- +## HTTPS (HTTP + HTTPS 동시 접속) + +기본은 **HTTP(80)만** 동작합니다. `docker/nginx/ssl/` 에 인증서가 있으면 frontend 컨테이너가 **HTTPS(443)** 도 함께 엽니다. HTTP→HTTPS 강제 리다이렉트는 하지 않습니다. + +### 최초 1회 (exdev.co.kr 서버) + +```bash +cd /Users/aidev/dev/goldenChart # work-tree 경로 + +# 1) frontend 가 80에서 동작 중인지 확인 +docker compose up -d frontend + +# 2) Let's Encrypt 발급 (도메인이 이 서버를 가리켜야 함, 443 방화벽 개방) +./scripts/setup-exdev-https.sh --email you@example.com --domain exdev.co.kr + +# 테스트용 자체 서명 (브라우저 경고 발생) +# ./scripts/setup-exdev-https.sh --self-signed +``` + +### 검증 + +```bash +curl -s -o /dev/null -w "HTTP: %{http_code}\n" http://exdev.co.kr/ +curl -s -o /dev/null -w "HTTPS: %{http_code}\n" https://exdev.co.kr/ +``` + +### 인증서 갱신 (cron 예시, 매월) + +```bash +docker run --rm \ + -v "$(pwd)/docker/nginx/certbot-webroot:/var/www/certbot" \ + -v "$(pwd)/docker/nginx/letsencrypt:/etc/letsencrypt" \ + certbot/certbot renew +docker compose exec frontend nginx -s reload +``` + +> **Mac 방화벽:** 시스템 설정 → 네트워크 → 방화벽에서 **443/tcp** 허용. 공유기/클라우드에서도 443 포워딩이 필요할 수 있습니다. + +--- + ## 12. 트러블슈팅 ### 12-1. git push 후 배포가 안 됨 diff --git a/docker-compose.yml b/docker-compose.yml index bb4d32d..db5d490 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,8 +114,13 @@ services: condition: service_healthy ports: - "${FRONTEND_PORT:-80}:80" + - "${FRONTEND_HTTPS_PORT:-443}:443" volumes: # Desktop updater latest.json·설치 파일 — Jenkins publish + nginx reload - ./frontend/public/desktop/updates:/usr/share/nginx/html/desktop/updates:ro + # HTTPS — fullchain.pem + privkey.pem (scripts/setup-exdev-https.sh) + - ${SSL_CERT_DIR:-./docker/nginx/ssl}:/etc/nginx/ssl:ro + # Let's Encrypt HTTP-01 webroot + - ${CERTBOT_WEBROOT_DIR:-./docker/nginx/certbot-webroot}:/var/www/certbot:ro networks: - goldenchart-net diff --git a/docker/nginx/certbot-webroot/.gitkeep b/docker/nginx/certbot-webroot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker/nginx/ssl/.gitkeep b/docker/nginx/ssl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 42a834f..e9c1eb9 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -2,12 +2,9 @@ FROM node:20-alpine AS builder WORKDIR /app -# 의존성 레이어 캐시 (package*.json 변경 시만 재설치) COPY package*.json ./ RUN npm ci -# 소스 복사 후 프로덕션 빌드 -# Docker 환경에서 /api 는 nginx 가 backend:8080 으로 프록시하므로 그대로 사용 COPY . . ARG VITE_API_BASE_URL=/api ARG VITE_APP_VERSION= @@ -18,11 +15,13 @@ RUN npm run build # ── 2단계: 서비스 ───────────────────────────────────────────────────────────── FROM nginx:1.27-alpine AS production -# 빌드 결과물 복사 COPY --from=builder /app/dist /usr/share/nginx/html -# SPA 라우팅 + 정적 파일 캐시 설정 COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx-locations.inc /etc/nginx/gc-locations.inc +COPY nginx-https.conf /etc/nginx/nginx-https.conf +COPY docker-entrypoint.d/40-enable-https.sh /docker-entrypoint.d/40-enable-https.sh +RUN chmod +x /docker-entrypoint.d/40-enable-https.sh -EXPOSE 80 +EXPOSE 80 443 CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/docker-entrypoint.d/40-enable-https.sh b/frontend/docker-entrypoint.d/40-enable-https.sh new file mode 100755 index 0000000..c91fb8f --- /dev/null +++ b/frontend/docker-entrypoint.d/40-enable-https.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +HTTPS_CONF="/etc/nginx/conf.d/https.conf" +HTTPS_TEMPLATE="/etc/nginx/nginx-https.conf" + +if [ -f /etc/nginx/ssl/fullchain.pem ] && [ -f /etc/nginx/ssl/privkey.pem ]; then + cp "$HTTPS_TEMPLATE" "$HTTPS_CONF" + echo "[gc-frontend] HTTPS enabled — listening on 443" +else + rm -f "$HTTPS_CONF" 2>/dev/null || true + echo "[gc-frontend] HTTPS skipped — place fullchain.pem + privkey.pem in /etc/nginx/ssl" +fi diff --git a/frontend/nginx-https.conf b/frontend/nginx-https.conf new file mode 100644 index 0000000..b28393a --- /dev/null +++ b/frontend/nginx-https.conf @@ -0,0 +1,22 @@ +# SSL 인증서가 /etc/nginx/ssl/ 에 있을 때 entrypoint 가 conf.d 에 복사한다. +server { + listen 443 ssl; + http2 on; + server_name _; + root /usr/share/nginx/html; + index index.html; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + client_max_body_size 32m; + + resolver 127.0.0.11 8.8.8.8 8.8.4.4 valid=30s; + resolver_timeout 5s; + + include /etc/nginx/gc-locations.inc; +} diff --git a/frontend/nginx-locations.inc b/frontend/nginx-locations.inc new file mode 100644 index 0000000..06d8912 --- /dev/null +++ b/frontend/nginx-locations.inc @@ -0,0 +1,135 @@ + # Let's Encrypt HTTP-01 (certbot webroot) + location /.well-known/acme-challenge/ { + root /var/www/certbot; + allow all; + } + + # ── STOMP / SockJS (Spring context-path /api → /api/ws/trading) ─── + location /api/ws/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + proxy_buffering off; + proxy_request_buffering off; + gzip off; + chunked_transfer_encoding on; + } + + # ── GoldenChart Spring Boot Backend 프록시 ──────────────────────── + location /api/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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 10s; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_buffering off; + } + + # ── Upbit REST API 역방향 프록시 ────────────────────────────────────── + location /upbit-api/ { + proxy_pass https://api.upbit.com/; + proxy_ssl_server_name on; + proxy_ssl_protocols TLSv1.2 TLSv1.3; + proxy_set_header Host api.upbit.com; + proxy_set_header Accept application/json; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_hide_header Access-Control-Allow-Origin; + add_header Access-Control-Allow-Origin *; + proxy_cache_bypass $http_upgrade; + } + + # ── Upbit WebSocket 역방향 프록시 ───────────────────────────────────── + location /upbit-ws { + proxy_pass https://api.upbit.com/websocket/v1; + proxy_http_version 1.1; + proxy_ssl_server_name on; + proxy_ssl_protocols TLSv1.2 TLSv1.3; + proxy_ssl_verify off; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host api.upbit.com; + proxy_set_header Origin "https://upbit.com"; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Authorization ""; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } + + # ── STOMP / SockJS WebSocket 프록시 (구버전 /ws/**) ───────────────── + location /ws/ { + proxy_pass http://backend:8080/api/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + proxy_buffering off; + } + + # ── 모바일 APK 다운로드 (대용량) ───────────────────────────────────── + location /api/mobile-app/download/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + 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_read_timeout 600s; + proxy_send_timeout 600s; + proxy_buffering off; + client_max_body_size 200m; + } + + # ── React SPA 폴백 ──────────────────────────────────────────────────── + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|woff2?|ttf|eot|svg|ico|png|jpg|jpeg|gif|webp)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # GoldenChart Desktop updater manifest + bundles + location /desktop/updates/ { + alias /usr/share/nginx/html/desktop/updates/; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + location = /desktop/updates/latest.json { + alias /usr/share/nginx/html/desktop/updates/latest.json; + default_type application/json; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/javascript application/javascript + application/json image/svg+xml; diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 415380c..bf0577f 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -10,152 +10,10 @@ server { root /usr/share/nginx/html; index index.html; - # multipart 이미지 업로드 (Spring max-request-size 30MB와 맞춤) client_max_body_size 32m; - # 외부 호스트명 DNS 조회용 resolver (Upbit + Backend 프록시에 필요) resolver 127.0.0.11 8.8.8.8 8.8.4.4 valid=30s; resolver_timeout 5s; - # ── STOMP / SockJS (Spring context-path /api → /api/ws/trading) ─── - # SockJS 가 /api/ws/trading/{id}/{session}/websocket 으로 업그레이드하므로 - # 일반 REST 프록시와 분리해 Upgrade 헤더·장시간 타임아웃을 반드시 설정한다. - location /api/ws/ { - proxy_pass http://backend:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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 7d; - proxy_send_timeout 7d; - proxy_read_timeout 7d; - proxy_buffering off; - proxy_request_buffering off; - gzip off; - chunked_transfer_encoding on; - } - - # ── GoldenChart Spring Boot Backend 프록시 ──────────────────────── - # 브라우저 → /api/... → http://backend:8080/api/... - location /api/ { - proxy_pass http://backend:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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 10s; - proxy_read_timeout 60s; - proxy_send_timeout 60s; - proxy_buffering off; - } - - # ── Upbit REST API 역방향 프록시 ────────────────────────────────────── - # 브라우저 → /upbit-api/v1/... → https://api.upbit.com/v1/... - # CORS 없이 동일 오리진 요청으로 처리됨 (dev: Vite proxy, prod: nginx proxy) - location /upbit-api/ { - proxy_pass https://api.upbit.com/; - proxy_ssl_server_name on; - proxy_ssl_protocols TLSv1.2 TLSv1.3; - proxy_set_header Host api.upbit.com; - proxy_set_header Accept application/json; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_hide_header Access-Control-Allow-Origin; - add_header Access-Control-Allow-Origin *; - proxy_cache_bypass $http_upgrade; - } - - # ── Upbit WebSocket 역방향 프록시 ───────────────────────────────────── - # 브라우저 → ws://HOST/upbit-ws → wss://api.upbit.com/websocket/v1 - location /upbit-ws { - proxy_pass https://api.upbit.com/websocket/v1; - proxy_http_version 1.1; - proxy_ssl_server_name on; - proxy_ssl_protocols TLSv1.2 TLSv1.3; - proxy_ssl_verify off; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Host api.upbit.com; - proxy_set_header Origin "https://upbit.com"; - proxy_set_header X-Real-IP $remote_addr; - # 쿠키/Authorization 헤더 제거 (업비트 WebSocket 인증 불필요) - proxy_set_header Authorization ""; - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - proxy_buffering off; - } - - # ── STOMP / SockJS WebSocket 프록시 ────────────────────────────────── - # Spring context-path /api → 실제 엔드포인트는 /api/ws/trading - # (구버전 클라이언트 /ws/** 요청도 /api/ws/** 로 전달) - location /ws/ { - proxy_pass http://backend:8080/api/ws/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - 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 7d; - proxy_send_timeout 7d; - proxy_read_timeout 7d; - proxy_buffering off; - } - - # ── 모바일 APK 다운로드 (대용량) ───────────────────────────────────── - location /api/mobile-app/download/ { - proxy_pass http://backend:8080; - proxy_http_version 1.1; - 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_read_timeout 600s; - proxy_send_timeout 600s; - proxy_buffering off; - client_max_body_size 200m; - } - - # ── React SPA 폴백 ──────────────────────────────────────────────────── - location / { - try_files $uri $uri/ /index.html; - } - - # JS/CSS/이미지 등 정적 자산 — 1년 장기 캐시 (Vite 콘텐츠 해시 포함 파일명) - location ~* \.(js|css|woff2?|ttf|eot|svg|ico|png|jpg|jpeg|gif|webp)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - access_log off; - } - - # HTML 은 캐시 안 함 (항상 최신 index.html 로드) - location ~* \.html$ { - expires -1; - add_header Cache-Control "no-store, no-cache, must-revalidate"; - } - - # GoldenChart Desktop updater manifest + bundles - location /desktop/updates/ { - alias /usr/share/nginx/html/desktop/updates/; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - location = /desktop/updates/latest.json { - alias /usr/share/nginx/html/desktop/updates/latest.json; - default_type application/json; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - # gzip 압축 - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/javascript application/javascript - application/json image/svg+xml; + include /etc/nginx/gc-locations.inc; } diff --git a/scripts/setup-exdev-https.sh b/scripts/setup-exdev-https.sh new file mode 100755 index 0000000..81c6ffe --- /dev/null +++ b/scripts/setup-exdev-https.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# exdev.co.kr — HTTP(80) 유지 + HTTPS(443) Let's Encrypt 인증서 발급 +# +# 사전: docker compose up -d frontend (80 포트 동작, /.well-known/acme-challenge/ 노출) +# 사용: ./scripts/setup-exdev-https.sh [--email you@example.com] [--domain exdev.co.kr] +# ./scripts/setup-exdev-https.sh --self-signed # 로컬/테스트용 자체 서명 +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +DOMAIN="${DOMAIN:-exdev.co.kr}" +EMAIL="${CERTBOT_EMAIL:-}" +SELF_SIGNED=false +SSL_DIR="${SSL_CERT_DIR:-$ROOT/docker/nginx/ssl}" +WEBROOT="${CERTBOT_WEBROOT_DIR:-$ROOT/docker/nginx/certbot-webroot}" +LE_DIR="$ROOT/docker/nginx/letsencrypt" + +while [[ $# -gt 0 ]]; do + case "$1" in + --email) EMAIL="$2"; shift 2 ;; + --domain) DOMAIN="$2"; shift 2 ;; + --self-signed) SELF_SIGNED=true; shift ;; + -h|--help) + echo "Usage: $0 [--email E] [--domain D] [--self-signed]" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +mkdir -p "$SSL_DIR" "$WEBROOT" "$LE_DIR" +chmod 755 "$SSL_DIR" "$WEBROOT" + +link_certs() { + ln -sf "../letsencrypt/live/$DOMAIN/fullchain.pem" "$SSL_DIR/fullchain.pem" + ln -sf "../letsencrypt/live/$DOMAIN/privkey.pem" "$SSL_DIR/privkey.pem" +} + +if [[ "$SELF_SIGNED" == true ]]; then + echo "[setup-https] self-signed certificate for $DOMAIN" + openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ + -keyout "$SSL_DIR/privkey.pem" \ + -out "$SSL_DIR/fullchain.pem" \ + -subj "/CN=$DOMAIN" +else + if [[ -z "$EMAIL" ]]; then + echo "[setup-https] ERROR: --email required for Let's Encrypt (or use --self-signed)" >&2 + exit 1 + fi + echo "[setup-https] certbot webroot — domain=$DOMAIN email=$EMAIL" + docker run --rm \ + -v "$WEBROOT:/var/www/certbot" \ + -v "$LE_DIR:/etc/letsencrypt" \ + certbot/certbot certonly --webroot -w /var/www/certbot \ + -d "$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive --keep-until-expiring + link_certs +fi + +echo "[setup-https] restarting frontend (443 enable)..." +docker compose up -d --force-recreate frontend + +echo "" +echo "Done." +echo " HTTP: http://${DOMAIN}/" +echo " HTTPS: https://${DOMAIN}/" +echo "" +echo "Renew (cron monthly):" +echo " docker run --rm -v \"$WEBROOT:/var/www/certbot\" -v \"$LE_DIR:/etc/letsencrypt\" \\" +echo " certbot/certbot renew && docker compose exec frontend nginx -s reload"