From 7a8436db47fbc7c342ba32629eb2c17cd7bb925f Mon Sep 17 00:00:00 2001 From: JianMiau Date: Wed, 15 Apr 2026 23:15:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AA=BF=E6=95=B4=20Docker=20SSL=20=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E8=88=87=E9=83=A8=E7=BD=B2=E8=AA=AA=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 ++ README.md | 102 +++++++++++++++++++++++++++++++------ docker-compose.yml | 27 +++++++++- docker/nginx/Dockerfile | 9 ++++ docker/nginx/entrypoint.sh | 96 ++++++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 18 deletions(-) create mode 100644 docker/nginx/Dockerfile create mode 100644 docker/nginx/entrypoint.sh diff --git a/.env.example b/.env.example index c7dad21..b770ffe 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,7 @@ DB_DATABASE=badminton DB_TABLE=badminton DB_HISTORY_TABLE=history SERVER_PORT=8788 +NGINX_SERVER_NAME=_ +SSL_CERT_FILE_NAME=RSA-cert.pem +SSL_CHAIN_FILE_NAME=RSA-chain.pem +SSL_KEY_FILE_NAME=RSA-privkey.pem diff --git a/README.md b/README.md index e302b36..1c6d71f 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,28 @@ - 比賽結算後可選擇是否上傳戰績到 `history` 資料表 - 歷史戰績頁直接從 DB 顯示列表,點擊可查看得分過程 -## 開發 Port +## Port 說明 + +### 本機開發 - Client: `3501` - Server API: `8788` -本機開發模式: +開發模式入口: - 前端:`http://localhost:3501` - API:`http://localhost:8788` +### Docker / NAS 部署 + +- 對外入口:`3501` +- 內部 Node app:`8788` + +也就是說: + +- Docker 部署後,使用者要連的是 `3501` +- `8788` 是容器內部 app port,給 Nginx 反向代理用 + ## 本機開發 安裝套件: @@ -56,6 +68,10 @@ DB_DATABASE=badminton DB_TABLE=badminton DB_HISTORY_TABLE=history SERVER_PORT=8788 +NGINX_SERVER_NAME=_ +SSL_CERT_FILE_NAME=RSA-cert.pem +SSL_CHAIN_FILE_NAME=RSA-chain.pem +SSL_KEY_FILE_NAME=RSA-privkey.pem ``` ## 資料表 @@ -99,14 +115,12 @@ npm run build ## Docker 單次啟動 -建置映像: +如果你只是要單獨跑 Node app,可用: ```bash docker build -t badminton-scoreboard . ``` -啟動容器: - ```bash docker run -d \ --name badminton-scoreboard \ @@ -122,15 +136,56 @@ docker run -d \ badminton-scoreboard ``` +這種方式只會直接提供 Node app 本身,不含 Nginx SSL 入口。 + ## NAS 部署 -這個專案現在已經補上 [docker-compose.yml](./docker-compose.yml),所以在 NAS 上可以直接使用: +這個專案已提供 [docker-compose.yml](./docker-compose.yml),會啟動兩個服務: + +1. `badminton-scoreboard` +說明:Node app,內部使用 `8788` + +2. `badminton-scoreboard-web` +說明:Nginx SSL 入口,對外使用 `3501` + +在 NAS 專案目錄中,直接執行: ```bash sudo docker compose up -d --build ``` -但前提是你要先在 NAS 的專案目錄準備好 `.env`,至少要有: +這樣就可以部署。 + +## SSL 憑證掛載 + +Nginx 會直接掛載這個 NAS 目錄: + +```text +/volume1/homes/JianMiau/www/certificate/ +``` + +目前預設使用你現有的檔名: + +- `RSA-cert.pem` +- `RSA-chain.pem` +- `RSA-privkey.pem` + +Nginx 會在容器內自動組合: + +- `RSA-cert.pem` + `RSA-chain.pem` => fullchain +- `RSA-privkey.pem` => private key + +而且已經加上憑證檔變更監看,所以你之後只要更新: + +```text +/volume1/homes/JianMiau/www/certificate/ +``` + +容器內的 Nginx 就會自動 reload,不需要重建 image。 + +## NAS `.env` 範例 + +部署前請先在 NAS 專案目錄準備 `.env`,至少要有: ```env DB_HOST=192.168.0.15 @@ -140,21 +195,36 @@ DB_PASSWORD=你的密碼 DB_DATABASE=badminton DB_TABLE=badminton DB_HISTORY_TABLE=history +NGINX_SERVER_NAME=你的網域 +SSL_CERT_FILE_NAME=RSA-cert.pem +SSL_CHAIN_FILE_NAME=RSA-chain.pem +SSL_KEY_FILE_NAME=RSA-privkey.pem ``` -部署後會對外提供: +## NAS 對外入口 + +部署後請從這個入口使用: ```text -http://NAS_IP:8788 +https://你的網域:3501 ``` -## NAS 部署注意事項 +如果你的憑證是簽給特定網域,請不要用 IP 直接開,否則瀏覽器會跳憑證警告。 -- 這個專案在正式部署時沒有獨立的 `3501` 前端埠,前端建置後由 Node server 一起從 `8788` 提供。 -- 如果 NAS 上已經有其他服務佔用 `8788`,要先改 `docker-compose.yml` 的左側對外埠。 -- 指令要完整寫成 `sudo docker compose up -d --build`,不是 `--buil`。 -- 第一次部署前,建議先確認 NAS 已安裝 Docker / Container Manager,且帳號可執行 `sudo docker compose`。 +## 注意事項 -## Git 記錄 +- `sudo docker compose up -d --build` 這條指令現在可以直接用,因為專案已經補上 compose 檔 +- Docker/NAS 對外入口是 `3501`,不是 `8788` +- `8788` 是 Node app 內部服務埠,不是給使用者直接連的 +- 如果 NAS 上 `3501` 已被其他服務使用,請改 `docker-compose.yml` 左側對外埠 +- 若你的憑證檔名之後改了,只要更新 `.env` 對應的 `SSL_*_FILE_NAME` 即可 -這個專案後續提交我會使用中文 commit 訊息,並已將本地 repo 的 git 中文編碼輸出設定好,方便直接看中文 log。 +## Git 設定 + +這個 repo 已設定: + +- `i18n.commitEncoding=utf-8` +- `i18n.logOutputEncoding=utf-8` +- `core.quotepath=false` + +之後可直接使用中文 commit 訊息與中文 git log。 diff --git a/docker-compose.yml b/docker-compose.yml index 287e65e..c289881 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: dockerfile: Dockerfile image: badminton-scoreboard:latest restart: unless-stopped - ports: - - "8788:8788" + expose: + - "8788" environment: PORT: 8788 DB_HOST: ${DB_HOST:-192.168.0.15} @@ -17,3 +17,26 @@ services: DB_DATABASE: ${DB_DATABASE:-badminton} DB_TABLE: ${DB_TABLE:-badminton} DB_HISTORY_TABLE: ${DB_HISTORY_TABLE:-history} + + badminton-scoreboard-web: + container_name: badminton-scoreboard-web + build: + context: . + dockerfile: docker/nginx/Dockerfile + image: badminton-scoreboard-web:latest + restart: unless-stopped + depends_on: + - badminton-scoreboard + ports: + - "3501:3501" + environment: + NGINX_PORT: 3501 + NGINX_SERVER_NAME: ${NGINX_SERVER_NAME:-_} + SSL_CERT_DIR: /etc/nginx/certs + SSL_CERT_FILE_NAME: ${SSL_CERT_FILE_NAME:-RSA-cert.pem} + SSL_CHAIN_FILE_NAME: ${SSL_CHAIN_FILE_NAME:-RSA-chain.pem} + SSL_KEY_FILE_NAME: ${SSL_KEY_FILE_NAME:-RSA-privkey.pem} + UPSTREAM_HOST: badminton-scoreboard + UPSTREAM_PORT: 8788 + volumes: + - /volume1/homes/JianMiau/www/certificate:/etc/nginx/certs:ro diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..e050910 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:1.27-alpine + +RUN apk add --no-cache inotify-tools + +COPY docker/nginx/entrypoint.sh /entrypoint.sh + +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh new file mode 100644 index 0000000..dd9f7c7 --- /dev/null +++ b/docker/nginx/entrypoint.sh @@ -0,0 +1,96 @@ +#!/bin/sh +set -eu + +NGINX_PORT="${NGINX_PORT:-3501}" +NGINX_SERVER_NAME="${NGINX_SERVER_NAME:-_}" +SSL_CERT_DIR="${SSL_CERT_DIR:-/etc/nginx/certs}" +SSL_CERT_FILE_NAME="${SSL_CERT_FILE_NAME:-RSA-cert.pem}" +SSL_CHAIN_FILE_NAME="${SSL_CHAIN_FILE_NAME:-RSA-chain.pem}" +SSL_KEY_FILE_NAME="${SSL_KEY_FILE_NAME:-RSA-privkey.pem}" +UPSTREAM_HOST="${UPSTREAM_HOST:-badminton-scoreboard}" +UPSTREAM_PORT="${UPSTREAM_PORT:-8788}" + +GENERATED_DIR="/etc/nginx/generated" +GENERATED_CERT_PATH="${GENERATED_DIR}/fullchain.pem" +GENERATED_KEY_PATH="${GENERATED_DIR}/privkey.pem" + +mkdir -p "${GENERATED_DIR}" + +build_cert_bundle() { + cert_path="${SSL_CERT_DIR}/${SSL_CERT_FILE_NAME}" + chain_path="${SSL_CERT_DIR}/${SSL_CHAIN_FILE_NAME}" + key_path="${SSL_CERT_DIR}/${SSL_KEY_FILE_NAME}" + + if [ ! -f "${cert_path}" ]; then + echo "Missing certificate file: ${cert_path}" >&2 + exit 1 + fi + + if [ ! -f "${chain_path}" ]; then + echo "Missing chain file: ${chain_path}" >&2 + exit 1 + fi + + if [ ! -f "${key_path}" ]; then + echo "Missing key file: ${key_path}" >&2 + exit 1 + fi + + cat "${cert_path}" "${chain_path}" > "${GENERATED_CERT_PATH}" + cp "${key_path}" "${GENERATED_KEY_PATH}" +} + +write_nginx_config() { + cat > /etc/nginx/conf.d/default.conf </dev/null || true +} + +trap cleanup INT TERM + +nginx -g 'daemon off;' & +NGINX_PID=$! + +wait "${NGINX_PID}"