클라우드 제공업체 없이 MEAN 스택 앱 배포
Source: Dev.to
위에 제공된 소스 링크만으로는 번역할 본문이 포함되어 있지 않습니다. 번역을 원하는 텍스트(본문)를 제공해 주시면 한국어로 번역해 드리겠습니다.
🏗️ 아키텍처
우리는 monorepo 방식을 사용하고 있습니다. 즉, Angular 프런트엔드와 Node.js 백엔드가 동일한 저장소에 함께 존재합니다. 흐름은 다음과 같습니다:
production브랜치에 코드 푸시.- GitHub Actions가 Docker 이미지를 빌드합니다.
- 이미지는 Docker Hub에 푸시됩니다.
- Self‑Hosted Runner가 VM에서 최신 이미지를 풀링하고 컨테이너를 재시작합니다.
- Nginx가 리버스 프록시 역할을 하여 트래픽을 라우팅합니다.
클라우드‑특화 호스팅과 어떻게 다른지 궁금하다면, 이전 글인 EC2 인스턴스에서 Node.js 서버 호스팅하기를 확인해 보세요.
1. 서버 설정 (VirtualBox)
I used a Debian VM for this setup.
- 네트워크: VM 어댑터를 브리지 모드로 설정하세요. 이렇게 하면 VM이 라우터로부터 IP를 받아 로컬 네트워크(LAN) 상의 실제 노드가 됩니다.
- 접근: 다음 명령으로 SSH 접속이 가능해야 합니다:
ssh user@your_vm_ip.
LAN 네트워킹 및 포트 포워딩을 통해 서버를 인터넷에서 접근 가능하도록 설정하는 방법에 대한 자세한 내용은 제 게시물 웹 기술 작동 방식 – Part 01 을 참고하세요.
2. Docker Hub & GitHub Secrets
이미지를 자동으로 푸시하려면 GitHub가 Docker Hub와 통신할 수 있는 권한이 필요합니다. 계정 비밀번호를 사용하지 마세요.
-
Docker Hub > Settings > Personal access tokens 로 이동합니다.
-
Read & Write 권한을 가진 New Access Token을 생성합니다.
-
GitHub 리포지토리에서 Settings > Secrets and variables > Actions 로 이동합니다.
-
다음 시크릿을 추가합니다:
DOCKERHUB_USERNAME– Docker Hub 사용자 이름DOCKERHUB_TOKEN– 방금 만든 토큰
3. 자체 호스팅 러너
GitHub의 서버를 사용해 배포하는 대신, 자체 VM을 사용합니다. 이를 Self‑Hosted Runner 라고 합니다.
- GitHub에서: Settings > Actions > Runners > New self‑hosted runner.
- Linux 를 선택하고 명령을 따라 VM에 다운로드 및 설정합니다.
설정이 완료되면 백그라운드에서 실행되도록 서비스를 설치합니다:
sudo ./svc.sh install
sudo ./svc.sh start
4. 컨테이너화 (코드)
모노레포에 있기 때문에 별도의 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 리버스 프록시
호스트 VM에 Nginx를 설치합니다:
sudo apt install nginx
포트 80 트래픽을 컨테이너로 라우팅하기 위해 사용합니다.
구성 (/etc/nginx/sites-available/default):
server {
listen 80;
server_name 10.131.44.201; # 사용 중인 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경로를 환경에 맞게 조정하세요.
🎉 완료되었습니다!
이제 Docker 이미지를 빌드하고 Docker Hub에 푸시하며, 자체 호스팅 GitHub Actions 러너와 리버스 프록시 역할을 하는 Nginx를 사용해 로컬 VM에 배포하는 완전 자동화 CI/CD 파이프라인이 준비되었습니다. 클라우드 제공자가 필요 없습니다. 즐거운 코딩 되세요!
로컬 가상 머신에서 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
이미지가 latest 태그와 함께 Docker Hub에 표시됩니다.

3. 서비스 실행
VM에 로그인하여 컨테이너가 정상적으로 실행 중인지 확인합니다.

애플리케이션은 이제 로컬 네트워크에서 접근 가능해야 합니다:
http://VM_IP/
왜 이렇게 동작하는가
로컬 가상 머신에 CI/CD 파이프라인을 설정하면 DevOps가 클라우드 제공업체에 의존하는 것이 아니라 논리와 아키텍처에 기반한다는 점을 보여줍니다. Bridged Mode로 설정한 VirtualBox를 사용하면 네트워킹과 배포 사이클을 완전히 제어할 수 있는 프로덕션 수준 환경을 얻을 수 있으며, 클라우드 비용이 발생하지 않습니다.
핵심 요약
- 인프라 유연성 – 동일한 설정이 모든 Linux 환경에서 작동합니다: VM, Raspberry Pi, 혹은 베어‑메탈 서버.
- 자동화 – 자체 호스팅 러너를 사용하면 배포 로직을 로컬에 유지하면서도 GitHub를 활용해 빌드할 수 있습니다.
- 모노레포 효율성 – Angular 프런트엔드와 Node.js 백엔드를 하나의 저장소에서 관리하면 CI/CD 워크플로우가 간소화됩니다.
로컬 환경을 설정하면서 어떤 어려움을 겪었나요? 댓글로 알려주세요!