Build & Push Docker Image #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build & Push Docker Image | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "自定义标签 (留空则使用时间戳)" | |
| required: false | |
| default: "" | |
| deploy: | |
| description: "构建后自动部署到阿里云" | |
| required: false | |
| type: boolean | |
| default: true | |
| env: | |
| REGISTRY: ghcr.io | |
| # 镜像名必须小写 | |
| IMAGE_NAME: xkcoding/myblog | |
| CONTAINER_NAME: myblog | |
| jobs: | |
| build-and-push: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| version_tag: ${{ steps.version.outputs.tag }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install | |
| - name: Build Astro (Production) | |
| run: pnpm build | |
| env: | |
| # 生产环境不使用 BASE_PATH | |
| BASE_PATH: / | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate version tag | |
| id: version | |
| run: | | |
| if [ -n "${{ github.event.inputs.tag }}" ]; then | |
| echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "tag=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ steps.version.outputs.tag }} | |
| type=raw,value=latest | |
| type=sha,prefix=sha- | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Output image info | |
| run: | | |
| echo "🎉 Docker 镜像构建完成!" | |
| echo "" | |
| echo "📦 推送的标签:" | |
| echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}" | |
| echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" | |
| echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7}" | |
| deploy: | |
| needs: build-and-push | |
| if: ${{ github.event.inputs.deploy == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Deploy to Aliyun Server | |
| uses: appleboy/ssh-action@v1.2.0 | |
| env: | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.version_tag }} | |
| with: | |
| host: ${{ secrets.ALIYUN_HOST }} | |
| username: ${{ secrets.ALIYUN_USER }} | |
| key: ${{ secrets.ALIYUN_SSH_KEY }} | |
| port: ${{ secrets.ALIYUN_PORT || 22 }} | |
| envs: IMAGE | |
| script: | | |
| echo "🚀 开始部署 $IMAGE" | |
| # 登录 GitHub Container Registry | |
| echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin | |
| # 拉取最新镜像 | |
| docker pull $IMAGE | |
| # 创建部署目录 | |
| mkdir -p /opt/myblog/dist | |
| # 停止并删除旧容器(如果存在) | |
| docker stop ${{ env.CONTAINER_NAME }} ${{ env.CONTAINER_NAME }}-nginx 2>/dev/null || true | |
| docker rm ${{ env.CONTAINER_NAME }} ${{ env.CONTAINER_NAME }}-nginx 2>/dev/null || true | |
| # 从镜像中提取静态文件 | |
| docker create --name temp-extract $IMAGE | |
| docker cp temp-extract:/usr/share/nginx/html/. /opt/myblog/dist/ | |
| docker rm temp-extract | |
| # 检查是否有 SSL 证书 | |
| if [ -d "/opt/myblog/certbot/etc/live/xkcoding.com" ]; then | |
| echo "🔐 检测到 SSL 证书,启用 HTTPS..." | |
| # 删除可能存在的错误目录,创建 nginx SSL 配置文件 | |
| rm -rf /opt/myblog/nginx-ssl.conf | |
| cat > /opt/myblog/nginx-ssl.conf << 'NGINX_CONF' | |
| # HTTP - 重定向到 HTTPS | |
| server { | |
| listen 80; | |
| server_name xkcoding.com www.xkcoding.com blog.xkcoding.com; | |
| location /.well-known/acme-challenge/ { | |
| root /usr/share/nginx/html; | |
| } | |
| location / { | |
| return 301 https://$server_name$request_uri; | |
| } | |
| } | |
| # HTTPS | |
| server { | |
| listen 443 ssl; | |
| server_name xkcoding.com www.xkcoding.com blog.xkcoding.com; | |
| ssl_certificate /etc/letsencrypt/live/xkcoding.com/fullchain.pem; | |
| ssl_certificate_key /etc/letsencrypt/live/xkcoding.com/privkey.pem; | |
| ssl_protocols TLSv1.2 TLSv1.3; | |
| ssl_prefer_server_ciphers off; | |
| ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; | |
| ssl_session_timeout 1d; | |
| ssl_session_cache shared:SSL:10m; | |
| ssl_session_tickets off; | |
| add_header Strict-Transport-Security "max-age=63072000" always; | |
| root /usr/share/nginx/html; | |
| index index.html; | |
| gzip on; | |
| gzip_vary on; | |
| gzip_proxied any; | |
| gzip_comp_level 6; | |
| gzip_min_length 1000; | |
| gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml image/svg+xml; | |
| location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { | |
| 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"; | |
| } | |
| location / { | |
| try_files $uri $uri/ $uri.html =404; | |
| } | |
| error_page 404 /404.html; | |
| add_header X-Frame-Options "SAMEORIGIN" always; | |
| add_header X-Content-Type-Options "nosniff" always; | |
| add_header X-XSS-Protection "1; mode=block" always; | |
| } | |
| NGINX_CONF | |
| # 使用 SSL 配置启动 nginx | |
| docker run -d \ | |
| --name ${{ env.CONTAINER_NAME }}-nginx \ | |
| --restart unless-stopped \ | |
| -p 80:80 \ | |
| -p 443:443 \ | |
| -v /opt/myblog/dist:/usr/share/nginx/html:ro \ | |
| -v /opt/myblog/nginx-ssl.conf:/etc/nginx/conf.d/default.conf:ro \ | |
| -v /opt/myblog/certbot/etc:/etc/letsencrypt:ro \ | |
| nginx:alpine | |
| else | |
| echo "⚠️ 未检测到 SSL 证书,使用 HTTP..." | |
| # 使用普通配置启动(统一使用 myblog-nginx 容器名,与 SSL 续期工作流保持一致) | |
| docker run -d \ | |
| --name ${{ env.CONTAINER_NAME }}-nginx \ | |
| --restart unless-stopped \ | |
| -p 80:80 \ | |
| $IMAGE | |
| fi | |
| # 清理旧镜像 | |
| docker image prune -f | |
| echo "✅ 部署完成!" | |
| - name: Deployment Summary | |
| run: | | |
| echo "## 🎉 部署成功" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **镜像**: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.version_tag }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **服务器**: \`${{ secrets.ALIYUN_HOST }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **容器名**: \`${{ env.CONTAINER_NAME }}\`" >> $GITHUB_STEP_SUMMARY |