라즈베리파이 3B + SSD로 집에서 홈페이지 서버 만들기

2026년 10월 21일날씨: 맑음

집에 있던 라즈베리파이 3B와 오래된 SSD(Intel 520, 120GB)로 이 홈페이지를 직접 만들었다. 목표는 세 가지였다. 매달 나가는 호스팅 비용이 없을 것, 공유기 포트포워딩이나 도메인 구입 없이 인터넷에 공개할 것, 고장 나도 다시 세울 수 있게 모든 과정을 스크립트로 남길 것. 그 과정을 정리해 둔다.

구성 한눈에

구분내용
본체Raspberry Pi 3 Model B (메모리 1GB, USB 2.0)
운영체제Raspberry Pi OS Lite (Debian 13 trixie, 64비트), microSD 카드
데이터 저장Intel SSD 520 120GB + USB-SATA 어댑터 (JMicron JMS578)
웹 서버nginx + PHP 8.4 (FPM) + MariaDB 11.8
홈페이지WordPress + GeneratePress 테마 + bbPress (게시판)
인터넷 공개Tailscale Funnel (HTTPS 자동, 집 IP 비공개)
백업매일 새벽 3시 30분 자동, 14일치 보관

운영체제는 SD 카드, 데이터는 SSD

처음에는 SSD 하나로 부팅까지 하려 했지만 켜지지 않았다. 찾아보니 라즈베리파이 3B(3B+가 아닌 3B)는 USB 부팅이 기본으로 꺼져 있고, 한 번 쓰면 되돌릴 수 없는 OTP 메모리의 비트를 켜야 USB 장치로 부팅할 수 있다. 이 보드는 꺼져 있는 상태였다.

$ vcgencmd otp_dump | grep ^17:
17:1020000a      ← USB 부팅이 켜져 있으면 3020000a

OTP를 건드리는 대신, 운영체제는 SD 카드에 두고 자주 쓰고 지우는 데이터만 SSD에 두기로 했다. SD 카드는 같은 자리에 쓰기가 반복되면 수명이 빨리 닳는다. 그래서 쓰기가 몰리는 폴더 세 개를 SSD 위의 폴더로 연결(bind mount)했다. 운영체제 입장에서는 원래 자리에 있는 폴더처럼 보이지만, 실제 쓰기는 SSD에서 일어난다.

폴더내용실제 위치
/var/www홈페이지 파일, 올린 사진SSD /srv/data/www
/var/lib/mysql데이터베이스SSD /srv/data/mysql
/var/backups/homepage매일 백업SSD /srv/data/backups

한 가지 함정이 있다. SSD가 빠진 채로 부팅되면 DB와 웹서버는 SD 카드에 남아 있는 빈 폴더를 보고 ‘텅 빈 홈페이지’로 켜져 버린다. 그래서 systemd 설정에 RequiresMountsFor를 넣어, SSD가 연결되지 않으면 DB와 웹서버가 아예 시작되지 않게 했다. 홈페이지가 안 열리는 편이 엉뚱한 빈 화면이 나오는 것보다 낫다.

메모리 1GB에 맞추기

  • MariaDB: 버퍼 풀(innodb_buffer_pool_size) 64MB, performance_schema 끔, 최대 접속 30
  • PHP-FPM: 요청이 올 때만 프로세스를 띄우는 ondemand 방식, 최대 4개
  • 스왑: 라즈베리파이 OS 기본인 zram(메모리를 압축해서 쓰는 스왑) 약 900MB

이렇게 맞추고 나니 쉬는 상태의 메모리 사용량이 300MB 안팎으로, 여유가 넉넉하다.

포트포워딩 없이 인터넷에 공개하기

집 공유기에 포트를 열면 집 IP가 그대로 드러나고, 밖에서 집 안으로 바로 들어오는 문이 하나 생긴다. 그래서 Tailscale Funnel을 썼다. 라즈베리파이가 먼저 바깥의 Tailscale 중계 서버로 연결을 열어 두고, 방문자는 그 중계 서버에 접속한다. 중계 서버가 암호화된 통로로 요청을 라즈베리파이까지 전달해 주는 구조라, 공유기에서 열 포트가 하나도 없다. HTTPS 인증서도 자동으로 붙는다.

실제로 확인해 보니 공개 주소는 Tailscale 중계 서버를 가리키고 있었고, 집 인터넷 주소와는 달랐다.

재미있었던 것은 DNS였다. 공개 직후 구글 DNS는 한동안 주소를 찾지 못했는데, Tailscale의 원본 DNS 서버에 직접 물어보니 이미 등록돼 있었다. 너무 일찍 물어본 탓에 구글 DNS가 ‘그런 주소 없음’이라는 답을 잠시 기억(negative cache)하고 있었던 것이다. Cloudflare DNS에 먼저 나타났고, 10분 남짓 지나자 구글 DNS에도 나타났다. 새 주소를 만들었을 때 안 열린다고 바로 다시 조회하면 오히려 더 늦어질 수 있다.

공개하자마자 찾아온 손님들

공개한 지 30분도 안 돼 웹서버 기록에 이런 요청이 찍히기 시작했다.

GET /.git/config
GET /.vscode/sftp.json
GET /.env.local
POST /xmlrpc.php

사람이 아니라 인터넷 전체를 훑고 다니는 자동 수집 로봇들이다. 개발자가 실수로 올려 둔 설정 파일에서 비밀번호나 API 키를 빼 가려는 것이다. 숨김 파일(.으로 시작하는 파일)과 xmlrpc.php는 미리 막아 두었기 때문에 모두 403으로 차단됐다. 이후 보안 설정을 몇 가지 더 넣었다.

  • 관리자 로그인 화면: 같은 IP에서 1분에 10번까지만 시도 가능 (nginx limit_req, 넘으면 429 응답)
  • SSH: 비밀번호 로그인을 끄고 열쇠 파일로만 접속
  • 운영체제 보안 업데이트와 WordPress, 플러그인, 테마 자동 업데이트
  • 사진 업로드 폴더에서 PHP 실행 금지, 관리 화면에서 코드 수정 금지

서버용 백신은 깔지 않았다. 리눅스용 백신 ClamAV는 바이러스 정보를 메모리에 올려 두느라 1GB 가까이 쓰는데, 3B의 메모리가 1GB라 오히려 홈페이지가 멈춘다. 서버에서 실제로 위험한 것은 비밀번호 대입, 업데이트 안 된 프로그램, 악성 파일 업로드라서 위 설정으로 막는 편이 맞다.

네이버 블로그처럼 꾸미기

왼쪽에 프로필과 카테고리, 오른쪽에 글이 오는 네이버 블로그 구성이 익숙해서 GeneratePress 테마로 그렇게 맞췄다. WordPress에 없는 기능은 작은 플러그인(mu-plugin)으로 직접 만들었다.

  • 좋아요 버튼: 로그인 없이 누를 수 있고, 같은 곳에서는 글마다 하루 한 번만 센다
  • 방문자 수: 오늘, 어제, 전체. 같은 브라우저는 하루 한 번, 검색 로봇과 관리자 방문은 뺀다
  • 날짜와 시계: 방문자가 어느 나라에 있든 한국 시간으로 보여 준다
  • 게시판: 회원가입 없이 쓰고, 이름과 메일도 선택

설치, 인터넷 공개, 꾸미기를 모두 스크립트로 만들어 두었다. SD 카드가 고장 나도 스크립트 몇 개와 백업 파일만 있으면 같은 홈페이지를 다시 세울 수 있다.

삽질 기록

  • nginx 설정 중복: Debian 13의 nginx는 기본 설정 파일에 server_tokens off;가 이미 켜져 있다. 같은 줄을 한 번 더 넣었더니 설정 검사에서 “directive is duplicate”로 실패했다. 이대로 재부팅했다면 웹서버가 아예 켜지지 않았을 것이다. 예전 버전처럼 주석(#)으로 꺼져 있을 때만 켜도록 고쳤다.
  • 설치를 두 개 동시에: 설치 스크립트가 끝난 줄 알고 하나를 더 돌렸다가, 뒤의 것이 DB를 재시작하는 5초 사이에 앞의 것이 “데이터베이스 연결 오류”로 멈췄다. 서버 작업은 한 번에 하나씩.
  • 테마의 옛날 색: 테마를 켜자마자 설정을 먼저 저장했더니, 테마가 예전 버전에서 업그레이드된 사이트로 판단하고 옛날 기본색(파란 링크, 회색 배경)을 설정에 직접 써 넣었다. 색 항목을 전부 하나하나 지정해서 해결했다.

다음 글

가장 고생한 것은 소프트웨어가 아니라 전원이었다. 홈페이지가 갑자기 안 열려서 들여다보니 전압 부족으로 SSD가 통째로 끊겨 있었다. 다음 글에서 원인을 분석하고 전원 배선을 직접 만든 과정을 정리한다.

댓글 남기기