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..504099a8 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 +- `0.6.2`, `0.6`, `0` — specific version tags (semver, `v` prefix stripped) - `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 +BACKEND_IMAGE_TAG=0.6.2 +FRONTEND_IMAGE_TAG=0.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) +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) ``` Then `docker-compose up -d` reads them automatically. @@ -122,16 +130,35 @@ 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 \ + -e BACKEND_HOST=host.docker.internal:3000 \ + --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..8af9c3cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,11 +24,11 @@ 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 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/README.md b/README.md index cef36b57..f106b6e3 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=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 a6d511ca..0cf0a7f4 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=0.6.2 # 固定后端版本(默认:latest) +FRONTEND_IMAGE_TAG=0.6.2 # 固定前端版本(默认:latest) ``` #### 仅后端(docker run) diff --git a/docker-compose.yml b/docker-compose.yml index 305b80af..98260e8b 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_HOST:-backend:3000} restart: unless-stopped backend: diff --git a/nginx.conf b/nginx.conf.template similarity index 88% rename from nginx.conf rename to nginx.conf.template index 9de0c1f7..bcf2f289 100644 --- a/nginx.conf +++ b/nginx.conf.template @@ -5,7 +5,10 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - resolver 127.0.0.11 ipv6=off valid=30s; + + # 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; @@ -31,12 +34,12 @@ 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; + 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; @@ -60,19 +63,18 @@ 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;