미니PC로 홈서버 구축하기 - Proxmox + Docker + GitHub Actions CI/CD 가이드
클라우드 비용 없이, 내 서버에서, 코드 push 한 번으로 자동 배포까지. 삽질의 기록을 그대로 담았다. (모놀리식)
온프레미스로 시작해 클라우드의 달콤함을 맛봤지만, '과금'이라는 현실의 벽에 부딪혀 결국 다시 온프레미스를 선택하게 되었습니다.. 그 과정을 다시한번 정리해봅니다..
목차
- 전체 아키텍처
- Docker VM 생성
- SSH 설정
- 디스크 확장
- GitHub Actions CI/CD 구성
- 디렉토리 및 환경변수 설정
- docker-compose.yml 구성
- GitHub Actions workflow 작성
- MySQL 데이터 복원
- 트러블슈팅 모음
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 를 선택해 직접 값을 입력한다.
| 항목 | 입력값 | 비고 |
|---|---|---|
| OS | Ubuntu 22.04 LTS | Debian 13은 Proxmox 8.4 미지원 |
| VM ID | 104 | |
| RAM | 2048MB | |
| CPU | 2코어 | |
| Disk | 10GB | 이후 확장 필요 |
| Machine Type | q35 | 최신 PCIe 방식 |
| CPU Model | Host | 실제 CPU 성능 그대로 사용 |
| Disk Cache | None | 안정성 우선 |
⚠️ 주의: 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:packagesread:packagesrepo
5-3. GitHub Secrets 등록
blog-v1 레포 → Settings → Secrets and variables → Actions 에서 등록한다.
| Secret 이름 | 값 |
|---|---|
SSH_HOST | 공인 IP |
SSH_USER | root |
SSH_KEY | 개인키 전체 내용 (cat ~/.ssh/github_actions) |
SSH_PORT | 2222 |
GHCR_TOKEN | 발급한 PAT |
GHCR_USERNAME | GitHub 유저명 |
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 접속 정보
| 항목 | 값 |
|---|---|
| Host | 192.168.55.83 (같은 네트워크면 내부 IP 사용) |
| Port | 3306 |
| Database | blog_v1 |
| Username | root |
| 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 구성을 다룰 예정이다.
