Skip to content
It's Ido
Go back

홈서버 위에 Docker 한 겹 더, 운영 골격 잡아두기

Edit page

지난 미니 PC 한 대로 시작한 우당탕탕 홈서버 구축기 글에서 홈서버의 기본 골격을 잡고 나니까, 그제야 위에 뭘 좀 올려도 되겠다는 마음이 들더라고요. 그래서 이번에는 Docker를 올려보려고 해요.

Docker는 쉽게 말하면 프로그램을 작은 상자에 담아 실행하는 도구예요. 웹앱이나 데이터베이스, 자동화 도구를 서버에 하나씩 직접 설치하면 설정이 서로 섞이고 관리가 점점 어려워지는데, Docker를 쓰면 각각을 독립된 상자처럼 띄울 수 있어서 설치·중지·삭제·이동이 훨씬 깔끔해져요.

사실 처음에는 “Docker만 빨리 깔고 서비스 한두 개 올리면 끝나는 거 아닌가” 싶은 마음이 컸어요. 그런데 막상 손대보니 포트를 어디로 열지, 로그 파일이 얼마나 쌓일지, 컨테이너 데이터를 어디에 둘지처럼 작은 결정들이 의외로 많더라고요. 한 번 잘못 잡아두면 서비스를 올릴 때마다 같은 고민을 반복하게 되는 지점들이거든요.

그래서 오늘은 Docker 자체를 안전하게 설치하고, 나중에 어떤 서비스를 올려도 헷갈리지 않을 기본 규칙을 먼저 잡아두는 데 집중할게요.

오늘 할 일

이번 글에서 할 일은 아래와 같아요.

할 일왜 하는가
Docker Engine 설치서버에서 Docker 컨테이너를 실행하기 위해 필요해요.
Docker Compose 준비여러 컨테이너를 파일 하나로 관리하기 위해 필요해요.
일반 사용자로 Docker 실행매번 sudo docker를 입력하지 않아도 되게 해요.
로그 용량 제한컨테이너 로그가 디스크를 꽉 채우는 일을 막아요.
포트 공개 규칙 정리실수로 서비스를 인터넷 전체에 열지 않기 위해 필요해요.
/srv 폴더 구조 정리운영 파일과 데이터를 어디에 둘지 기준을 잡아요.
테스트 서비스 실행Docker가 실제로 잘 작동하는지 확인해요.

시작 전에 확인하기

이 글은 1편을 따라 홈서버 기본 골격을 잡아둔 상태에서 이어진다고 가정해요.

본격적으로 시작하기 전에, 서버가 1편에서 잡아둔 상태 그대로인지 한 번 점검해요. 별것 아닌 단계 같아 보이지만, 이 지점에서 한 번 꼬이면 뒤에서 자꾸 발목 잡히더라고요.

항목확인 명령어기준
서버 OSlsb_release -aUbuntu Server 26.04 LTS
호스트네임hostname내가 설정한 이름이 나옴 (예: homeserver)
서버 시간timedatectlAsia/Seoul로 동기화됨
Tailscale 연결tailscale status로그아웃 상태가 아님, 내 장비 목록이 보임
Tailscale IPv4tailscale ip -4100.x.y.z 형태로 하나 나옴
SSH 포트sudo sshd -T | grep '^port '내가 쓰는 포트가 나옴 (예: port 22)
SSH 공개 범위sudo ufw status verboseStatus: active, ALLOW 규칙이 tailscale0에만 있음
운영 폴더ls -ld /srv/docker /srv/data /srv/backups /srv/logs/srv/docker, /srv/data, /srv/backups, /srv/logs 모두 존재
sudo 권한groups출력에 sudo가 보임

예전 Docker 패키지 정리하기

여기서부터 Docker 설치까지는 Docker 공식 문서의 Ubuntu 설치 절차를 그대로 따라가요. Docker 설치 방법은 시간이 지나면서 조금씩 바뀔 수 있으니, 실제로 설치하기 직전에는 Docker 공식 Ubuntu 설치 문서를 한 번 더 펴보는 걸 추천해요.

[ Docker와 Docker 패키지 ]

본격적으로 시작하기 전에, 한 가지 헷갈리기 쉬운 표현을 짚고 갈게요. 이 글에는 “Docker”와 “Docker 패키지”라는 말이 자주 나오는데, 둘은 같은 말이 아니에요.

비유하자면 같은 영화의 여러 매체 같은 거예요. 영화 ‘아바타’를 보고 싶을 때 보는 방법은 여러 가지예요. 동네 비디오 가게에서 옛날 DVD를 빌릴 수도, 공식 사이트에서 4K Blu-ray를 살 수도, 스트리밍 서비스로 볼 수도 있죠. 영화 내용은 모두 같은 ‘아바타’지만, 우리가 손에 쥐는 건 그 영화를 담은 디스크나 파일이에요. 한 책상 위에 같은 영화의 여러 디스크가 섞이면 어느 걸 플레이어에 넣어야 할지 헷갈리고요.

Docker도 똑같아요.

  • Docker(영화) = 컨테이너를 실행하는 소프트웨어 자체.
  • Docker 패키지(디스크·파일) = 그 Docker를 담아 배포하는 단위. 같은 Docker라도 어느 회사가 어떻게 포장해 배포했느냐에 따라 제품 이름이 달라요.

여기서 docker-ce-ceCommunity Edition(무료 공개판)의 줄임말이에요. Docker 회사가 직접 만들어 공식 저장소에 올려두는 정식 제품이고, 이 글에서 설치할 것도 이거예요. (참고로 기업용 유료판은 docker-ee Enterprise Edition이에요.)

이 글에 등장하는 패키지들을 영화 매체에 빗대 보면 이런 느낌이에요.

패키지 이름출처영화로 치면비고
docker-ceDocker 공식 저장소영화사 직배 정식 4K Blu-ray이 글에서 설치할 정식 제품이에요.
docker.ioUbuntu 기본 저장소동네 비디오 가게의 옛 DVDUbuntu가 직접 만든 자체 판본, 버전이 뒤처져 있어요.
docker-composeUbuntu 기본 저장소단종된 옛 테이프옛 Compose v1이에요. 지금은 docker-ce에 따라와요.
podman-dockerPodman 쪽Docker처럼 보이는 다른 영화Docker처럼 흉내내는 다른 도구예요.

그래서 이 단계에서 지우려는 건 Docker 자체가 아니라, 다른 출처에서 받았을 수 있는 옛 Docker 관련 패키지들이에요.

이전에 Ubuntu 기본 저장소나 다른 방식으로 Docker를 설치해본 적이 있다면, 그 흔적이 공식 Docker 패키지와 충돌할 수 있어요. 모르고 그냥 두었다가 뒤에서 한참 헤맨 적이 있어서, 이번에는 먼저 깨끗하게 비워두려고 해요. 아직 Docker를 설치한 적이 없다면 “지울 게 없다”고 답할 수 있는데, 그건 정상이에요.

[ 실행 명령어 ]

현재 시스템에 설치되어 있는 충돌 패키지만 골라내고, 그 목록을 한 번에 지워요.

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)

[ 확인 방법 ]

지우려던 옛 패키지가 남아 있는지 확인해요. 아무것도 나오지 않거나 clean이 찍히면 옛 패키지들이 다 사라진 거예요.

dpkg -l | grep -E '^ii\s+(docker\.io|docker-compose|docker-compose-v2|docker-doc|podman-docker|containerd|runc)\s' || echo "clean"

Docker 공식 저장소 추가하기

이제 apt가 정식 docker-ce 패키지를 받아올 수 있게, Docker 공식 저장소를 서버의 “받아올 곳 목록”에 추가할 거예요.

apt는 패키지를 받아올 때 미리 등록된 저장소 목록을 먼저 봐요. 그런데 Ubuntu 기본 저장소에는 옛 docker.io만 있고, 정식 docker-ce는 Docker 공식 저장소에만 있어요. 그래서 apt의 저장소 목록에 Docker 공식 저장소를 미리 추가해줘야, 다음 단계에서 docker-ce를 받아올 수 있어요.

Docker Engine, Compose Plugin, Buildx Plugin 같은 부속도 모두 같은 저장소에서 받아오고 업데이트할 수 있어서, 한 곳을 등록해두면 흐름이 깔끔해져요.

[ 필요한 도구 설치 ]

curl은 인터넷에서 파일을 내려받는 도구예요. ca-certificates는 HTTPS 연결을 안전하게 확인하는 데 필요해요.

sudo apt update
sudo apt install -y ca-certificates curl

[ Docker 공식 서명 키 저장 ]

Docker 공식 문서에서는 먼저 Docker의 GPG 키를 /etc/apt/keyrings 아래에 저장하도록 안내해요. 이 키는 나중에 Docker 패키지가 진짜 Docker에서 온 것인지 확인하는 데 쓰여요.

여기서 받는 docker.asc는 “이 패키지가 Docker에서 온 게 맞다”를 확인하는 도장 같은 파일이에요.

sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

[ Docker 저장소 등록 ]

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

이제 패키지 목록을 다시 읽어요.

sudo apt update

[ 확인 방법 ]

출력에 download.docker.com이 보이고 Candidate에 버전이 보이면 Docker 공식 저장소가 잘 등록된 거예요.

apt-cache policy docker-ce

Ubuntu 코드네임도 확인해볼 수 있어요.

. /etc/os-release
echo "$VERSION_CODENAME"
echo "$UBUNTU_CODENAME"

첫 번째 줄인 . /etc/os-release는 값을 불러오는 명령이라서 아무것도 출력되지 않는 게 정상이에요. 그다음 echo 명령에서 Ubuntu Server 26.04 LTS라면 아래처럼 resolute가 나와야 해요.

resolute
resolute

Docker Engine 설치하기

이제 Docker 본체를 설치할 차례예요. 여기서 말하는 Docker Engine은 컨테이너를 실제로 만들고 실행하고 멈추는 핵심 프로그램이에요.

앞에서 Docker를 “프로그램을 작은 상자에 담아 실행하는 도구”라고 했죠. Docker Engine은 그 상자를 만들고, 켜고, 끄고, 네트워크와 저장공간을 연결해주는 서버 쪽 본체라고 보면 돼요. 우리가 입력하는 docker 명령어는 이 Docker Engine에게 일을 시키는 리모컨에 가깝고요.

설치하는 것들은 역할이 조금씩 달라요.

패키지역할
docker-ceDocker Engine 본체예요.
docker-ce-clidocker 명령어를 쓸 수 있게 해요.
containerd.io컨테이너 실행을 맡는 내부 구성요소예요.
docker-buildx-pluginDocker 이미지를 빌드할 때 쓰는 플러그인이에요.
docker-compose-plugindocker compose 명령어를 쓸 수 있게 해요.

[ 실행 명령어 ]

sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

[ 확인 방법 ]

sudo systemctl status docker --no-pager
docker --version
docker compose version

systemctl status docker에서 active (running)이 보이면 Docker 서비스가 실행 중이라는 뜻이에요. docker --versiondocker compose version에서 버전이 잘 찍히면 설치는 성공이에요.

설치 테스트도 한 번 해봐요.

sudo docker run hello-world

Hello from Docker!라는 문장이 나오면 Docker Engine 설치는 완료예요.

일반 사용자로 Docker 실행하기

지금까지는 sudo docker ...처럼 관리자 권한을 붙여서 Docker를 실행했어요. 매번 sudo를 붙이는 게 은근 귀찮으니까, 현재 사용자를 docker 그룹에 넣어두면 한결 편해져요.

다만 이 설정은 편한 만큼 신중해야 해요. docker 그룹에 들어간 사용자는 사실상 서버에서 매우 강한 권한을 갖게 돼요. 혼자 쓰는 홈서버라면 보통 괜찮지만, 믿을 수 없는 사람에게 서버 계정을 만들어주고 그 계정을 docker 그룹에 넣으면 안 돼요.

[ 실행 명령어 ]

docker 그룹이 있는지 확인하고, 없으면 만들어요.

getent group docker || sudo groupadd docker

현재 사용자를 docker 그룹에 추가해요.

sudo usermod -aG docker $USER

[ 확인 방법 ]

그룹 변경은 다시 로그인해야 완전히 반영돼요. 그래서 여기서는 SSH 접속을 끊고 다시 서버에 접속한 후 확인해주세요.

groupsdocker가 보이고, sudo 없이 docker run hello-world가 실행되면 성공이에요.

groups
docker run hello-world

[ 테스트 컨테이너 정리 ]

hello-world는 설치 확인용이라 계속 남겨둘 필요가 없어요.

docker rm $(docker ps -aq -f ancestor=hello-world) 2>/dev/null || true
docker rmi hello-world 2>/dev/null || true

정리됐는지도 확인해요.

docker ps -a
docker images

첫 번째 명령에서 hello-world 컨테이너가 보이지 않고, 두 번째 명령에서 hello-world 이미지가 보이지 않으면 정리된 거예요.

Docker 자동 시작 확인하기

홈서버는 정전이나 커널 업데이트, 가끔 손수 하는 재부팅 같은 일이 생겨도 알아서 다시 살아나는 게 좋아요. 그래야 위에 올린 서비스들도 같이 다시 깨어나거든요.

[ 실행 명령어 ]

sudo systemctl enable docker.service
sudo systemctl enable containerd.service

[ 확인 방법 ]

systemctl is-enabled docker
systemctl is-enabled containerd

둘 다 enabled가 나오면 부팅할 때 자동으로 시작되도록 설정된 거예요.

Docker 로그 용량 제한하기

컨테이너는 실행되는 동안 로그를 계속 남겨요. 어떤 컨테이너가 로그를 너무 많이 찍으면 디스크가 어느 날 갑자기 꽉 차 있는 걸 보고 당황할 수 있어요. 그래서 Docker 전체 기본 설정에 “로그 파일은 너무 커지지 않게 하자”는 규칙을 미리 박아둘 거예요.

이 글에서는 로그 드라이버를 local로 정할게요. 홈서버에서 로그 파일을 직접 파싱하거나 별도 로그 수집기를 붙일 계획이 없다면 local이 단순하면서도 디스크를 덜 써요. Docker의 docker logs, docker compose logs 명령도 그대로 사용할 수 있고요.

json-file은 Docker의 기본 로그 형식에 가깝고 호환성이 좋지만, 직접 용량 제한을 걸어두지 않으면 로그가 야금야금 커질 수 있어요. 나중에 Loki나 Fluent Bit 같은 로그 수집기를 붙이거나, 어떤 도구가 JSON 로그 파일을 기대할 때 다시 선택하면 돼요.

여기서는 두 가지를 설정해요.

설정
로그 로테이션로그 파일 크기와 개수를 제한해요.
live-restoreDocker 관리자가 잠깐 재시작되어도 이미 실행 중인 컨테이너가 최대한 계속 돌게 도와줘요.

Docker 데몬은 컨테이너를 관리하는 관리자이고, 컨테이너는 실제로 돌아가는 서비스예요. live-restore는 관리자를 잠깐 다시 시작해도 서비스가 바로 같이 꺼지지 않게 도와주는 안전장치라고 보면 돼요. 다만 서버 재부팅이나 Docker의 큰 버전 업그레이드, 네트워크·스토리지 설정 변경까지 무중단으로 보장해주는 기능은 아니에요.

[ 실행 명령어 ]

Docker 설정 폴더를 만들어요.

sudo mkdir -p /etc/docker

설정 파일을 열어요.

sudo vi /etc/docker/daemon.json

아래 내용을 넣어요.

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true
}

이 설정은 컨테이너 하나의 로그 파일을 최대 10MB로 두고, 그런 파일을 3개까지만 유지하겠다는 뜻이에요. 즉 컨테이너 하나가 기본적으로 최대 30MB 정도의 로그만 남기게 돼요.

파일 권한을 정리해요.

sudo chown root:root /etc/docker/daemon.json
sudo chmod 644 /etc/docker/daemon.json

Docker를 재시작해서 설정을 적용해요.

sudo systemctl restart docker

이 글의 흐름에서는 아직 운영 중인 컨테이너가 없으니 재시작해도 부담이 거의 없어요. 이미 실제 서비스를 운영 중인 서버라면, 재시작 전에 점검 시간을 한 번 잡는 편이 좋아요.

[ 확인 방법 ]

docker info --format '{{.LoggingDriver}}'
docker info --format '{{.LiveRestoreEnabled}}'

각각 아래처럼 나오면 돼요.

local
true

[ 주의할 점 ]

로그 설정은 새로 만들어지는 컨테이너부터 적용돼요. 이미 만들어진 컨테이너의 로그 설정은 자동으로 바뀌지 않으니, 필요하면 해당 컨테이너를 다시 만들어 줘야 해요.

테스트 서비스 올려보기

이제 Docker 설치와 기본 설정은 마무리됐어요. 실제로 잘 도는지 가벼운 nginx 컨테이너 하나를 띄워서 확인할 거예요.

시작 전 정해둘 두 가지 원칙

[ 포트를 어느 주소에 열지 ]

서비스를 띄우기 전에, 포트를 어디에 열지부터 정해두려고 해요. 이 부분은 실제 웹 테스트를 하기 전에 이해해두면 덜 막막하더라고요.

서버에는 이미 UFW 방화벽이 켜져 있어요. 그래서 “UFW가 있으니 Docker 서비스도 알아서 막아주겠지”라고 생각하기 쉬워요. 그런데 막상 손대보면, Docker는 컨테이너 포트를 열 때 자체적으로 네트워크 규칙을 만들어서, 이 과정에서 UFW 규칙보다 Docker 규칙이 먼저 적용될 수 있더라고요.

그래서 Docker로 포트를 열 때는 UFW만 믿기보다, 처음부터 어느 주소에 열지를 정확히 적어두는 습관이 중요해요. 포트는 집의 문처럼 생각하면 쉬워요.

열고 싶은 범위추천하는 방식
서버 안에서만 쓰기127.0.0.1:호스트포트:컨테이너포트
내 맥북에서 SSH 터널로만 보기127.0.0.1:호스트포트:컨테이너포트
내 Tailnet 장비에서 직접 보기100.x.y.z:호스트포트:컨테이너포트
인터넷 전체 공개이 글에서는 하지 않음. reverse proxy, TLS, 인증을 함께 다뤄야 해요.

가장 안전한 기본값은 127.0.0.1이에요. 127.0.0.1은 “이 서버 자기 자신”을 뜻해요. 여기에만 열어두면 외부 장비가 바로 접속할 수 없고, 필요할 때는 SSH 터널로만 들여다볼 수 있어요.

Compose 파일에서는 이런 식으로 적어요.

ports:
  - "127.0.0.1:8080:80"

이 한 줄의 뜻은 아래와 같아요.

부분
127.0.0.1서버 자기 자신에서만 접근 가능
8080서버 쪽 포트
80컨테이너 안쪽 웹서버 포트

조심해야 하는 예시는 이런 모양이에요.

ports:
  - "8080:80"

앞에 주소를 생략하면 보통 0.0.0.0:8080처럼 모든 네트워크에 열려버릴 수 있어요. 홈서버에서는 이 차이가 꽤 커요. 그래서 이 글에서는 “Docker 포트는 UFW만 믿지 말고, 바인딩 주소를 명확히 적는다”를 기본 원칙으로 잡을게요.

[ 운영 파일을 /srv 어디에 둘지 ]

서버에는 이미 /srv 아래에 운영용 폴더가 만들어져 있어요. Docker를 쓸 때도 이 구조를 유지하면 나중에 백업하거나 서비스를 다른 서버로 옮길 때 덜 헷갈리더라고요.

홈서버에 서비스를 하나 올리면 보통 두 종류의 파일이 생겨요.

종류예시잃어버리면?
실행 설정compose.yaml, .env다시 만들 수는 있지만 귀찮아요.
실제 데이터DB 파일, 업로드 파일, 앱 설정 데이터서비스 내용이 날아갈 수 있어요.

그래서 /srv 아래를 역할별로 나눠서 써요.

위치쉽게 말하면무엇을 두나
/srv/docker/서비스명실행 설명서 보관함Compose 파일, .env 같은 실행 설정
/srv/data/서비스명진짜 데이터 보관함DB 파일, 업로드 파일, 앱 상태 데이터
/srv/backups/서비스명복구용 사본 보관함백업 파일
/srv/logs/서비스명기록 보관함따로 파일로 남기는 로그

핵심은 간단해요. 서비스를 어떻게 실행할지는 /srv/docker, 잃어버리면 안 되는 데이터는 /srv/data에 둔다고 생각하면 돼요.

기본 구조는 이런 느낌이에요.

/srv
├── docker/
│   └── 서비스명/
│       ├── compose.yaml
│       └── .env
├── data/
│   └── 서비스명/
├── backups/
│   └── 서비스명/
└── logs/
    └── 서비스명/

중요한 운영 기준이 하나 있어요. 컨테이너 데이터는 Docker가 알아서 관리하는 숨은 위치에 둘 수도 있고, 내가 정한 서버 폴더에 보이게 둘 수도 있어요.

방식데이터가 보이는 위치느낌
named volume보통 /var/lib/docker/volumes 아래편하지만 위치가 덜 눈에 보여요.
bind mount내가 정한 폴더. 예: /srv/data/서비스명위치가 눈에 보여서 백업하기 쉬워요.

이 글에서는 중요한 데이터를 Docker가 알아서 숨겨두게 두기보다, 내가 정한 /srv/data/서비스명 폴더에 보이게 두는 방식을 기본으로 잡을게요. 나중에 백업할 때 “중요한 데이터는 /srv/data 아래에 있다”라고 한 번에 떠올릴 수 있어서요.

테스트 서비스 띄우기

이제 위의 원칙대로 가벼운 웹서버 컨테이너 하나를 띄워볼 거예요. Nginx라는 가벼운 웹서버를 테스트용으로 쓰고, 앞에서 정한 안전한 기본값대로 127.0.0.1:8080:80으로 포트를 열게요. 즉 서버 안에서만 직접 접근 가능하고, 맥북에서는 SSH 터널로 확인하는 방식이에요.

[ 운영 폴더 만들기 ]

이번 글에서는 test-nginx라는 연습용 서비스를 만들어 띄울 거예요. 지금 테스트하는 Nginx에는 중요한 데이터가 들어가지는 않지만, 앞으로 실제 서비스를 올릴 때를 대비해서 같은 폴더 습관을 미리 잡아둘게요.

아래 명령은 test-nginx를 위한 자리를 미리 만드는 작업이에요.

mkdir -p /srv/docker/test-nginx
mkdir -p /srv/data/test-nginx
mkdir -p /srv/backups/test-nginx
mkdir -p /srv/logs/test-nginx

확인은 아래처럼 해요.

ls -la /srv/docker
ls -la /srv/data
ls -la /srv/backups
ls -la /srv/logs

각 폴더 안에 test-nginx가 보이면 돼요. 뒤에서 Compose 파일은 /srv/docker/test-nginx/compose.yaml에 만들고, 테스트 웹페이지는 /srv/data/test-nginx/index.html에 만들 거예요.

[ Compose 파일 작성하기 ]

docker run으로도 컨테이너를 실행할 수 있지만, 실제 홈서버 운영에서는 Compose 파일을 미리 만들어두는 편이 마음 편해요. Compose 파일은 “이 서비스를 어떻게 실행할지 적어둔 레시피”예요. 이미지, 포트, 저장 폴더, 재시작 정책을 파일 하나에 다 적어둘 수 있어서, 나중에 다시 띄울 때나 다른 서버로 옮길 때 같은 모양으로 굴러가더라고요.

cd /srv/docker/test-nginx

이 섹션에서 쓰는 docker compose ... 명령은 이 폴더, 즉 compose.yaml이 있는 /srv/docker/test-nginx에서 실행한다고 생각하면 돼요. 다른 폴더에서 실행하면 no configuration file provided: not found 같은 에러가 날 수 있어요.

vi compose.yaml

아래 내용을 넣어요.

services:
  web:
    image: nginx:stable
    container_name: test-nginx
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - /srv/data/test-nginx:/usr/share/nginx/html:ro

여기서 중요한 부분은 이거예요.

image: nginx:stableNginx stable 이미지를 사용한다.
container_name: test-nginx컨테이너 이름을 test-nginx로 한다.
restart: unless-stopped서버 재부팅 후에도 다시 실행한다. 단, 내가 직접 끈 경우는 제외한다.
ports서버의 127.0.0.1:8080을 컨테이너의 80에 연결한다. 서버 밖으로 바로 열지 않는다.
volumes/srv/data/test-nginx를 컨테이너 안의 웹 문서 폴더로 연결한다.

container_name은 테스트에서는 알아보기 쉬워서 넣었어요. 실제 운영 서비스에서는 여러 개를 복제하거나 같은 구성을 여러 번 띄울 가능성이 있다면 생략하는 편이 충돌을 줄일 수 있어요.

테스트용 웹페이지도 하나 만들어요.

vi /srv/data/test-nginx/index.html

아래처럼 적어요.

hello from docker on homeserver

[ 실행·확인하기 ]

Compose 파일이 문법상 맞는지 먼저 확인해요.

docker compose config

에러 없이 정리된 YAML 내용이 출력되면 괜찮아요. yaml: line ... 같은 에러가 나오면 들여쓰기나 문법을 다시 확인하면 돼요.

문제가 없으면 실행해요.

docker compose up -d

실행됐는지 확인해요.

docker compose ps
curl http://127.0.0.1:8080
ss -tulpen | grep 8080

curl 결과로 아래 문장이 보이면 성공이에요.

hello from docker on homeserver

ss 출력에 127.0.0.1:8080이 보이면 외부 전체가 아니라 서버 내부 주소에만 열린 거예요.

맥북에서 브라우저로 보고 싶다면 맥북의 새 터미널에서 SSH 터널을 열어요.

ssh -L 8080:127.0.0.1:8080 사용자명@homeserver

또는 MagicDNS 대신 Tailscale IP를 써도 돼요.

ssh -L 8080:127.0.0.1:8080 사용자명@100.x.y.z

그다음 맥북 브라우저에서 아래 주소를 열어요.

http://127.0.0.1:8080

여기까지 잘 보였다면 Compose 흐름은 정상이에요.

재부팅 후 자동 복귀 확인

마지막으로 재부팅한 뒤에도 test-nginx 컨테이너가 알아서 다시 뜨는지 확인할게요. 이걸로 두 가지를 한 번에 검증해요 — Docker 서비스 자체가 부팅 시 자동 시작되는지(systemctl enable docker), 그리고 Compose의 restart: unless-stopped가 잘 동작하는지.

[ 확인 방법 ]

재부팅 후 다시 접속해요.

sudo reboot
docker ps
curl http://127.0.0.1:8080

docker pstest-nginx가 보이고, curl 결과로 hello from docker on homeserver가 나오면 Docker 자동 시작과 restart: unless-stopped 둘 다 통과예요.

테스트 서비스 정리하기

이번 글의 목적은 Nginx를 계속 운영하는 게 아니에요. 재부팅 검증까지 끝났다면 테스트 서비스는 가볍게 내려도 돼요.

[ 테스트 서비스 내리기 ]

cd /srv/docker/test-nginx
docker compose down
docker compose ps

docker compose ps에서 test-nginx가 보이지 않으면 컨테이너는 내려간 거예요.

[ 테스트 파일·이미지 정리 ]

테스트 파일을 남겨둘지 한 번 확인해요.

ls -la /srv/docker/test-nginx
ls -la /srv/data/test-nginx

정말 지우고 싶다면 아래 명령을 쓸 수 있어요.

rm -rI /srv/docker/test-nginx /srv/data/test-nginx /srv/backups/test-nginx /srv/logs/test-nginx
docker rmi nginx:stable

rm은 파일을 지우는 명령이에요. 실제 서비스 데이터가 들어간 폴더에는 바로 쓰지 않는 편이 좋아요. 위 명령은 테스트용 test-nginx 폴더를 지울 때만 사용해요. docker rmi nginx:stable은 테스트 때 내려받은 Nginx 이미지를 지워요. 다른 컨테이너가 같은 이미지를 쓰고 있으면 삭제가 실패할 수 있어요.

마지막 점검

테스트 흔적까지 정리했으니, 글 전체 설정이 그대로 살아 있는지 한 번에 짚어볼게요. Docker 자동 시작은 앞 재부팅 검증에서 이미 확인한 항목이라, 여기서는 표 한 번으로 마무리예요.

항목확인 명령정상 기준
Docker 공식 저장소apt-cache policy docker-cedownload.docker.com과 Candidate 버전이 보임
Docker Enginedocker --version버전이 출력됨
Docker Composedocker compose version버전이 출력됨
Docker 서비스systemctl status docker --no-pageractive (running)
자동 시작systemctl is-enabled dockerenabled
로그 설정docker info --format '{{.LoggingDriver}} {{.LiveRestoreEnabled}}'local true
실행 중 컨테이너docker ps(정리했다면) 비어 있음
테스트 흔적docker ps -a, docker imagestest-nginx, nginx 모두 없음
/srv 골격ls -ld /srv/docker /srv/data /srv/backups /srv/logs네 폴더 모두 존재
SSH 포트sudo sshd -T | grep '^port '의도한 포트가 출력됨
방화벽sudo ufw status verboseStatus: active, SSH가 tailscale0 범위

이 표가 다 통과되면 Docker 위에서 서비스를 올릴 준비까지 마친 거예요.

마치며

여기까지가 제 홈서버 시리즈의 두 번째 골격이에요. 이번 셋업으로 Docker가 그 위에 한 겹 더 올라간 셈이고요.

처음에는 저도 ‘Docker만 설치하면 서비스는 알아서 잘 돌아가겠지’ 싶었는데, 막상 손대보니 설치 자체보다 그 주변의 작은 결정들 — 로그를 어떻게 다룰지, 포트를 어디로 열지, 데이터를 어디에 둘지 — 가 더 마음에 남더라고요. 이 결정들을 처음에 한 번 잡아두면 다음에 어떤 서비스를 올려도 같은 자리에서 같은 모양으로 출발할 수 있어서 마음이 한결 편해져요.

이번 구성에서 가장 중요한 생각은 세 가지로 정리할 수 있어요.

  • 운영 설정은 /srv/docker에 둔다.
  • 잃어버리면 안 되는 데이터는 /srv/data에 둔다.
  • Docker 포트는 127.0.0.1 또는 Tailscale IP처럼 열 주소를 직접 적는다.

처음 한 번은 명령어가 많아 보여도, 실제로는 “설치한다 → 어디에 데이터를 둘지 정한다 → 밖으로 열 포트를 조심한다 → 테스트한다”의 반복이에요. 이 기준만 잡아두면 나중에 개인 웹앱, 데이터베이스, 자동화 도구, reverse proxy, 백업 자동화 같은 것들을 하나씩 올릴 때 훨씬 덜 헷갈리게 돼요.

비슷한 환경에서 처음 Docker를 홈서버에 올려보려는 분께, 이 글이 작은 길잡이가 되었으면 좋겠어요.


Edit page