본문으로 건너뛰기
Architecture#Docker#Docker Compose#Tailscale#nginx#셀프호스팅#홈서버#인프라

N100 미니 PC 한 대로 회사 사이트 셀프호스팅하기

플린랩스 홈페이지와 API, DB는 저전력 N100 미니 PC 한 대에서 돌아갑니다. 엣지 서버와 Tailscale로 외부 노출을 줄인 구성, 실제 자원 사용량, 저전력 서버의 한계를 정리했습니다.

Ian3 min read13

미니 PC 한 대

플린랩스 홈페이지(fleanlabs.io), 관리자 페이지, API, 데이터베이스는 모두 미니 PC 한 대에서 돌아갑니다. 클라우드 인스턴스가 아닙니다. 사양은 이렇습니다.

  • CPU: Intel N100 (4코어, Intel 기준 TDP 6W)

  • 메모리 16GB, SSD 500GB

  • OS: Ubuntu 24.04 LTS, Docker Compose

이 서버에는 홈페이지 말고도 사내 도구와 데모 서비스가 함께 올라가 있고, 지금 컨테이너 18개가 돌고 있습니다. 이 글에서는 이 구성으로 회사 사이트를 운영하면서 정한 원칙과, 직접 겪어 본 한계를 정리합니다.

요청이 들어오는 길

홈서버를 인터넷에 바로 노출하지 않습니다. 도메인은 공개 IP를 가진 작은 엣지 서버를 가리키고, HTTPS는 엣지에서 끝납니다. 엣지는 Tailscale로 만든 사설 네트워크를 통해 홈서버의 nginx로 요청을 넘깁니다. 실제로 홈서버 nginx 접근 로그를 보면, 요청은 모두 Tailscale 대역(100.x) 주소에서 들어옵니다.

방문자에서 엣지 서버, Tailscale, 홈서버 nginx를 거쳐 컨테이너로 가는 요청 흐름

홈서버의 nginx는 도메인을 보고 web, admin, api로 나눠 보냅니다. 컨테이너 포트는 모두 127.0.0.1에만 열어서, 같은 서버의 nginx만 접근할 수 있습니다.

yaml
# docker-compose.prod.yml
web:
  ports:
    - "${WEB_BIND:-127.0.0.1}:${WEB_PORT:-3000}:3000"
postgres:
  image: postgres:16-alpine
  # ports 없음: Compose 내부 네트워크에서 api만 접근

PostgreSQL은 호스트에 포트를 아예 열지 않습니다. 같은 Compose 네트워크 안의 api 컨테이너만 접근합니다. 우리는 집이나 사무실 회선에 포트를 직접 열기보다, 이렇게 엣지 한 곳만 외부에 두는 편이 관리하기 훨씬 낫다고 봅니다. 방화벽 규칙을 신경 쓸 곳이 엣지 하나로 줄어들기 때문입니다.

Compose 파일 하나

서비스 네 개는 docker-compose.prod.yml 하나로 관리합니다. 운영에 필요한 규칙도 이 파일에 함께 들어 있습니다.

  • api는 PostgreSQL의 healthcheck가 통과한 뒤에 뜨고, 시작할 때 prisma migrate deploy로 스키마를 적용합니다.

  • DB 데이터와 업로드 이미지는 각각 Docker 볼륨(fleanlabs_pgdata, fleanlabs_uploads)에 둡니다. 그래서 컨테이너를 다시 만들어도 데이터가 남습니다.

  • JWT 시크릿처럼 꼭 필요한 값은 ${JWT_ACCESS_SECRET:?set in .env} 형식으로 적었습니다. 값이 비어 있으면 컨테이너가 뜨지 않고 Compose가 바로 에러를 냅니다.

yaml
api:
  depends_on:
    postgres:
      condition: service_healthy
  environment:
    JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?set in .env}
  volumes:
    - fleanlabs_uploads:/app/uploads

설정을 빠뜨렸을 때 서비스가 이상하게 동작하는 것보다, 아예 뜨지 않는 편이 낫습니다. 홈서버처럼 혼자 관리하는 환경일수록 이런 안전장치가 중요합니다.

실제 자원 사용량

N100 한 대로 충분한지가 가장 궁금할 부분입니다. docker stats로 평상시 사용량을 재 봤습니다.

컨테이너별 메모리 사용량: web 168MiB, api 47MiB, admin 39MiB, PostgreSQL 30MiB

홈페이지 관련 컨테이너 네 개를 합쳐도 메모리는 약 284MiB입니다. 서버 전체로 보면 컨테이너 18개가 16GB 중 5.7GiB를 쓰고, load average는 0.2~0.3 수준입니다. 트래픽이 많지 않은 회사 사이트에는 남는 자원입니다.

저전력의 대가

남는 건 메모리이고, CPU는 그렇지 않습니다. 요청이 들어올 때 CPU를 많이 쓰는 작업은 바로 티가 납니다. 이전 글 Next.js 이미지 최적화, AVIF를 끄고 WebP만 남긴 이유에서 다룬 것처럼, 이 서버에서는 1920px AVIF 인코딩에 7.6초, WebP는 1.1초가 걸렸습니다. 그래서 이미지 포맷을 WebP로 정했습니다.

미니 PC 한 대로 얻는 것과 내주는 것 비교

장애 대응도 직접 해야 합니다. 정전이나 회선 장애가 나면 그대로 서비스 장애입니다. 하드웨어가 고장 나도 대신 바꿔 줄 곳이 없습니다. 백업은 문서로 정리한 pg_dump 절차로 받고 있는데, 이를 자동으로 돌리고 상태를 모니터링하는 일은 다음 과제로 남아 있습니다.

배포는 서버 안에서

배포에는 별도 레지스트리나 CI 배포 단계를 쓰지 않습니다. main에 push한 뒤 서버에서 받아서, 바뀐 서비스만 다시 빌드합니다.

main에 push한 뒤 git pull, N100에서 docker build, 컨테이너 교체로 이어지는 배포 흐름
bash
git pull origin main
docker compose -f docker-compose.prod.yml up -d --build web

빌드도 N100에서 하기 때문에 클라우드 빌드 서버보다 느리지만, 사이트 하나를 다시 올리는 데 무리는 없습니다. 주의할 점이 하나 있습니다. NEXT_PUBLIC_* 값은 빌드할 때 번들에 들어갑니다. 그래서 도메인이나 API 주소를 바꾸면 환경변수만 고치고 재시작해서는 반영되지 않고, 이미지를 다시 빌드해야 합니다.

마치며

트래픽이 크지 않은 회사 사이트와 사내 도구라면, 클라우드 인스턴스 여러 대보다 저전력 미니 PC 한 대가 합리적인 선택이라고 생각합니다. 서비스가 늘어도 비용은 그대로이고, 서버 안에서 무슨 일이 일어나는지 직접 볼 수 있습니다.

조건이 있습니다. 외부에는 엣지만 노출하고, 컨테이너는 loopback에만 열고, CPU를 많이 쓰는 작업은 설계 단계에서 피해야 합니다. 그리고 백업과 모니터링도 직접 챙겨야 합니다. 우리도 이 마지막 부분을 채워 가는 중입니다.

플린랩스와 함께

함께 미래를 만들 준비가 되셨나요?

기획·설계부터 구축, 운영까지 — 플린랩스가 전 과정을 함께합니다.

인천, 북위 37.53도 동경 126.73도