From 5281b33bcc79e3c8a16b1800c8f33fcfa26cadc7 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Tue, 19 Aug 2025 20:37:29 +0800 Subject: [PATCH] Add Docker support and server entrypoint Introduced Dockerfile, .dockerignore, and GitHub Actions workflow for building and publishing Docker images. Added server.js as the Express entrypoint for standalone deployment. Updated README with Docker, Docker Compose, and Kubernetes deployment instructions. --- .dockerignore | 82 ++++++++++++++++ .github/workflows/docker.yml | 94 ++++++++++++++++++ Dockerfile | 38 ++++++++ README.md | 181 +++++++++++++++++++++++++++++++---- server.js | 78 +++++++++++++++ 5 files changed, 453 insertions(+), 20 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 server.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..089f897 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,82 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage +.nyc_output +test-results + +# Production build +dist +build + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log + +# Git +.git +.gitignore + +# GitHub +.github + +# Temporary files +tmp +temp + +# Documentation +*.md +!README.md + +# Vercel +.vercel +vercel.json + +# Cloudflare Workers +wrangler.toml + +# ESLint +.eslintcache + +# Package manager +.npm +.pnpm-debug.log* + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..e1f345f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,94 @@ +name: Build and Push Docker Image to GHCR + +on: + push: + branches: + - main + - develop + tags: + - 'v*' + pull_request: + branches: + - main + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + security-scan: + runs-on: ubuntu-latest + needs: build-and-push + if: github.event_name != 'pull_request' + permissions: + contents: read + packages: read + security-events: write + + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bebd37d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Use the official Node.js runtime as the base image +FROM node:20-alpine + +# Set the working directory inside the container +WORKDIR /app + +# Copy package.json first to leverage Docker cache for dependencies +COPY package.json ./ + +# Install dependencies including express for standalone server +RUN npm ci --only=production && npm install express + +# Copy the rest of the application code +COPY . . + +# Create a non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S xget -u 1001 + +# Change ownership of the app directory to the nodejs user +RUN chown -R xget:nodejs /app + +# Switch to the non-root user +USER xget + +# Expose the port the app runs on +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 + +# Start the application +CMD ["node", "server.js"] diff --git a/README.md b/README.md index cb696c0..04ae7ed 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Firefox 扩展](https://img.shields.io/badge/Firefox%20扩展-582ACB?logo=Firefox&logoColor=white)](#-生态系统集成) [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare%20Workers-F38020?&logo=cloudflare&logoColor=white)](#cloudflare-workers-一键部署) [![Vercel](https://img.shields.io/badge/Vercel-000000?logo=vercel&logoColor=white)](#vercel-一键部署) +[![Docker](https://img.shields.io/badge/Docker-2496ED?&logo=docker&logoColor=white)](#docker-部署) [![GitHub](https://img.shields.io/badge/GitHub-181717?&logo=github&logoColor=white)](#github) [![GitLab](https://img.shields.io/badge/GitLab-FC6D26?&logo=gitlab&logoColor=white)](#gitlab) @@ -990,7 +991,7 @@ conda env update -f environment.yml https://xget.xi-xu.me/maven/maven2 - + xget-maven-central @@ -1563,7 +1564,7 @@ import requests def download_arxiv_paper(arxiv_id, output_path): url = f"https://xget.xi-xu.me/arxiv/pdf/{arxiv_id}.pdf" response = requests.get(url) - + if response.status_code == 200: with open(output_path, 'wb') as f: f.write(response.content) @@ -1731,10 +1732,10 @@ import requests class XgetTransport: def __init__(self, base_url): self.base_url = base_url - + def request(self, method, url, **kwargs): # 将请求转发到 Xget 加速服务 - accelerated_url = url.replace("https://generativelanguage.googleapis.com", + accelerated_url = url.replace("https://generativelanguage.googleapis.com", "https://xget.xi-xu.me/ip/gemini") return requests.request(method, accelerated_url, **kwargs) @@ -1760,10 +1761,10 @@ def call_ai_api(provider, endpoint, data, api_key): "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - + # 使用 Xget 加速 URL url = f"https://xget.xi-xu.me/ip/{provider}/{endpoint}" - + response = requests.post(url, headers=headers, json=data) return response.json() @@ -1824,7 +1825,7 @@ async function chatWithGPT() { messages: [{ role: 'user', content: 'Hello!' }], model: 'gpt-4', }); - + console.log(completion.choices[0].message.content); } @@ -1842,7 +1843,7 @@ async function chatWithClaude() { max_tokens: 1000, messages: [{ role: 'user', content: 'Hello!' }], }); - + console.log(message.content); } ``` @@ -1949,7 +1950,7 @@ services: - "80:80" volumes: - ./html:/usr/share/nginx/html - + database: image: xget.xi-xu.me/cr/mcr/mssql/server:2022-latest environment: @@ -1957,7 +1958,7 @@ services: SA_PASSWORD: "MyStrongPassword123!" volumes: - mssql_data:/var/opt/mssql - + cache: image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine ports: @@ -2003,13 +2004,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: Build with accelerated base images run: | # 构建时使用 Xget 加速的基础镜像 docker build -t myapp:latest \ --build-arg BASE_IMAGE=xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine . - + - name: Test with accelerated images run: | # 使用加速镜像进行测试 @@ -2063,17 +2064,17 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Download model files run: | # 使用 Xget 加速下载大型模型文件 wget https://xget.xi-xu.me/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin - + - name: Clone dependency repo run: | # 使用 Xget 加速 Git 克隆 git clone https://xget.xi-xu.me/gh/[所有者]/[存储库].git - + - name: Download release assets run: | # 批量下载发布文件 @@ -2146,6 +2147,146 @@ WORKDIR /app 部署后,你的 Xget 服务将在 `your-project-name.vercel.app` 上可用。 +### Docker 部署 + +[![Docker](https://img.shields.io/badge/Docker-2496ED?&logo=docker&logoColor=white)](https://github.com/xixu-me/Xget/pkgs/container/xget) + +#### 使用预构建镜像(推荐) + +```bash +# 拉取最新镜像 +docker pull ghcr.io/xixu-me/xget:latest + +# 运行容器 +docker run -d \ + --name xget \ + -p 3000:3000 \ + --restart unless-stopped \ + ghcr.io/xixu-me/xget:latest +``` + +#### 本地构建镜像 + +```bash +# 克隆存储库 +git clone https://github.com/xixu-me/Xget.git +cd Xget + +# 构建镜像 +docker build -t xget . + +# 运行容器 +docker run -d \ + --name xget \ + -p 3000:3000 \ + --restart unless-stopped \ + xget +``` + +#### Docker Compose + +创建 `docker-compose.yml` 文件: + +```yaml +version: '3.8' + +services: + xget: + image: ghcr.io/xixu-me/xget:latest + container_name: xget + ports: + - "3000:3000" + restart: unless-stopped + environment: + - NODE_ENV=production + - PORT=3000 + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +然后运行: + +```bash +docker-compose up -d +``` + +#### Kubernetes 部署 + +创建 `k8s-deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xget + labels: + app: xget +spec: + replicas: 3 + selector: + matchLabels: + app: xget + template: + metadata: + labels: + app: xget + spec: + containers: + - name: xget + image: ghcr.io/xixu-me/xget:latest + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3000" + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: xget-service +spec: + selector: + app: xget + ports: + - protocol: TCP + port: 80 + targetPort: 3000 + type: LoadBalancer +``` + +部署到 Kubernetes: + +```bash +kubectl apply -f k8s-deployment.yaml +``` + +部署完成后,你的 Xget 服务将在 `http://localhost:3000` 上可用。通过 `/api/health` 端点可以检查服务状态。 + ### 手动部署 如果你更喜欢手动部署或需要自定义配置: @@ -2220,7 +2361,7 @@ export const CONFIG = { ```javascript export const PLATFORMS = { // 现有平台... - + // 新平台示例 custom: { base: "https://example.com", @@ -2282,16 +2423,16 @@ npm run test:watch ### 常见问题 -**Q: 下载速度没有明显提升?** +**Q: 下载速度没有明显提升?** A: 检查源文件是否已经在 CDN 边缘节点缓存,首次访问可能较慢,后续访问会显著提升。 -**Q: Git 操作失败?** +**Q: Git 操作失败?** A: 确认使用了正确的 URL 格式,且 Git 客户端版本支持 HTTPS 代理。 -**Q: 部署后无法访问?** +**Q: 部署后无法访问?** A: 检查 Cloudflare Workers 域名是否正确绑定,确认 `wrangler.toml` 配置正确。 -**Q: 出现 400 错误?** +**Q: 出现 400 错误?** A: 检查 URL 路径格式,确认平台前缀正确使用。 ### 性能监控 diff --git a/server.js b/server.js new file mode 100644 index 0000000..a16d835 --- /dev/null +++ b/server.js @@ -0,0 +1,78 @@ +import express from 'express'; +import handler from './api/[...path].js'; +import healthHandler from './api/health.js'; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware to parse JSON bodies +app.use(express.json()); +app.use(express.raw({ type: 'application/octet-stream', limit: '50mb' })); + +// Health endpoint +app.get('/api/health', async (req, res) => { + try { + const request = new Request(`http://localhost:${PORT}${req.url}`, { + method: req.method, + headers: req.headers + }); + const response = await healthHandler(request); + + res.status(response.status); + response.headers.forEach((value, key) => { + res.set(key, value); + }); + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + res.json(await response.json()); + } else { + res.send(await response.text()); + } + } catch (error) { + console.error('Health check error:', error); + res.status(500).json({ error: 'Health check failed' }); + } +}); + +// Main handler for all other routes +app.use('*', async (req, res) => { + try { + const request = new Request(`http://localhost:${PORT}${req.originalUrl}`, { + method: req.method, + headers: req.headers, + body: req.method !== 'GET' && req.method !== 'HEAD' ? req.body : undefined + }); + + const response = await handler(request); + + res.status(response.status); + response.headers.forEach((value, key) => { + res.set(key, value); + }); + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + res.json(await response.json()); + } else if (response.body) { + // Handle binary data + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + res.send(buffer); + } else { + res.send(await response.text()); + } + } catch (error) { + console.error('Request handler error:', error); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + timestamp: new Date().toISOString() + }); + } +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Xget server running on port ${PORT}`); + console.log(`📡 Health check: http://localhost:${PORT}/api/health`); +});