From 58f1143cfd709f147b82661b30206be55b1ba9da Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 11 Jul 2026 18:25:30 +0800 Subject: [PATCH 1/3] feat: publish frontend Docker image to GHCR (issue #239) - Add docker-publish-frontend.yml workflow mirroring the backend one (image github-stars-manager-frontend, multi-arch amd64/arm64, latest/semver/sha tags) - Make backend proxy upstream configurable via BACKEND_HOST env (default backend:3000) using nginx.conf.template + envsubst - Add .dockerignore to slim the frontend build context - Switch docker-compose frontend to the published GHCR image, keeping a local build option - Update DOCKER.md / README docs for the frontend image - Remove dead nginx.conf (superseded by nginx.conf.template) --- .dockerignore | 20 ++++++ .github/workflows/docker-publish-frontend.yml | 63 +++++++++++++++++++ DOCKER.md | 39 +++++++++--- Dockerfile | 9 +-- README.md | 6 +- README_zh.md | 6 +- docker-compose.yml | 6 +- nginx.conf => nginx.conf.template | 12 ++-- 8 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish-frontend.yml rename nginx.conf => nginx.conf.template (95%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8425e8f5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.github +.claude +.vscode +.sisyphus +.omo +.writing +server +node_modules +dist +build +electron +upload +versions +LandingPage +cloudflare-worker +assets +templates +*.md +.DS_Store diff --git a/.github/workflows/docker-publish-frontend.yml b/.github/workflows/docker-publish-frontend.yml new file mode 100644 index 00000000..5740305a --- /dev/null +++ b/.github/workflows/docker-publish-frontend.yml @@ -0,0 +1,63 @@ +name: Publish Frontend Docker Image to GHCR + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager-frontend + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # branch push → "latest" + type=raw,value=latest,enable={{is_default_branch}} + # tag push → "v1.2.3", "1.2.3", "1.2", "1" + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # every build → sha-abc1234 + type=sha,prefix=sha- + + - name: Build and push Docker image + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/DOCKER.md b/DOCKER.md index 48c6c9c2..9a8f3a53 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -24,15 +24,21 @@ docker-compose up -d > ``` > Use a [Personal Access Token](https://github.com/settings/tokens) (with `read:packages` scope) as the password. -Available image tags: +Available image tags (both images share the same tagging scheme): - `latest` — latest build from the `main` branch - `v0.6.2`, `0.6.2` — specific version tags - `sha-abc1234` — specific commit builds -To pin a specific version in `docker-compose.yml`, set `BACKEND_IMAGE_TAG` in your `.env` file: +Published images: +- Backend: `ghcr.io/amintacccp/github-stars-manager-server` +- Frontend: `ghcr.io/amintacccp/github-stars-manager-frontend` + +To pin specific versions in `docker-compose.yml`, set `BACKEND_IMAGE_TAG` and/or +`FRONTEND_IMAGE_TAG` in your `.env` file: ```bash BACKEND_IMAGE_TAG=v0.6.2 +FRONTEND_IMAGE_TAG=v0.6.2 ``` ## Backend Server (docker run) @@ -84,12 +90,14 @@ docker run -d \ docker-compose up -d ``` -To customize secrets, create a `.env` file in the project root: +To customize secrets and image versions, create a `.env` file in the project root: ```bash API_SECRET=my-strong-secret ENCRYPTION_KEY=my-encryption-key BACKEND_IMAGE_TAG=v0.6.2 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=v0.6.2 # pin frontend image version (default: latest) +# BACKEND_HOST=backend:3000 # target for the frontend's /api proxy (default: backend:3000) ``` Then `docker-compose up -d` reads them automatically. @@ -122,16 +130,33 @@ Then run `docker-compose up -d --build`. The override file takes precedence auto ### Using Docker directly (frontend only) +The pre-built frontend image is published to GHCR — no local build required: + ```bash -# Build the image -docker build -t github-stars-manager . +# Pull the published image +docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest -# Run the container -docker run -d -p 8080:80 --name github-stars-manager github-stars-manager +# Run the container (point /api proxy at your backend) +docker run -d -p 8080:80 \ + -e BACKEND_HOST=host.docker.internal:3000 \ + --name github-stars-manager \ + ghcr.io/amintacccp/github-stars-manager-frontend:latest # The application will be available at http://localhost:8080 ``` +> `BACKEND_HOST` sets the upstream the frontend proxies `/api/` to. In Docker Compose +> it defaults to `backend:3000`. When running standalone, point it at your backend's +> reachable address (e.g. `host.docker.internal:3000` on Docker Desktop, or the backend +> container's IP/host on a shared network). + +To build the image locally instead (uses the repository `Dockerfile`): + +```bash +docker build -t github-stars-manager . +docker run -d -p 8080:80 --name github-stars-manager github-stars-manager +``` + ## CORS Handling This Docker setup handles CORS in two ways: diff --git a/Dockerfile b/Dockerfile index 1fb17cc0..8d9876a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,11 +24,12 @@ RUN rm -f /etc/nginx/conf.d/default.conf # Copy built files from build stage COPY --from=build /app/dist /usr/share/nginx/html -# Copy custom nginx configuration -COPY nginx.conf /etc/nginx/nginx.conf +# Copy custom nginx configuration template (rendered at startup with BACKEND_HOST) +COPY nginx.conf.template /etc/nginx/nginx.conf.template # Expose port EXPOSE 80 -# Start nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +# Render the nginx config (substituting BACKEND_HOST, defaulting to backend:3000) +# and start nginx +CMD ["sh", "-c", "export BACKEND_HOST=${BACKEND_HOST:-backend:3000}; envsubst '${BACKEND_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/README.md b/README.md index cef36b57..5b7807b3 100644 --- a/README.md +++ b/README.md @@ -212,10 +212,11 @@ https://github.com/AmintaCCCP/GithubStarsManager/releases ### 🐳 Run With Docker -Pre-built backend image is available on GHCR — no local build required: +Pre-built backend **and frontend** images are available on GHCR — no local build required: ```bash docker pull ghcr.io/amintacccp/github-stars-manager-server:latest +docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` @@ -240,7 +241,8 @@ To customize, create a `.env` file: ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=v0.6.2 # pin a specific version (default: latest) +BACKEND_IMAGE_TAG=v0.6.2 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=v0.6.2 # pin frontend image version (default: latest) ``` #### Backend only (docker run) diff --git a/README_zh.md b/README_zh.md index a6d511ca..66306522 100644 --- a/README_zh.md +++ b/README_zh.md @@ -328,10 +328,11 @@ npm run build ### Docker 部署 -GHCR 上有预构建的后端镜像,无需本地构建: +GHCR 上提供预构建的**后端和前端**镜像,无需本地构建: ```bash docker pull ghcr.io/amintacccp/github-stars-manager-server:latest +docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` @@ -357,7 +358,8 @@ docker-compose up -d ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=v0.6.2 # 固定版本(默认:latest) +BACKEND_IMAGE_TAG=v0.6.2 # 固定后端版本(默认:latest) +FRONTEND_IMAGE_TAG=v0.6.2 # 固定前端版本(默认:latest) ``` #### 仅后端(docker run) diff --git a/docker-compose.yml b/docker-compose.yml index 305b80af..cd4e2018 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,15 @@ version: '3.8' services: frontend: - build: . + image: ghcr.io/amintacccp/github-stars-manager-frontend:${FRONTEND_IMAGE_TAG:-latest} + # Uncomment below (and comment out 'image') to build locally instead: + # build: . ports: - "8080:80" depends_on: - backend + environment: + - BACKEND_HOST=backend:3000 restart: unless-stopped backend: diff --git a/nginx.conf b/nginx.conf.template similarity index 95% rename from nginx.conf rename to nginx.conf.template index 9de0c1f7..911ea5b6 100644 --- a/nginx.conf +++ b/nginx.conf.template @@ -31,9 +31,9 @@ http { server { listen 80; server_name localhost; - # Backend API proxy + # Backend API proxy (configurable via BACKEND_HOST, default: backend:3000) location /api/ { - set $backend_upstream backend:3000; + set $backend_upstream ${BACKEND_HOST}; proxy_pass http://$backend_upstream; proxy_http_version 1.1; proxy_set_header Host $host; @@ -60,19 +60,19 @@ http { add_header 'Content-Length' 0 always; return 204; } - + root /usr/share/nginx/html; index index.html index.htm; - + # Try to serve file, if not found, serve index.html (for SPA routing) try_files $uri $uri/ /index.html; - + # Add CORS headers for all responses add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; - + # Add security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; From 4f265a3c7757f73bec18c1f24253104651fa0bfd Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 11 Jul 2026 18:52:02 +0800 Subject: [PATCH 2/3] fix: address coderabbitai & gemini-code-assist review findings - nginx.conf.template: use static proxy_pass http://${BACKEND_HOST} (substituted at container start) instead of a runtime variable, and drop the hardcoded Docker-only resolver so the image works on Kubernetes/ECS too (gemini HIGH) - nginx.conf.template: remove invalid Access-Control-Allow-Credentials:true alongside Access-Control-Allow-Origin:* (gemini MEDIUM) - docker-compose.yml: BACKEND_HOST now uses ${BACKEND_HOST:-backend:3000} so .env overrides are honored (coderabbitai MAJOR) - Docs: use unprefixed semver tags (0.6.2) consistent with the backend image (coderabbitai MAJOR); add -e BACKEND_HOST to the local-build docker run example (coderabbitai MINOR) --- DOCKER.md | 14 ++++++++------ README.md | 4 ++-- README_zh.md | 4 ++-- docker-compose.yml | 2 +- nginx.conf.template | 5 +---- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 9a8f3a53..504099a8 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -26,7 +26,7 @@ docker-compose up -d Available image tags (both images share the same tagging scheme): - `latest` — latest build from the `main` branch -- `v0.6.2`, `0.6.2` — specific version tags +- `0.6.2`, `0.6`, `0` — specific version tags (semver, `v` prefix stripped) - `sha-abc1234` — specific commit builds Published images: @@ -37,8 +37,8 @@ To pin specific versions in `docker-compose.yml`, set `BACKEND_IMAGE_TAG` and/or `FRONTEND_IMAGE_TAG` in your `.env` file: ```bash -BACKEND_IMAGE_TAG=v0.6.2 -FRONTEND_IMAGE_TAG=v0.6.2 +BACKEND_IMAGE_TAG=0.6.2 +FRONTEND_IMAGE_TAG=0.6.2 ``` ## Backend Server (docker run) @@ -95,8 +95,8 @@ To customize secrets and image versions, create a `.env` file in the project roo ```bash API_SECRET=my-strong-secret ENCRYPTION_KEY=my-encryption-key -BACKEND_IMAGE_TAG=v0.6.2 # pin backend image version (default: latest) -FRONTEND_IMAGE_TAG=v0.6.2 # pin frontend image version (default: latest) +BACKEND_IMAGE_TAG=0.6.2 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=0.6.2 # pin frontend image version (default: latest) # BACKEND_HOST=backend:3000 # target for the frontend's /api proxy (default: backend:3000) ``` @@ -154,7 +154,9 @@ To build the image locally instead (uses the repository `Dockerfile`): ```bash docker build -t github-stars-manager . -docker run -d -p 8080:80 --name github-stars-manager github-stars-manager +docker run -d -p 8080:80 \ + -e BACKEND_HOST=host.docker.internal:3000 \ + --name github-stars-manager github-stars-manager ``` ## CORS Handling diff --git a/README.md b/README.md index 5b7807b3..f106b6e3 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,8 @@ To customize, create a `.env` file: ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=v0.6.2 # pin backend image version (default: latest) -FRONTEND_IMAGE_TAG=v0.6.2 # pin frontend image version (default: latest) +BACKEND_IMAGE_TAG=0.6.2 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=0.6.2 # pin frontend image version (default: latest) ``` #### Backend only (docker run) diff --git a/README_zh.md b/README_zh.md index 66306522..0cf0a7f4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -358,8 +358,8 @@ docker-compose up -d ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=v0.6.2 # 固定后端版本(默认:latest) -FRONTEND_IMAGE_TAG=v0.6.2 # 固定前端版本(默认:latest) +BACKEND_IMAGE_TAG=0.6.2 # 固定后端版本(默认:latest) +FRONTEND_IMAGE_TAG=0.6.2 # 固定前端版本(默认:latest) ``` #### 仅后端(docker run) diff --git a/docker-compose.yml b/docker-compose.yml index cd4e2018..98260e8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: depends_on: - backend environment: - - BACKEND_HOST=backend:3000 + - BACKEND_HOST=${BACKEND_HOST:-backend:3000} restart: unless-stopped backend: diff --git a/nginx.conf.template b/nginx.conf.template index 911ea5b6..8e53d5f1 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -5,7 +5,6 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - resolver 127.0.0.11 ipv6=off valid=30s; # Hide nginx version server_tokens off; @@ -33,8 +32,7 @@ http { server_name localhost; # Backend API proxy (configurable via BACKEND_HOST, default: backend:3000) location /api/ { - set $backend_upstream ${BACKEND_HOST}; - proxy_pass http://$backend_upstream; + proxy_pass http://${BACKEND_HOST}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -69,7 +67,6 @@ http { # Add CORS headers for all responses add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; From a2879245990897c00783b2a97bbf2700e548dcc6 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 11 Jul 2026 19:21:37 +0800 Subject: [PATCH 3/3] fix: address coderabbitai re-review (runtime resolver + Host header) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nginx.conf.template: restore a configurable resolver (RESOLVER env, default 127.0.0.11) so the backend upstream is re-resolved at runtime (recreated backend containers no longer leave /api/ hitting a stale IP) — coderabbitai Major stability finding - nginx.conf.template: proxy_set_header Host $proxy_host (forwards the configured BACKEND_HOST) instead of $host — coderabbitai Minor - Dockerfile: export RESOLVER (default 127.0.0.11) and include it in the envsubst substitution so the resolver is overridable for Kubernetes/ECS (keeps the image portable, addressing the earlier gemini portability concern without a hardcoded Docker-only resolver) --- Dockerfile | 5 ++--- nginx.conf.template | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8d9876a9..8af9c3cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,5 @@ COPY nginx.conf.template /etc/nginx/nginx.conf.template # Expose port EXPOSE 80 -# Render the nginx config (substituting BACKEND_HOST, defaulting to backend:3000) -# and start nginx -CMD ["sh", "-c", "export BACKEND_HOST=${BACKEND_HOST:-backend:3000}; envsubst '${BACKEND_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"] \ No newline at end of file +# Render the nginx config (substituting BACKEND_HOST and RESOLVER) and start nginx +CMD ["sh", "-c", "export BACKEND_HOST=${BACKEND_HOST:-backend:3000}; export RESOLVER=${RESOLVER:-127.0.0.11}; envsubst '${BACKEND_HOST} ${RESOLVER}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/nginx.conf.template b/nginx.conf.template index 8e53d5f1..bcf2f289 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -6,6 +6,10 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; + # Resolver for runtime backend DNS re-resolution (configurable; defaults to + # Docker's embedded DNS, override with RESOLVER for Kubernetes/ECS/etc.) + resolver ${RESOLVER} ipv6=off valid=30s; + # Hide nginx version server_tokens off; @@ -32,9 +36,10 @@ http { server_name localhost; # Backend API proxy (configurable via BACKEND_HOST, default: backend:3000) location /api/ { - proxy_pass http://${BACKEND_HOST}; + set $backend_upstream ${BACKEND_HOST}; + proxy_pass http://$backend_upstream; proxy_http_version 1.1; - proxy_set_header Host $host; + proxy_set_header Host $proxy_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;