
왜 만들었나
가족 폰 2대(Android)에 쌓인 사진·영상이 1TB 가까이 되었습니다. 클라우드 요금을 계속 내는 대신 남는 PC로 직접 백업 서버를 만들기로 했고, 목표는 다음과 같았습니다.
- 사람이 신경 쓰지 않아도 새 사진이 서버에 자동으로 올라갈 것
- 폰에서 지워도 서버에서는 지워지지 않을 것 (단방향 백업)
- 인터넷에 포트를 하나도 열지 않을 것
- 서버를 같은 네트워크의 다른 장비와 분리할 것
- 디스크 이상, 서버 다운, 백업 실패를 알림으로 알 수 있을 것
- 백업이 실제로 복원되는지 확인해 볼 것
이 글은 구축 과정과 중간에 막혔던 부분을 순서대로 정리한 기록입니다.
환경
| 구분 | 구성 |
|---|---|
| 서버 PC | Intel 9세대 i5 (6코어, 내장 그래픽), RAM 16GB |
| OS 디스크 | SATA SSD 250GB |
| 앱·DB 디스크 | SATA SSD 500GB |
| 원본 디스크 | 8TB 헬륨 HDD 1개 (외장하드에서 꺼낸 5400rpm급, 사용 시간 약 6.7년) |
| OS | TrueNAS Community Edition 25.10.7 |
| 사진 앱 | Immich (TrueNAS 앱, 설치 v3.2.4 → 이후 v3.3.0) |
| 원격 접속 | Tailscale (TrueNAS 앱) |
| 네트워크 | 기존 내부망, 차세대 방화벽(UTM) 뒤에 설치 |
| 클라이언트 | Android 폰 2대 |
HDD가 한 개뿐이고 오래된 디스크라는 점이 이 구성의 가장 큰 약점입니다. 그래서 “2차 사본을 확보하기 전까지 폰 원본은 지우지 않는다”를 원칙으로 두었습니다.
1단계: 디스크 확인과 TrueNAS 설치
설치 이미지 받기
TrueNAS 다운로드 페이지가 뉴스레터 가입 화면을 먼저 보여 줘서 링크를 찾기 어려웠습니다. 공식 배포 서버(download.truenas.com)에서 버전 폴더를 직접 열면 ISO와 체크섬 파일을 바로 받을 수 있습니다. 파일 이름에는 예전 이름인 SCALE이 그대로 남아 있습니다.
:: Windows에서 ISO 해시 확인
certutil -hashfile "TrueNAS-SCALE-25.10.7.iso" SHA256
type "TrueNAS-SCALE-25.10.7.iso.sha256"
두 값이 같으면 정상입니다.

하드웨어
- HDD는 USB 독이 아니라 메인보드 SATA에 직결했습니다. USB 연결 위의 ZFS는 연결이 끊기면 풀이 손상될 수 있습니다.
- 외장하드에서 꺼낸 HDD는 SATA 전원의 3.3V 핀(PWDIS) 때문에 인식되지 않는 경우가 있습니다. 이번에는 BIOS에서 바로 인식되었습니다.
- BIOS는 AHCI, UEFI로 설정했습니다. 정전 후 자동으로 켜지는 설정(AC Power Recovery)은 나중에 정전을 겪고 나서야 켰습니다. 처음부터 켜 두는 것을 권합니다.
설치 대상 선택
설치 화면에서 디스크 3개가 보이면 용량과 모델명으로 250GB SSD만 선택합니다. TrueNAS는 설치한 디스크 전체를 OS 전용으로 쓰므로, 다른 디스크를 고르면 그 디스크를 데이터 저장에 쓸 수 없게 됩니다.

설치 후에는 다음 두 가지를 바로 바꿨습니다.
- 시간대: 기본값이 미국 서부 시간이라 System → General Settings → Localization에서
Asia/Seoul로 변경. 이대로 두면 스냅샷과 Cron 일정이 모두 어긋납니다. - UI 언어: 커뮤니티 번역은 일부 메뉴가 빠져 있어 English로 두는 편이 문서와 대조하기 쉽습니다.
2단계: HDD 장기 검사 (TrueNAS 25.10은 GUI 없음)
TrueNAS 25.10부터 S.M.A.R.T. 검사를 실행하고 예약하는 GUI가 빠졌습니다. 그래서 System → Shell에서 smartctl로 실행했습니다.
# 대상 디스크 확인 (모델명·일련번호)
sudo smartctl -i /dev/sdb
# 장기 검사 시작 (읽기 전용, 데이터는 바뀌지 않음)
sudo smartctl -t long /dev/sdb
# 진행률 확인
sudo smartctl -a /dev/sdb | grep -i -A1 "self-test execution"
# 결과 확인
sudo smartctl -l selftest /dev/sdb
sudo smartctl -A /dev/sdb
8TB 5400rpm 디스크는 장기 검사에 약 18시간(1088분)이 걸렸습니다. 결과에서 다음 항목을 확인했습니다.
| ID | 항목 | 기준 |
|---|---|---|
| 5 | Reallocated_Sector_Ct | 0 |
| 197 | Current_Pending_Sector | 0 |
| 198 | Offline_Uncorrectable | 0 |
| 199 | UDMA_CRC_Error_Count | 0 |
| 22 | Helium_Level (헬륨 디스크) | 100 |
모두 정상이어서 대량 업로드를 진행했습니다.

검사 중에는 재부팅하면 검사가 중단되므로 서버를 켜 둔 상태로 다른 작업을 병행했습니다.
3단계: 풀과 데이터셋 구성
| 풀 | 디스크 | 용도 |
|---|---|---|
| apps | SSD 500GB | 앱, Immich DB, 시스템 데이터셋 |
| DATA | HDD 8TB | 사진·영상 원본 |
- 디스크가 한 개씩이라 레이아웃은 Stripe입니다. “Stripe는 권장하지 않는다”는 경고가 나오지만 구조상 피할 수 없습니다.
- 풀 암호화는 쓰지 않았습니다. TrueNAS 문서도 풀 단위 암호화를 권장하지 않으며, 키를 잃으면 전체를 복구할 수 없습니다.
- 풀 이름은 생성 후 바꿀 수 없습니다.
- Apps → Configuration → Choose Pool에서 앱 저장 풀을 SSD 풀로 지정했습니다.
- SSD 풀은 Auto TRIM을 켰습니다.

4단계: 방화벽에서 사진 서버 분리
처음에는 서버를 내부망 공유기 아래에 두었는데, 이렇게 하면 같은 대역의 다른 장비와 방화벽을 거치지 않고 직접 통신합니다. 같은 서브넷 안의 통신은 방화벽 규칙으로 막을 수 없습니다.
그래서 방화벽의 빈 물리 포트 하나를 DMZ 존으로 지정하고, 서버를 그 포트에 직결했습니다.
[내부망] ── 방화벽 ── [DMZ 포트: 사진 서버 전용, 사설 /24 대역]
│
인터넷
- 서버에는 공인 IP를 주지 않고 사설 대역을 썼습니다. 규칙을 실수로 열어도 인터넷에서 바로 들어올 수 없기 때문입니다.
- 내부 PC들이 쓰는 흔한 사설 대역(예: 192.168.0.x)과 겹치지 않는 대역을 골랐습니다. 겹치면 PC가 서버를 자기 공유기 안에서 찾다가 접속에 실패합니다.
- DMZ 존의 장치 접근(방화벽 관리 화면 HTTPS, SSH 등)은 모두 끕니다.
방화벽 규칙 (위에서부터)
| 순서 | 출발 | 대상 | 서비스 | 동작 |
|---|---|---|---|---|
| 1 | 관리 PC 1대 | 사진 서버 | HTTPS | 허용 |
| 2 | 사진 서버 | WAN | DNS, HTTP, HTTPS, NTP, ping, Tailscale UDP | 허용 |
| 3 | DMZ | LAN, VPN, WiFi | 전체 | 차단(Drop) |
| 4 | LAN, WAN, VPN, WiFi | DMZ | 전체 | 차단(Drop) |
- 인터넷에서 들어오는 DNAT는 만들지 않았습니다.
- 차단은 Reject가 아니라 Drop으로 설정했습니다. 응답을 주지 않아야 내부 스캔에 정보가 덜 노출됩니다.
- 허용 규칙의 출발지는 “모두”가 아니라 서버 객체로 정확히 지정합니다.
SNAT
기존 기본 SNAT 규칙이 “지정한 호스트만” 변환하도록 되어 있어서 새 대역은 인터넷으로 나가지 못했습니다. 기본 규칙은 건드리지 않고 원본을 사진 서버 하나로 한정한 SNAT(MASQ) 규칙을 기본 규칙 위에 추가했습니다. 원본과 대상을 반대로 넣는 실수를 하기 쉬우니 저장 전에 확인이 필요합니다.
서버 IP 변경 (콘솔 메뉴)
케이블을 옮기면 웹 UI 접속이 끊기므로 서버 앞 콘솔에서 바꿨습니다.
1) Configure network interfaces→ipv4_dhcp: No,aliases: <서버 IP>/24→ Save- 목록 화면에서
a(적용) → 바로p(영구 저장). 약 60초 안에p를 누르지 않으면 원래 설정으로 되돌아갑니다. 2) Configure network settings→ 게이트웨이(방화벽 포트 IP), DNS(1.1.1.1, 8.8.8.8)
차단 테스트
ping -c 3 1.1.1.1 # 인터넷: 성공해야 함
ping -c 3 google.com # DNS: 성공해야 함
ping -c 3 -W 2 <내부망 서버 IP> # 내부망: 100% loss여야 함
sudo chronyc sources # 시간 동기화: ^* 표시가 있어야 함
관리 PC가 아닌 다른 PC에서 웹 UI 접속을 시도하면 방화벽 차단 페이지가 표시되었습니다. 로그를 확인하니 DMZ 차단 규칙에 걸렸고 전송량은 0바이트였습니다. 웹 통신이 방화벽 규칙에서 차단되면 방화벽이 차단 페이지를 대신 보여 주는 것으로, 정상 동작입니다.
5단계: Tailscale 설치
Apps → Discover Apps → Tailscale로 설치했습니다.
| 항목 | 값 | 이유 |
|---|---|---|
| Auth Key | 일회용 키 (Reusable 끔, Ephemeral 끔) | 서버 1대만 등록 |
| Advertise Exit Node | 끔 | 서버를 경유한 인터넷 사용 금지 |
| Advertise Routes | 비움 | 외부에서 내부망으로 들어오는 경로 차단 |
| Accept Routes / Accept DNS | 끔 | |
| Host Network | 켬 | 서버 자신의 웹 UI와 Immich에 접속 |
| Userspace | 끔 | Host Network와 함께 사용 |
등록 후 관리 콘솔에서 서버의 Disable key expiry를 켰습니다. 켜지 않으면 일정 기간 뒤 재인증을 요구하면서 백업이 끊깁니다.
두 번째 사용자는 같은 계정에 로그인시키지 않고, 본인 계정으로 가입한 뒤 서버 한 대만 기기 공유(Share)했습니다.
테스트: 폰을 Wi-Fi가 아닌 LTE/5G에 두고 Tailscale을 켠 상태에서 서버에 접속되는지, 끈 상태에서는 접속되지 않는지 확인했습니다.
6단계: Immich 설치
저장소가 3개 항목뿐이었다
예전 문서에는 Immich 저장소가 7개(library, upload, thumbs 등)라고 되어 있어서 데이터셋을 미리 7개 만들어 두었습니다. 그런데 설치한 앱 버전(1.14.x)의 설치 화면에는 3개 항목만 있었습니다.
| 항목 | 설정 |
|---|---|
| Data Storage (Upload Location) | Host Path → HDD 풀의 데이터셋 |
| Machine Learning Cache | ixVolume |
| Postgres Data Storage | Host Path → SSD 풀의 데이터셋 |
Data Storage 안에 library, upload, thumbs, encoded-video, profile, backups를 Immich가 직접 만듭니다. 결과적으로 DB 본체는 SSD에, 원본과 DB 자동 백업은 HDD에 있게 되어, SSD가 고장 나도 DB 백업은 남는 구조가 되었습니다. 미리 만든 빈 데이터셋 6개는 삭제했습니다.
그 밖의 설정:
- 데이터셋은 Apps 프리셋으로 만들었습니다. 앱 실행 계정(UID 568)에 쓰기 권한이 자동으로 부여됩니다.
- Automatic Permissions를 켰습니다. DB 컨테이너는 별도 계정으로 실행되므로 pgData 폴더 소유자를 맞춰야 시작됩니다.
- GPU Passthrough로 내장 그래픽을 연결해 영상 변환에 Quick Sync를 쓰도록 했습니다.
- DB·Redis 비밀번호는 영문과 숫자만 썼습니다. 이유는 아래 “막혔던 부분”에 있습니다.
- Host IPs는 비워 두었습니다. Tailscale IP로 지정하면 재부팅 시 Immich가 Tailscale보다 먼저 시작될 때 포트를 열지 못해 실패할 수 있습니다. 접근 제한은 방화벽이 담당합니다.
서버 설정 (Immich 관리 화면)
| 항목 | 값 |
|---|---|
| 저장소 템플릿 | {{y}}/{{y}}-{{MM}}/{{filename}} |
| 사용자별 저장소 레이블 | 영문 이름 |
| DB 자동 백업 | 매일 02:00, 14개 보관 |
| 휴지통 | 30일 |
| 영상 변환 가속 | Quick Sync |
관리자 계정은 서버 설정 전용으로 쓰고, 폰 앱에는 사용자 계정만 로그인했습니다.
폰 설정
- Immich 앱 서버 주소:
http://<Tailscale IP>:30041(https가 아님) - 원격 삭제 동기화(Sync remote deletions) 끔, 공간 확보(Free up space) 사용 안 함
- 백업 조건: Wi-Fi + 충전 중
- Tailscale: 상시 VPN 켬, VPN 없는 연결 차단은 끔 (켜면 Tailscale이 끊길 때 폰 인터넷 전체가 막힘)
- Tailscale, Immich 모두 배터리 사용 “제한 없음” + 절전 예외 앱 등록
새벽에만 올라갈 것으로 예상했는데, 실제로는 충전 중이거나 폰을 쓰지 않을 때 수시로 올라가고 앱을 열면 즉시 올라갔습니다. 당일 아침에 찍은 사진이 바로 올라와 있어서 예약 업로드 앱으로 바꿀 필요는 없었습니다.
7단계: 스냅샷, 백업, 알림, 디스크 감시
ZFS 스냅샷
Data Protection → Periodic Snapshot Tasks
| 데이터셋 | 시각 | 보관 |
|---|---|---|
| 원본 데이터셋 (HDD) | 매일 07:00 | 30일 |
| DB 데이터셋 (SSD) | 매일 07:00 | 14일 |
- 02:00 DB 백업 파일이 07:00 스냅샷에 함께 들어가도록 시간을 배치했습니다.
- “Allow Taking Empty Snapshots”는 켰습니다. 날짜가 빠짐없이 생기면 작업이 실패했는지 목록만 보고 알 수 있습니다.

TrueNAS 설정 파일 백업
TrueNAS 25.10에서는 System → Advanced Settings → Manage Configuration → Download File 위치에 있습니다(이전 버전은 General Settings). Export Password Secret Seed를 체크해야 새 부팅 디스크에서 복원할 때 저장된 인증 정보를 쓸 수 있습니다. 비밀 정보가 들어 있으므로 평소 쓰는 PC나 공유 폴더에 두지 않았습니다.
알림: 텔레그램
방화벽에서 메일 포트를 추가로 열지 않으려고 HTTPS만으로 동작하는 텔레그램을 썼습니다.
- BotFather로 봇 생성 → 토큰 보관
- 내 봇에서 시작(Start)
- 내 Chat ID 확인
- System → Alert Settings → Alert Services → Add → Telegram, Level은 Warning
서버 다운 감지: Healthchecks.io
TrueNAS 알림은 서버가 꺼지면 보낼 수 없습니다. 외부 감시 서비스에 5분마다 신호를 보내고, 끊기면 그쪽에서 알리게 했습니다. Period 5분, Grace 15분으로 설정했습니다.
System → Advanced Settings → Cron Jobs (5분마다):
curl -fsS -m 10 -o /dev/null http://127.0.0.1:30041/api/server/ping && curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<UUID> || curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<UUID>/fail
- Immich가 응답하면 정상 신호, 응답하지 않으면 실패 신호를 보냅니다.
- 서버가 꺼지거나 인터넷이 끊기면 신호 자체가 끊겨 20분 뒤 알림이 옵니다.
- Immich 앱을 직접 정지했다가 다시 시작해서 다운 알림과 복구 알림이 모두 오는지 확인했습니다.

정기 디스크 검사
재부팅하면 sda, sdb 같은 장치 이름이 바뀝니다(실제로 바뀌었습니다). 그래서 일련번호가 들어간 고정 경로를 썼습니다.
lsblk -d -o NAME,SIZE,MODEL,SERIAL
ls -l /dev/disk/by-id/ | grep -v part
점검 스크립트(/mnt/<SSD 풀>/scripts/disk-health.sh):
#!/bin/bash
# 디스크 상태 점검: 기준값보다 늘어나거나 검사 실패 시 Healthchecks.io로 fail 전송
URL="https://hc-ping.com/<UUID>"
DISKS="ata-<HDD모델>_<일련번호> ata-<SSD모델>_<일련번호> ata-<SSD모델>_<일련번호>"
declare -A BASE
BASE[ata-<SSD모델>_<일련번호>_199]=394 # 과거 누적값을 기준으로 둔 항목
MSG=""
for d in $DISKS; do
DEV="/dev/disk/by-id/$d"
if [ ! -e "$DEV" ]; then MSG="$MSG\n$d: 디스크 인식 안 됨"; continue; fi
H=$(smartctl -H "$DEV" | grep -iE "result|status")
echo "$H" | grep -qiE "PASSED|OK" || MSG="$MSG\n$d: 전체 상태 이상 ($H)"
for id in 5 197 198 199; do
v=$(smartctl -A "$DEV" | awk -v id="$id" '$1==id {print $10}')
b=${BASE[${d}_${id}]:-0}
if [ -n "$v" ] && [ "$v" -gt "$b" ] 2>/dev/null; then MSG="$MSG\n$d: ID $id 값 $v (기준 $b)"; fi
done
st=$(smartctl -l selftest "$DEV" | grep -m1 "^# 1")
if [ -n "$st" ] && ! echo "$st" | grep -qiE "Completed without error|in progress|Interrupted|Aborted by host"; then
MSG="$MSG\n$d: 최근 자가 검사 이상 ($st)"
fi
done
if [ -z "$MSG" ]; then
curl -fsS -m 10 --retry 3 -o /dev/null "$URL"
else
printf "%b" "$MSG" | curl -fsS -m 10 --retry 3 -o /dev/null --data-binary @- "$URL/fail"
fi
sudo chmod 700 /mnt/<SSD 풀>/scripts/disk-health.sh # UUID가 있으므로 root만 읽게
Cron 등록(Run As User: root):
| 작업 | 일정 | 명령 |
|---|---|---|
| 상태 점검 + 알림 | 매일 08:00 | bash /mnt/<SSD 풀>/scripts/disk-health.sh |
| 짧은 검사 | 매주 수 04:30 | for d in <by-id 3개>; do smartctl -t short /dev/disk/by-id/$d; done |
| 장기 검사 | 매월 15일 09:00 | for d in <by-id 3개>; do smartctl -t long /dev/disk/by-id/$d; done |
검사 명령은 검사를 시작만 시키고, 결과는 다음 날 점검 스크립트가 확인합니다.

8단계: 복원 테스트
백업이 있다는 것과 복원된다는 것은 다릅니다. 원본은 건드리지 않고 세 가지를 확인했습니다.
A. 스냅샷 속 파일이 원본과 같은지
sudo ls /mnt/<원본 데이터셋>/.zfs/snapshot/
sudo sha256sum /mnt/<원본 데이터셋>/.zfs/snapshot/<스냅샷명>/library/<사용자>/<연>/<연-월>/<파일> \
/mnt/<원본 데이터셋>/library/<사용자>/<연>/<연-월>/<파일>
두 해시값이 일치했습니다.

B. DB 백업 파일이 깨지지 않았는지
sudo gzip -t /mnt/<원본 데이터셋>/backups/<백업파일>.sql.gz && echo "압축 파일 정상"
sudo zcat /mnt/<원본 데이터셋>/backups/<백업파일>.sql.gz | head -n 20
PostgreSQL database dump 문구를 확인했습니다.
C. 스냅샷 복제본으로 Immich를 하나 더 띄우기
- 원본·DB 데이터셋의 스냅샷을 각각 Clone To New Dataset으로 복제
- Install Another Instance로 테스트용 Immich 설치: 포트 30042, 머신러닝 끔, 저장소는 복제본, Automatic Permissions 끔
- 브라우저로 접속해 사용자 계정으로 로그인 → 사진이 정상 표시되는 것을 확인
- 테스트 앱과 복제본 삭제 (복제본이 남아 있으면 원본 스냅샷이 자동 삭제되지 않음)

카탈로그에 최신 버전만 있어서, 이 테스트가 복원과 함께 새 버전으로의 DB 업그레이드 리허설도 겸하게 되었습니다. 이 과정에서 두 가지 문제를 만났고, 아래에 정리했습니다. 리허설이 성공한 뒤 수동 스냅샷을 찍고 운영 Immich를 업데이트했습니다.
막혔던 부분과 해결
1. Ventoy로 만든 USB 부팅 실패
Unable to find a medium containing a live file system 오류로 설치 화면 전에 멈췄습니다. Debian Live 계열 ISO를 Ventoy 일반 모드로 부팅할 때 나는 증상입니다. Rufus의 DD 이미지 모드로 USB를 새로 만들어 해결했습니다. DD 모드로 만든 USB는 Windows 탐색기에서 “포맷하라”는 창이 뜨는데, 여기서 포맷을 누르면 안 됩니다.
2. smartctl, grep 입력 실수
smartctl -t long의 하이픈이 다른 문자로 바뀌어You have provided 2 device names오류가 났습니다.grep "self-test execution"은 출력의 대문자Self-test와 맞지 않아 아무것도 나오지 않았습니다.grep -i로 해결했습니다.- 웹 Shell에 긴 명령을 붙여 넣으면 끝 글자가 잘리거나 제어 문자(
^[[200~)가 섞이는 경우가 있습니다. 붙여 넣은 뒤 마지막 줄을 확인하는 습관이 필요합니다.
3. Immich 접속이 안 됨 → https로 입력
Immich 앱 포트는 http로만 응답합니다. 브라우저에 https로 입력해서 접속되지 않았습니다. Tailscale 구간이 암호화되므로 http로 써도 됩니다.
4. Tailscale이 중계(relay)로만 연결됨
내부망 Wi-Fi에서 폰으로 대량 업로드를 하려는데 tailscale status에 relay "tok"이 표시되었습니다. 해외 중계 서버를 거치는 연결이라 대량 업로드에는 너무 느립니다.
# Tailscale 앱 컨테이너 셸에서
tailscale status
tailscale ping <폰 Tailscale IP>
방화벽 로그를 보니 서버와 폰이 서로 UDP로 직접 연결하려다 DMZ↔LAN 차단 규칙에 막혀 있었습니다. 업로드 기간에만 폰이 연결된 공유기 1대 ↔ 서버, UDP 전체 포트를 허용하는 임시 규칙 2개를 차단 규칙 위에 추가했고, direct로 바뀌었습니다. 이후 수신 속도는 약 340Mbps였습니다. 대량 업로드가 끝난 뒤에는 이 규칙을 비활성화했습니다.
집이나 셀룰러에서는 중계 연결이 되지만, 사진 한 장에 몇 초 수준이라 일상 백업에는 문제가 없었습니다.
5. 원본이 UUID 폴더와 Other 폴더에 저장됨
업로드를 시작한 뒤 확인해 보니 경로가 library/<사용자 UUID>/2025/Other/07/파일.jpg였습니다.
- 사용자 저장소 레이블이 비어 있어 UUID가 폴더 이름이 되었습니다.
- 템플릿이 앨범 기준 프리셋이라 앨범이 없는 사진은
Other로 들어갔습니다. 이 방식은 사진을 앨범에 넣거나 빼면 파일 위치가 바뀔 수 있습니다.
레이블을 설정하고 템플릿을 날짜 기준으로 바꾼 뒤, 관리 화면의 작업 대기열 → Storage Template Migration을 실행해 기존 파일을 옮겼습니다. 같은 데이터셋 안에서 이동하므로 빠르게 끝났습니다.
sudo ls /mnt/<원본 데이터셋>/library
sudo find /mnt/<원본 데이터셋>/library/<레이블> -type f | head -n 5
6. 정전 후 서버가 다시 켜지지 않음
정전 뒤 원격 접속이 되지 않았습니다. 같은 UPS에 연결된 다른 서버들은 살아 있었습니다.
sudo journalctl -b -1 -n 50 --no-pager # 직전 부팅의 마지막 로그
last -x | head -n 10 # 부팅·종료 기록 (crash 표시 확인)
sudo zpool status -x # all pools are healthy 확인
마지막 로그가 오류 없이 정상 기록 중에 끊겨 있었고, 종료 기록은 crash였습니다. 서버 플러그가 UPS의 배터리 백업 콘센트에 꽂혀 있지 않아 전원이 바로 끊긴 것이었습니다. BIOS에 정전 후 자동 켜짐 설정도 없었습니다.
- 배터리 백업 콘센트로 옮기고 BIOS AC Power Recovery = Power On으로 설정
- 풀과 디스크 점검 결과 이상 없음 (Power-Off_Retract_Count만 늘어남)
- 이 일을 계기로 위의 Healthchecks.io 다운 감지를 추가
참고로 서버가 꺼져 있었는데도 방화벽에는 포트가 1000Mbps 연결됨으로 표시되었습니다. 메인보드 대기 전원으로 랜 카드가 링크를 유지했기 때문입니다. 포트 상태만 보고 서버가 켜져 있다고 판단하면 안 됩니다.
UPS의 USB 연동(정전 시 자동 종료)은 하지 않았습니다. 다른 서버와 같이 쓰는 UPS라서 설정 실수로 “UPS 출력 차단” 명령이 나가면 다른 장비까지 꺼질 수 있기 때문입니다.
7. 점검 스크립트가 바로 실패 알림을 보냄
OS용 SSD의 UDMA_CRC_Error_Count가 394였습니다. 디스크와 메인보드 사이 전송 오류 누적값으로, 저장 매체 손상과는 별개이고 줄어들지 않는 값입니다. 현재 값을 기준값으로 저장하고 늘어날 때만 알리도록 스크립트를 고쳤습니다(위 스크립트의 BASE 배열). 값이 늘어나면 SATA 케이블을 점검합니다.
8. 테스트용 Immich 설치 실패: Redis password cannot contain [#]
비밀번호에 #이 들어가 설치 템플릿 생성 단계에서 거부되었습니다. 앱 비밀번호는 영문과 숫자만 쓰는 것이 안전합니다.
9. 테스트용 Immich 시작 실패: password authentication failed for user "immich"
복제한 DB에는 운영 Immich의 DB 비밀번호가 저장되어 있습니다. 테스트 앱에는 다른 값을 넣어서 접속에 실패했습니다. 운영 앱 Edit 화면에서 비밀번호를 확인해(저장하지 않고 취소) 같은 값을 넣어 해결했습니다. 실제 재해 복원에서도 DB 비밀번호가 없으면 DB를 살릴 수 없다는 뜻이므로, 비밀번호 관리자에 정확한 값이 있는지 다시 확인했습니다.

10. 테스트용 Immich 시작 실패: EACCES: permission denied … /data/encoded-video/.immich
DB 마이그레이션까지는 성공했는데 폴더 확인 단계에서 멈췄습니다.

sudo zfs get acltype,aclmode <원본 데이터셋> <복제 데이터셋>
| 데이터셋 | acltype | aclmode | SOURCE |
|---|---|---|---|
| 원본 (Apps 프리셋) | nfsv4 | passthrough | local |
| 복제본 | posix | discard | inherited |
ZFS 복제본은 이 속성을 원본이 아니라 상위 데이터셋에서 물려받습니다. 그래서 파일에 붙어 있는 NFSv4 권한이 무시되었습니다. 복제본에만 원본과 같은 값을 지정해서 해결했습니다.
sudo zfs set acltype=nfsv4 aclmode=passthrough <복제 데이터셋>
스냅샷 롤백(원래 데이터셋을 되돌리는 방식)에서는 속성이 유지되므로 이 문제가 생기지 않습니다.

11. 수동 스냅샷에 데이터가 안 들어갈 뻔함
수동 스냅샷을 만들 때 Dataset 기본값이 최상위 풀로 잡혀 있었습니다. Recursive를 끈 상태로 최상위를 고르면 하위 데이터셋의 실제 데이터는 스냅샷에 포함되지 않습니다. 실제 데이터가 있는 데이터셋을 직접 선택해야 합니다.

12. 텔레그램에서 가짜 BotFather
검색 결과 첫 번째가 인증 표시 없이 “구독”과 다른 봇으로의 이동을 유도하는 사칭 계정이었습니다. https://t.me/BotFather 링크로 직접 열고 파란 인증 표시를 확인해야 합니다. Healthchecks.io 텔레그램 연동도 텔레그램에서 검색하지 않고 서비스 화면의 링크로 열었습니다.
업데이트 절차
복원 테스트에서 정리한 순서로 Immich를 업데이트했습니다.
- 원본·DB 데이터셋 수동 스냅샷 (최상위가 아니라 실제 데이터셋 선택)
- 릴리스 노트 확인, 가능하면 복제본으로 리허설
- Apps → 해당 앱 → Update (Update All은 쓰지 않음)
- 확인
curl -sS -m 5 http://127.0.0.1:30041/api/server/ping # {"res":"pong"}
- 폰 Immich 앱도 최신으로 업데이트
- 문제가 생기면: Roll Back → 앱 정지 → 스냅샷 Rollback → 앱 시작
업데이트 중에는 다운 감지 알림이 한 번 올 수 있습니다.

정리 및 주의사항
결과적으로 갖춘 것
- 폰 2대 → 서버 자동 단방향 백업 (Wi-Fi + 충전 조건, 당일 사진도 자동 업로드)
- 인터넷 인바운드 개방 포트 없음, 원격 접속은 Tailscale만
- 사진 서버는 방화벽 DMZ 포트에 분리, 내부망 접근 차단 확인
- 매일 ZFS 스냅샷과 Immich DB 백업
- 디스크 정기 검사와 이상 시 알림, 서버 다운 알림
- 복원 테스트와 업데이트 리허설 완료
주의할 점
- 스냅샷은 디스크 고장에 대비하지 못합니다. HDD 1개 구성이므로 2차 사본(다른 디스크나 다른 장소)을 확보하기 전까지는 폰 원본을 지우지 않습니다. 이 글 시점에서 2차 사본은 아직 남은 과제입니다.
- TrueNAS와 Immich는 버전마다 메뉴 위치와 앱 설치 화면이 자주 바뀝니다. 오래된 가이드보다 설치 화면과 현재 문서를 기준으로 판단하는 편이 낫습니다.
- 디스크는
/dev/sdX가 아니라/dev/disk/by-id/경로로 지정합니다. - 서버 전원은 UPS의 배터리 백업 콘센트에 연결하고, BIOS에서 정전 후 자동 켜짐을 설정합니다.
- 복원에 필요한 정보(DB 비밀번호, 설정 파일, 앱 버전)는 서버 밖에 따로 보관합니다.
- Android 폰에서 VPN 방식 광고 차단 앱을 쓰면 Tailscale이 끊깁니다. VPN은 한 번에 하나만 쓸 수 있습니다.
- 공유기 아래 PC는 방화벽에서 공유기 주소 하나로 보입니다. 특정 PC만 허용하려면 그 PC가 방화벽에서 직접 보이는 주소를 가져야 합니다.

