1편에서 폰을 준비했으니, 이번엔 폰으로 붙을 서버 이야기다.
원래 개인 프로젝트들은 Hetzner 서버에 올려 두고 있었다. 독일 서버라 싸긴 한데, 영상 생성 같은 걸 돌리면 리소스가 너무 부족했다. 10월 12일에는 영상 렌더랑 브라우저 자동화가 같이 돌다가 8GB 메모리가 바닥나서 서버가 15분쯤 먹통이 됐다.
직접 서버를 구축해 보고 싶기도 해서 집에 두기로 했다.
이번에도 번개장터 매물을 API로 전수분석했다. 가격을 제일 먼저 봤는데, 램 32GB짜리 HP 800 G4 미니가 30만 원이었던 게 제일 컸다. 받아 보니 포트 하나가 불량이어서 3만 원 할인받았다. OS는 Linux Mint 22.3을 깔았다.

군대에 가면 1년 9개월 동안 서버에 손을 못 댄다. 그래서 고장 나도 혼자 일어나게 해 뒀다. 정전됐다가 전기가 들어오면 알아서 켜지게 BIOS를 바꿨고, 서버가 멈추면 워치독이 재부팅한다. 보안 업데이트는 알아서 깔리고, 새 커널이 깔리면 새벽 4시 반에 스스로 재부팅한다. 문제가 생기면 폰으로 알림이 온다.

배포도 자동이다. GitHub에 푸시하면 집 서버가 5분 안에 받아서 배포하고, 실패하면 되돌린 다음 폰으로 알려 준다. 폰으로 고쳐서 푸시만 하면 끝나게 하려고 했다.
GitHub이 서버로 밀어 넣는 게 아니라 서버가 5분마다 GitHub에 물어보게 한 건 보안 때문이다. 보통은 GitHub Actions가 서버에 SSH로 접속해서 배포하는데, 그러려면 내 서버에 들어올 수 있는 키를 GitHub에 맡겨 둬야 한다. 그 키가 새면 누구든 내 서버에 들어올 수 있다. 서버가 가져오는 방식이면 GitHub 쪽에는 내 서버로 들어올 수단이 아예 없다. 서버가 가진 것도 레포마다 하나씩 만든 읽기 전용 배포 키뿐이다. OpenGitOps 원칙에서도 이렇게 당겨 오는 방식을 권한다.
전체 구조는 이렇다.

처음엔 Cloudflare 터널로 열려고 했는데, 무료 플랜은 한국 방문자를 홍콩이나 미국으로 돌려서 몇 배씩 느려진다는 글이 많았다. 그래서 공유기 포트를 열고 Caddy가 직접 받게 했다.
서버를 옮기면서 규칙도 정했다. 나중에 폰으로 들어왔을 때 어디에 뭐가 있는지 헤매지 않으려고.
앱은 전부 Docker Compose로 돌리고, 레포 하나를 /srv/<앱> 폴더 하나에 둔다. 데이터도 Docker 볼륨 말고 그 폴더 안 data/에 둔다. 폴더 하나만 통째로 옮기면 앱이 그대로 옮겨지게 하려고. /srv는 리눅스 파일 시스템 표준(FHS)에서 "이 시스템이 제공하는 서비스의 데이터"를 두라는 곳이다.
/srv/<앱>/ ├── compose.yaml ├── .env # 비밀값 (레포에는 암호화해서) ├── data/ # DB·파일 ├── backup.sh # 이 앱을 백업하는 법 └── SRV.md # 서버 메모
서버 설정을 건드릴 땐 원래 파일을 고치지 않고 srv-로 시작하는 파일을 덧붙인다. 그러면 내가 손댄 게 명령 한 줄로 다 나온다.
find /etc /usr/local/bin -name '*srv-*'
새 앱을 올릴 때도 순서가 정해져 있다. srv-add-app으로 배포 키를 만들고, 비밀값은 SOPS로 암호화해서 레포에 넣는다. backup.sh만 만들어 두면 백업할 때 같이 들어가고, 바깥 공개는 공용 Caddy 하나에 한 줄 추가하면 된다.
Caddy 설정도 앱마다 블록 하나로 묶고, 보안 헤더랑 접속 기록은 공통으로 뺐다.
# 모든 앱 공통: 보안 헤더 + 앱별 접속 기록 (common) { header { Strict-Transport-Security "max-age=31536000" ?X-Content-Type-Options nosniff } log { output file /var/log/caddy/{args[0]}.log } } # ── portfolio (/srv/portfolio) ── yiye.in, www.yiye.in, comments.yiye.in { import common portfolio @www host www.yiye.in redir @www https://yiye.in{uri} 301 @comments host comments.yiye.in handle @comments { reverse_proxy 127.0.0.1:3031 # 댓글 } handle { reverse_proxy 127.0.0.1:3030 # 블로그 } }