[aniflux 5/5] Docker 단일 바이너리 + Proxmox 홈서버 운영기
도입
홈서버에서 Docker로 여러 서비스를 돌리고 있습니다.
Plex, Jellyfin, Home Assistant, AdGuard, 각종 모니터링 도구들.
이 정도 되면 컨테이너 하나 추가할 때마다 신경 쓰이는 게 리소스입니다.
성능이 넉넉하지 않은 소규모 홈서버입니다.
넉넉하지 않은 환경에서 aniflux까지 올리려면, 이미지 크기와 메모리 사용량을 최대한 줄여야 했어요.
그래서 처음부터 "단일 바이너리, 최소 이미지"를 목표로 잡았습니다.
배경
Go의 장점 중 하나가 static binary를 만들 수 있다는 거더라고요.
CGO를 끄면 외부 라이브러리 의존 없이 바이너리 하나로 끝나요.
이걸 scratch 이미지에 올리면 이미지 크기가 극적으로 줄어듭니다.
빌드와 배포는 GitHub Actions로 자동화했어요.
태그를 push하면 Actions가 빌드하고, ghcr.io에 이미지를 올리고, 홈서버가 pull 받아서 재시작하는 구조입니다.
수동 배포는 실수가 생기기 마련이라, 처음부터 CI/CD를 붙였습니다.
버전 관리는 빌드 시점에 ARG로 주입하는 방식을 택했어요.
UI 하단에 버전을 표시하고 싶었거든요.
과정
Dockerfile은 multi-stage로 구성했어요.
FROM golang:1.25-alpine AS builder
ARG VERSION=dev
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o /aniflux ./cmd/aniflux
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /aniflux /usr/local/bin/aniflux
ENTRYPOINT ["aniflux"]
처음엔 scratch를 썼는데, 나중에 alpine으로 바꿨어요.
이유는 뒤에서 설명하겠습니다.
docker-compose는 단순하게 잡았어요.
볼륨 두 개 (데이터베이스, 미디어), 네트워크 하나, healthcheck 하나.
services:
aniflux:
image: ghcr.io/user/aniflux:latest
volumes:
- ./data:/data
- /mnt/media:/media
environment:
- TZ=Asia/Seoul
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
배포 파이프라인은 이렇게 돼요.
git tag push하면 Actions가 트리거되고, docker buildx로 이미지 빌드해서 ghcr.io에 push.
홈서버에서는 수동으로 docker compose pull && docker compose up -d를 실행합니다.
운영 서비스라 자동 업데이트보다는 확인 후 배포하는 게 안전하다고 판단했습니다.
삽질
scratch 이미지에서 첫 번째로 만난 문제가 CA certificates였어요.
외부 API들이 전부 HTTPS인데, scratch에는 인증서가 없거든요.
x509: certificate signed by unknown authority 에러가 나서 한참 헤맸습니다.
처음엔 빌더 스테이지에서 /etc/ssl/certs/ca-certificates.crt를 복사하는 방식으로 해결했어요.
근데 이후에 timezone 문제가 또 터졌어요.
스케줄러가 UTC로 동작해서, 방영 시간 계산이 전부 9시간 어긋나더라고요.
/etc/localtime도 없고 tzdata도 없으니 당연한 거였죠.
결국 alpine으로 갈아탔어요.
ca-certificates랑 tzdata 두 개만 설치하면 되니까요.
이미지 크기가 5MB 정도 늘었지만, 안정성을 생각하면 합리적인 트레이드오프였습니다.
세 번째 삽질은 cross-filesystem move였어요.
다운로드 클라이언트가 받은 파일을 Plex 라이브러리 폴더로 옮길 때, Docker 볼륨이 다른 파일시스템이면 os.Rename이 EXDEV 에러를 뱉습니다.
"cross-device link"라는 메시지를 보고 한참 검색했어요.
해결은 단순했습니다.
Rename 실패 시 copy + remove로 fallback하는 함수를 만들었어요.
파일 크기가 수백 MB라 복사 시간이 걸리긴 하지만, 어차피 백그라운드 작업이라 괜찮았습니다.
결과
최종 이미지 크기는 15MB.
컨테이너 RSS 메모리는 평상시 30MB, 스캔 돌릴 때 잠깐 50MB.
cold start는 1초 이내.
홈서버에서 다른 서비스들과 함께 돌아가는데, 존재감이 거의 없습니다.
CPU 사용률은 평상시 0%에 가깝고, 스케줄러 돌 때만 잠깐 올라갑니다.
배포는 수동이지만, 태그 push 후 pull 한 번이면 끝입니다.
코드 수정하고 태그 push하면 5분 후에 홈서버에 반영할 수 있습니다.
참고
- Docker multi-stage builds — 빌드/런타임 분리
- GitHub Actions — Docker 이미지 발행
- Proxmox LXC — 컨테이너 호스팅 환경