在没有云服务提供商的情况下部署 MEAN Stack 应用
Source: Dev.to
🏗️ 架构
我们采用 monorepo(单仓库)方式,这意味着 Angular 前端和 Node.js 后端都位于同一个仓库中。其工作流程如下:
- 将代码推送 到
production分支。 - GitHub Actions 构建 Docker 镜像。
- 镜像被推送到 Docker Hub。
- 在你的 VM 上的 Self‑Hosted Runner 拉取最新镜像并重启容器。
- Nginx 充当反向代理,负责路由流量。
如果你想了解这与云特定托管方式的区别,请查看我之前的文章《Hosting a Node.js Server in an EC2 Instance》。
1. 设置服务器(VirtualBox)
我在此使用了 Debian 虚拟机。
- 网络: 将虚拟机的适配器设置为 桥接模式。这使得虚拟机可以从路由器获取 IP,成为局域网(LAN)中的真实节点。
- 访问: 你应该能够通过 SSH 登录:
ssh user@your_vm_ip.
如需详细了解如何处理 LAN 网络和端口转发,以便让你的服务器可以从互联网访问,请参阅我的文章:How Web Technology Works – Part 01.
2. Docker Hub 与 GitHub Secrets
要自动推送镜像,GitHub 需要获得与 Docker Hub 通信的权限。不要使用你的账户密码。
-
前往 Docker Hub > Settings > Personal access tokens。
-
创建一个 New Access Token,并授予 Read & Write 权限。
-
在你的 GitHub 仓库中,进入 Settings > Secrets and variables > Actions。
-
添加以下 Secrets:
DOCKERHUB_USERNAME– 你的 Docker Hub 用户名DOCKERHUB_TOKEN– 刚才创建的令牌
3. 自托管运行器
与其使用 GitHub 的服务器进行部署,我们改为使用自己的 VM。这称为 自托管运行器。
- 在 GitHub:Settings > Actions > Runners > New self‑hosted runner。
- 选择 Linux 并按照指令在你的 VM 上下载并配置它。
配置完成后,将其安装为服务,使其在后台运行:
sudo ./svc.sh install
sudo ./svc.sh start
4. 容器化(代码)
由于我们使用的是 monorepo,需要分别编写 Dockerfile 并使用一个 Compose 文件。
后端 Dockerfile (backend/Dockerfile)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
前端 Dockerfile (frontend/Dockerfile)
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist/your-app-name /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Docker Compose (docker-compose.yml)
version: '3.8'
services:
backend:
image: your-docker-username/mean-backend:latest
extra_hosts:
- "host.docker.internal:host-gateway"
container_name: mean-backend
restart: always
ports:
- "8080:8080"
frontend:
image: your-docker-username/mean-frontend:latest
container_name: mean-frontend
restart: always
depends_on:
- backend
ports:
- "81:80"
5. Nginx 反向代理
在宿主虚拟机上安装 Nginx:
sudo apt install nginx
我们使用它将 80 端口的流量路由到容器。
配置 (/etc/nginx/sites-available/default):
server {
listen 80;
server_name 10.131.44.201; # Use your VM IP
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://localhost:81;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
检查配置语法:
sudo nginx -t
重新加载 Nginx:
sudo systemctl reload nginx
6. CI/CD 流水线
创建 .github/workflows/deploy.yml。此脚本会自动化整个过程。
name: Build and Deploy
on:
push:
branches: [ production ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Backend
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mean-backend:latest ./backend
docker push ${{ secrets.DOCKERHUB_USERNAME }}/mean-backend:latest
- name: Build and Push Frontend
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mean-frontend:latest ./frontend
docker push ${{ secrets.DOCKERHUB_USERNAME }}/mean-frontend:latest
- name: Deploy on Self‑Hosted Runner
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.VM_HOST }}
username: ${{ secrets.VM_USER }}
key: ${{ secrets.VM_SSH_KEY }}
script: |
cd /path/to/your/repo
docker compose pull
docker compose up -d
Note: 调整
host、username、key和cd路径,以匹配你的环境。
🎉 你完成了!
现在你已经拥有一个完整自动化的 CI/CD 流水线,能够构建 Docker 镜像、将其推送到 Docker Hub,并使用自托管的 GitHub Actions Runner 和 Nginx 作为反向代理,在本地 VM 上进行部署。无需任何云服务提供商。祝编码愉快!
本地虚拟机上的 CI/CD 流水线
以下是完整的 GitHub Actions 工作流以及验证部署的步骤。
GitHub Actions 工作流 (.github/workflows/ci-cd.yml)
name: CI/CD
on:
push:
branches: [ main ]
jobs:
build:
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build Backend Image
run: |
cd ./backend
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mean-backend .
- name: Build Frontend Image
run: |
cd ./frontend
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/mean-frontend .
- name: Push Backend Image
run: |
docker push ${{ secrets.DOCKERHUB_USERNAME }}/mean-backend
- name: Push Frontend Image
run: |
docker push ${{ secrets.DOCKERHUB_USERNAME }}/mean-frontend
deploy:
needs: build
runs-on: self-hosted
steps:
- name: Pull and Restart Services
run: |
cd ~/your-app-dir
docker-compose pull
docker-compose up -d
验证流水线
1. CI/CD 成功
将代码推送到 main 后,您应该在 GitHub Actions 中看到全部绿色勾选。

2. Docker Hub
您的镜像将在 Docker Hub 上显示 latest 标签。

3. 运行中的服务
登录到虚拟机,确认容器已启动并运行。

您的应用现在应该可以通过本地网络访问:
http://VM_IP/
为什么这样有效
在 本地虚拟机 上设置 CI/CD 流水线表明 DevOps 本质上是关于 逻辑和架构,而不仅仅是您选择的云提供商。通过使用 桥接模式的 VirtualBox,您可以获得类似生产环境的环境,完全控制网络和部署周期——且无需任何云费用。
关键要点
- 基础设施灵活性 – 相同的设置可在任何 Linux 机器上运行:虚拟机、Raspberry Pi 或裸金属服务器。
- 自动化 – 自托管 runner 让您在本地保留部署逻辑,同时仍利用 GitHub 进行构建。
- 单仓库效率 – 在同一个仓库中管理 Angular 前端和 Node.js 后端,简化 CI/CD 工作流。
在设置本地环境时遇到了哪些挑战? 在评论中告诉我吧!