인프라CI / CD
7

미니PC로 홈서버 구축하기 - Proxmox + Docker + GitHub Actions CI/CD 가이드

클라우드 비용 없이, 내 서버에서, 코드 push 한 번으로 자동 배포까지. 삽질의 기록을 그대로 담았다. (모놀리식)

미니PC로 홈서버 구축하기 - Proxmox + Docker + GitHub Actions CI/CD 가이드

온프레미스로 시작해 클라우드의 달콤함을 맛봤지만, '과금'이라는 현실의 벽에 부딪혀 결국 다시 온프레미스를 선택하게 되었습니다.. 그 과정을 다시한번 정리해봅니다.. image


목차

  1. 전체 아키텍처
  2. Docker VM 생성
  3. SSH 설정
  4. 디스크 확장
  5. GitHub Actions CI/CD 구성
  6. 디렉토리 및 환경변수 설정
  7. docker-compose.yml 구성
  8. GitHub Actions workflow 작성
  9. MySQL 데이터 복원
  10. 트러블슈팅 모음

1. 전체 아키텍처

Proxmox 8.4.17 (미니PC, RAM 16GB, 6코어)
├── 100 Xpenology NAS
├── 101 NginxProxyManager       # 리버스 프록시 + SSL
└── 104 Docker VM               # IP: 192.168.55.83
     ├── blog-v1 (NestJS + React, 7000포트)
     └── blog-v1-mysql (MySQL 8.0)

왜 이 구조를 선택했나?

  • 블로그와 사이드 프로젝트를 VM 단위로 분리 → 블로그 장애가 사이드 프로젝트에 영향 없음
  • SEO 크롤러는 항상 접근 → 블로그 다운타임이 Search Console 크롤링 오류로 기록됨
  • NginxProxyManager를 별도 VM으로 → 도메인/SSL을 한 곳에서 중앙 관리

2. Docker VM 생성

Proxmox Shell에서 아래 명령어를 실행한다.

bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/vm/docker-vm.sh)"

스크립트 실행 시 설정 화면이 나온다. Advanced 를 선택해 직접 값을 입력한다.

항목입력값비고
OSUbuntu 22.04 LTSDebian 13은 Proxmox 8.4 미지원
VM ID104
RAM2048MB
CPU2코어
Disk10GB이후 확장 필요
Machine Typeq35최신 PCIe 방식
CPU ModelHost실제 CPU 성능 그대로 사용
Disk CacheNone안정성 우선

⚠️ 주의: OS 선택 시 반드시 ubuntu2204를 선택할 것. Debian 13은 Proxmox 8.4에서 unsupported debian version '13.1' 에러 발생.


3. SSH 설정

VM이 생성되면 Proxmox WebUI에서 104 → Console 로 접속한다.

3-1. openssh-server 설치

apt update && apt install -y openssh-server

3-2. root 로그인 허용

기본 설정은 root 직접 로그인이 막혀있다.

nano /etc/ssh/sshd_config

아래 두 줄을 수정한다.

PermitRootLogin yes
PasswordAuthentication yes
systemctl restart ssh
passwd root  # 비밀번호 설정

3-3. 로컬 PC에서 접속 확인

ssh root@192.168.55.83

4. 디스크 확장

VM 생성 시 10GB로 설정했지만 실제 파티션이 2.8GB만 잡혀있는 문제가 발생했다. Docker 이미지를 받다가 no space left on device 에러가 발생하면 아래 과정을 진행한다.

4-1. Proxmox Shell에서 디스크 크기 늘리기

qm disk resize 104 scsi0 +20G

4-2. 104 VM 안에서 파티션 확장

apt install -y cloud-guest-utils
growpart /dev/sda 1
resize2fs /dev/sda1

4-3. 확인

df -h
# /dev/sda1  30G  2.3G  26G  9%  /  ← 정상

5. GitHub Actions CI/CD 구성

코드를 push하면 자동으로 빌드 → 이미지 저장 → 서버 배포까지 이루어지도록 구성한다.

5-1. SSH 키 생성

104 VM에서 GitHub Actions 전용 SSH 키를 생성한다.

ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""

# 공개키를 authorized_keys에 등록
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

5-2. PAT(Personal Access Token) 생성

GHCR(GitHub Container Registry)에 이미지를 push하려면 PAT가 필요하다.

GitHub → Settings → Developer settings
→ Personal access tokens → Tokens (classic)
→ Generate new token (classic)

필요한 권한:

  • write:packages
  • read:packages
  • repo

5-3. GitHub Secrets 등록

blog-v1 레포 → Settings → Secrets and variables → Actions 에서 등록한다.

Secret 이름
SSH_HOST공인 IP
SSH_USERroot
SSH_KEY개인키 전체 내용 (cat ~/.ssh/github_actions)
SSH_PORT2222
GHCR_TOKEN발급한 PAT
GHCR_USERNAMEGitHub 유저명

5-4. ipTIME 포트포워딩

GitHub Actions는 외부 인터넷에서 VM으로 SSH 접속하기 때문에 포트포워딩이 필요하다.

고급 설정 → NAT/라우터 관리 → 포트포워드 설정

외부 포트 2222 → 내부 192.168.55.83:22

6. 디렉토리 및 환경변수 설정

mkdir -p /opt/blog-v1

.env (NestJS 환경변수)

# DB 연결
DB_HOST=mysql
DB_PORT=3306
DB_USERNAME=blog_v1
DB_PASSWORD=비밀번호
DB_DATABASE=blog_v1

# Cloudflare R2
r2.account-id=
r2.access-key=
r2.secret-key=
r2.bucket=blog
r2.public-url=

# JWT
jwt.secret=

⚠️ 주의: NestJS는 jdbc:mysql:// 형식이 아닌 DB_HOST, DB_PORT 형식으로 분리해서 입력해야 한다. jdbc:// 는 Java(Spring Boot) 문법이라 NestJS에서는 호스트명으로 인식하지 못한다.

.env.mysql (MySQL 컨테이너 전용)

MYSQL_ROOT_PASSWORD=비밀번호
MYSQL_DATABASE=blog_v1
MYSQL_USER=blog_v1
MYSQL_PASSWORD=비밀번호

MySQL과 NestJS 환경변수 파일을 반드시 분리해야 한다. 하나의 .env에 합치면 MySQL이 MYSQL_ROOT_PASSWORD를 못 찾아 재시작을 반복한다.


7. docker-compose.yml 구성

networks:
  blog-v1-network:
    driver: bridge

volumes:
  mysql-v1-data:

services:
  mysql:
    image: mysql:8.0
    container_name: blog-v1-mysql
    restart: always
    env_file:
      - .env.mysql
    volumes:
      - mysql-v1-data:/var/lib/mysql
    networks:
      - blog-v1-network

  blog-v1:
    image: ghcr.io/유저명/blog-v1:latest
    container_name: blog-v1
    restart: always
    ports:
      - "7000:7000"
    env_file:
      - .env
    networks:
      - blog-v1-network
    depends_on:
      - mysql

포인트:

  • MySQL 포트는 외부에 노출하지 않는다. NginxProxyManager가 도메인으로 라우팅한다.
  • volumes: mysql-v1-data 설정으로 docker compose down 후 재시작해도 데이터가 유지된다.
  • docker compose down -v 는 볼륨까지 삭제되므로 운영 환경에서는 절대 사용하지 않는다.

8. GitHub Actions workflow 작성

blog-v1 레포 → .github/workflows/deploy.yml 파일 생성

name: Deploy Blog V1

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: 체크아웃
        uses: actions/checkout@v3

      - name: GHCR 로그인
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ secrets.GHCR_USERNAME }}
          password: ${{ secrets.GHCR_TOKEN }}

      - name: 이미지 빌드 & Push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/blog-v1:latest

      - name: SSH 배포
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
            cd /opt/blog-v1
            docker compose pull
            docker compose up -d

배포 흐름:

main 브랜치 push
    ↓
GitHub Actions 실행 (GitHub 클라우드 서버)
    ↓
Docker 이미지 빌드
    ↓
GHCR에 이미지 push
    ↓
SSH로 104 VM 접속
    ↓
docker compose pull & up -d
    ↓
배포 완료

GitHub Actions가 빌드와 배포 명령을 대신해줄 뿐, 실제 서비스는 미니PC(104 VM) 에서 24시간 실행된다.


9. MySQL 데이터 복원

기존 SQL 덤프 파일이 있다면 DBeaver로 import할 수 있다.

9-1. MySQL 포트 임시 오픈

임시로 MySQL 포트를 외부에 노출한다. (데이터 복원 후 제거 필요)

docker-compose.yml mysql 서비스에 추가:

ports:
  - "3306:3306"
docker compose down
docker compose up -d

9-2. DBeaver 접속 정보

항목
Host192.168.55.83 (같은 네트워크면 내부 IP 사용)
Port3306
Databaseblog_v1
Usernameroot
Password설정한 비밀번호

⚠️ 주의: restore 시 blog_v1 유저가 아닌 root 유저로 접속해야 한다. blog_v1 유저는 SUPER 권한이 없어 Access denied 에러 발생.

9-3. 복원 완료 후 포트 제거

보안을 위해 MySQL 포트 외부 노출을 제거한다.

# docker-compose.yml에서 아래 부분 삭제
ports:
  - "3306:3306"
docker compose down
docker compose up -d

10. 트러블슈팅 모음

실제 구축 과정에서 마주친 에러들을 정리했다.

LXC Debian 13 에러

unsupported debian version '13.1'

Helper Scripts가 자동으로 Debian 13 템플릿을 받았는데 Proxmox 8.4가 미지원. → Docker VM 스크립트로 전환 후 OS를 Ubuntu 22.04로 선택해서 해결.


SSH Connection refused

ssh: connect to host 192.168.55.83 port 22: Connection refused

VM에 openssh-server가 설치되어 있지 않음. → Proxmox Console에서 apt install openssh-server 후 해결.


SSH Permission denied

root@192.168.55.83's password: Permission denied

Debian 기본값은 root 직접 로그인이 막혀있음. → sshd_config에서 PermitRootLogin yes 설정 후 해결.


GHCR push denied

denied: installation not allowed to Create organization package

Workflow permissions가 Read only로 설정되어 있음. → 레포 Settings → Actions → General → Read and write permissions 활성화.


no space left on device

write /var/lib/docker/tmp/GetImageBlob: no space left on device

VM 생성 시 10GB 설정했지만 실제 파티션이 2.8GB만 잡힘. → Proxmox에서 디스크 확장 후 growpart로 파티션 확장.


MySQL 재시작 반복

Database is uninitialized and password option is not specified

docker-compose.yml의 mysql env_file이 .env.mysql이 아닌 .env로 잘못 지정됨. → mysql 서비스의 env_file을 .env.mysql로 분리해서 해결.


NestJS DB 연결 실패

Error: getaddrinfo EAI_AGAIN jdbc:mysql://mysql:3306/blog_v1

.env에 Spring Boot 형식(spring.datasource.url=jdbc:mysql://...)으로 작성됨. NestJS는 jdbc:// 전체를 호스트명으로 인식함. → NestJS TypeORM 코드에서 실제 사용하는 변수명 확인 후 수정.

# 잘못된 형식 (Spring Boot)
spring.datasource.url=jdbc:mysql://mysql:3306/blog_v1

# 올바른 형식 (NestJS)
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=blog_v1

마무리

✅ Proxmox 위에 Docker VM 생성
✅ GitHub Actions로 CI/CD 자동화
✅ GHCR에 이미지 빌드 및 push
✅ SSH 배포 자동화
✅ MySQL 데이터 복원 완료
✅ blog-v1 서비스 운영 중

다음 포스팅에서는 NginxProxyManager HTTPS 설정blog-v2 배포, 사이드 프로젝트 VM 구성을 다룰 예정이다.

#온프레미스#도커#깃허브액션#CI/CD#proxmox

댓글

(0)
미니PC로 홈서버 구축하기 - Proxmox + Docker + GitHub Actions CI/CD 가이드 | 강민석의 개발블로그