Feb 12, 2025
本文记录了如何使用 GitHub Actions 实现博客前后端的自动部署,并通过 Docker 容器化部署,完全脱离 Vercel,实现自定义服务器部署流程。
在线预览:
shssh-keygen -t rsa -b 4096 -C "your_email@example.com"
sh# 将公钥添加到 authorized_keys 文件* cat ~/.ssh/id_rsa.pub **>>** ~/.ssh/authorized_keys # 设置正确的权限* chmod 600 ~/.ssh/authorized_keys chmod 700 ~/.ssh
SSHPWD
: 您的 SSH 私钥内容
HOST
: 您的服务器 IP 地址或域名
USERNAME
: 服务器登录用户名
前端应用相对简单,直接在docker环境中运行镜像即可
大致流程如下:
dockerfile# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app # 全局安装 pnpm RUN npm install -g pnpm && \ npm install -g pnpm@latest && \ pnpm config set registry https://registry.npmmirror.com/ # 复制依赖文件 COPY package.json pnpm-lock.yaml .npmrc ./ # 安装依赖 RUN pnpm install # 复制源代码 COPY . . # 构建应用 RUN pnpm build # 运行阶段 FROM node:18-alpine AS runner WORKDIR /app # 设置环境变量 ENV NODE_ENV production ENV PORT 3000 # 从构建阶段复制必要文件 # 复制 standalone 目录 COPY --from=builder /app/.next/standalone ./ # 复制静态文件 COPY --from=builder /app/.next/static ./.next/static # 复制公共文件(如果有) COPY --from=builder /app/public ./public # 暴露端口 EXPOSE 3000 # 启动服务 CMD ["node", "server.js"]
这里其他都比较好理解,对standalone
相关进行说明
这是对Nextjs镜像体积的优化:
背景:为了支持SSR,Next.js项目构建完成后的产物不仅仅是前端的静态资产,还包含可以使用node命令运行的js文件,以及node_modules
文件夹,导致整体文件偏大。
解决方法:使用官方的 output: "standalone"
设置,官网参考,打包后会输出一个不带node_modules
的最小server.js
文件,可以直接代替next start
可以看到这个最小包是不包含静态文件的,因此在dockerfile需要将静态文件复制到 standalone 目录
简而言之,使用 output: "standalone"
的主要目的是生成一个更加轻量化的部署包,减少冗余文件,并且保持 SSR 功能的同时优化了部署流程。
路径:/.github/workflows/deploy.yml
ymlname: Deploy Next.js to Cloud Server on: push: branches: - main # 当推送到主分支时触发 jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build Docker image run: | docker build -t lizh606/wanyue-blog:latest . - name: Push Docker image to Docker Hub run: | docker push lizh606/wanyue-blog:latest - name: SSH to remote server and deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSHPWD }} port: 22 debug: true script: | # 登录后,拉取最新的 Docker 镜像并启动容器 docker pull lizh606/wanyue-blog:latest docker stop next-client || true docker rm next-client || true docker run -d --name next-client -p 3000:3000 lizh606/wanyue-blog:latest
也就是对应上面👆流程图的步骤
完成后提交代码开始执行
Vue项目的部署类似,这里不过多介绍
dockerfile# 使用官方 Nginx 镜像作为基础镜像 FROM nginx:latest # 复制自定义的 nginx 配置文件到容器中 COPY nginx.conf /etc/nginx/nginx.conf # 复制构建的应用文件到 Nginx 的默认静态文件目录 COPY dist /usr/share/nginx/html # 暴露容器的 8080 端口 EXPOSE 8080 # 启动 Nginx 让Nginx在前台运行,而不是作为守护进程在后台运行。 CMD ["nginx", "-g", "daemon off;"]
⚠️ 项目中使用了域名,最后路径为域名/admin
,所以需要设置路由基本路径为/admin/
,将 IP 访问重定向到域名是处理在ip+端口无法访问到资源问题,若无域名,中间的处理步骤可删除
nginxuser nginx; worker_processes auto; pid /run/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; gzip on; # 服务器配置 server { listen 8080; server_name ip; # 将 IP 访问重定向到域名 if ($host = 'ip:端口') { return 301 https://wanyue.me$request_uri; } # 根目录设置 root /usr/share/nginx/html; index index.html index.htm; # 处理 JavaScript 文件的 MIME 类型 location ~* \.js$ { add_header Content-Type application/javascript; try_files $uri =404; } # 处理 /admin 路径 location /admin { alias /usr/share/nginx/html; # 使用 alias 而不是 root try_files $uri $uri/ /admin/index.html; # 处理静态资源 location ~ ^/admin/assets/.*\.(js|css|png|jpg|jpeg|gif|ico|svg)$ { add_header Content-Type application/javascript; try_files $uri =404; } } # 默认位置设置 location / { try_files $uri $uri/ /index.html; } # 添加错误页面配置 error_page 404 /404.html; location = /404.html { root /usr/share/nginx/html; } } }
路径:/.github/workflows/deploy.yml
ymlname: Deploy on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: # 下载代码仓库 - uses: actions/checkout@v2 # 设置 Docker Buildx - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 # 登录 Docker Hub - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} # 安装依赖并构建 - name: Install and Build run: | npm install -g pnpm && \ pnpm install --no-frozen-lockfile && \ pnpm build # 构建并推送 Docker 镜像 - name: Build and push Docker image run: | docker build -t ${{ secrets.DOCKER_USERNAME }}/vue-admin:latest . docker push ${{ secrets.DOCKER_USERNAME }}/vue-admin:latest # SSH 连接并部署 - name: Deploy to server uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.SSHPWD }} port: 22 script: | # 拉取最新镜像 docker pull ${{ secrets.DOCKER_USERNAME }}/vue-admin:latest # 确保网络存在 docker network create app_common-network 2>/dev/null || true # 停止并删除旧容器 docker stop vue-admin-app || true docker rm vue-admin-app || true # 运行新容器 docker run -d --name vue-admin-app \ -p 8080:8080 \ --network=app_common-network \ ${{ secrets.DOCKER_USERNAME }}/vue-admin:latest
提交代码自动运行即可
相比于前端部署,后台需要进行数据库的构建和连接,需要通过docker compose
来实现
dockerfileFROM node:18-alpine WORKDIR /app # 安装 pnpm RUN npm install -g pnpm COPY package*.json ./ # 使用 pnpm 安装依赖 RUN pnpm install COPY . . RUN pnpm run build EXPOSE 13000 CMD ["sh", "-c", "ls -la /app && ls -la /app/dist/src && cat /app/dist/src/main.js && echo '\n环境变量:' && printenv && echo '\n正在启动应用...' && pnpm start:prod"]
CMD主要是方便判断执行问题,也可指运行pnpm start:prod
yml# Use root/example as user/password credentials version: '3.1' services: api: build: . image: nest-api container_name: nest-api ports: - '13000:13000' networks: - app-network depends_on: - db db: image: mysql:8.0 container_name: nest-db restart: always environment: MYSQL_ROOT_PASSWORD: x x x MYSQL_DATABASE: your-database-name ports: - '12000:3306' networks: app-network: ipv4_address: 172.22.0.2 volumes: - mysql_data_new:/var/lib/mysql command: - --default-authentication-plugin=mysql_native_password - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-flush-log-at-trx-commit=0 # navicat # adminer: # image: adminer # restart: always # ports: # - 12005:8080 # networks: # - app-network # 添加网络配置 networks: app-network: driver: bridge ipam: driver: default config: - subnet: 172.22.0.0/16 # 在文件末尾添加 volumes 数据卷配置 volumes: mysql_data_new: driver: local
⚠️注意点:
db
和api
需要设置内部网络连接app-network
adminer
可选,生产环境我直接使用了phpmyadmin
来进行数据库管理ymlDB_HOST=nest-db DB_PORT=3306 DB_DATABASE=your-database-name DB_USERNAME="xxx" DB_PASSWORD="xxx" DB_SYNC=true //生产环境不开启 LOG_ON=true # JWT secret SECRET="xxx" APP_PORT=13000
⚠️注意点:
DB_SYNC
是typeorm的同步设置,官网表明生产环境使用是不安全的,建议使用migration
进行迁移
DB_HOST
不能是127.0.0.1
或者localhost
,需设置成数据库服务名称nest-db
可在本地直接运行docker compose up -d --build
测试是否可以启动,访问http://localhost:13000
运行成功后直接推送代码部署
分类 | 命令 | 描述 |
---|---|---|
构建和管理镜像 | docker build -t <tag_name> <path> | 根据 Dockerfile 构建镜像,并使用 <tag_name> 给镜像打标签。 |
docker images | 列出所有本地存储的 Docker 镜像。 | |
docker rmi <image_id> | 删除指定的镜像。 | |
管理容器 | docker run -d --name <container_name> <image_name> | 使用指定镜像在后台启动一个容器,并命名为 <container_name> 。 |
docker ps | 列出当前正在运行的所有容器。 | |
docker ps -a | 列出所有容器,包括停止的容器。 | |
docker stop <container_id> | 停止指定的容器。 | |
docker start <container_id> | 启动已停止的容器。 | |
docker restart <container_id> | 重启指定的容器。 | |
docker rm <container_id> | 删除指定的容器。 | |
容器日志和监控 | docker logs <container_id> | 查看指定容器的日志。 |
docker stats | 显示所有容器的实时资源使用情况。 | |
网络和卷管理 | docker network ls | 列出所有 Docker 网络。 |
docker network create <network_name> | 创建一个新的 Docker 网络。 | |
docker volume ls | 列出所有 Docker 卷。 | |
docker volume create <volume_name> | 创建一个新的 Docker 卷。 | |
清理系统 | docker system prune -a -f | 删除所有未使用的容器、网络、镜像(包括悬空镜像)和构建缓存。 |
docker volume prune -f | 删除所有未使用的卷。 | |
Docker Compose | docker-compose up | 启动定义在 docker-compose.yml 文件中的所有服务。 |
docker-compose up -d | 在后台启动所有服务。 | |
docker-compose down | 停止并删除所有服务和网络。 | |
docker-compose stop | 停止所有服务。 | |
docker-compose start | 启动已停止的服务。 | |
docker-compose restart | 重启所有服务。 | |
构建和管理服务 | docker-compose build | 根据 docker-compose.yml 文件构建或重新构建服务。 |
docker-compose build --no-cache | 不使用缓存构建服务。 | |
docker-compose logs | 查看所有服务的日志。 | |
docker-compose logs <service_name> | 查看指定服务的日志。 | |
docker-compose ps | 列出所有服务的状态。 | |
docker-compose exec <service_name> <command> | 在运行中的服务容器内执行命令。 | |
docker-compose run <service_name> <command> | 在新容器中运行命令。 |