Build & Push Docker Image #13
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: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Upload nginx config to Server | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.ALIYUN_HOST }} | |
| username: ${{ secrets.ALIYUN_USER }} | |
| key: ${{ secrets.ALIYUN_SSH_KEY }} | |
| port: ${{ secrets.ALIYUN_PORT || 22 }} | |
| source: "nginx-ssl.conf" | |
| target: "/opt/myblog/" | |
| - 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 }} | |
| command_timeout: 15m | |
| 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..." | |
| # 使用 SSL 配置启动 nginx(配置文件已通过 SCP 上传) | |
| 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..." | |
| # 使用普通配置启动 | |
| docker run -d \ | |
| --name ${{ env.CONTAINER_NAME }}-nginx \ | |
| --restart unless-stopped \ | |
| -p 80:80 \ | |
| $IMAGE | |
| fi | |
| # ==================== 清理 Docker 资源 ==================== | |
| echo "🧹 清理 Docker 资源..." | |
| # 清理未使用的镜像(无标签的悬空镜像) | |
| docker image prune -f | |
| # 清理旧版本的 myblog 镜像,只保留最新的 3 个 | |
| echo "📦 清理旧版本镜像..." | |
| docker images ghcr.io/xkcoding/myblog --format "{{.ID}} {{.CreatedAt}}" | \ | |
| sort -k2 -r | \ | |
| tail -n +4 | \ | |
| awk '{print $1}' | \ | |
| xargs -r docker rmi -f 2>/dev/null || true | |
| # 显示当前镜像状态 | |
| echo "📊 当前 Docker 镜像:" | |
| docker images ghcr.io/xkcoding/myblog --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" | |
| 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 |