16 KiB
NAS 배포 상세 가이드 (UGREEN 등 Docker 호스트)
이 문서는 다른 PC에서 작업한 뒤 NAS에 올리는 경우까지 포함해, 처음부터 끝까지 순서대로 따라 할 수 있도록 적었다.
요약·변수 설명은 docs/deploy.md를 참고하고, 여기서는 SSH 기준으로 한 단계씩 기술한다.
앱·문서의 공식 버전은 package.json의 version과 docs/spec.md의 현재 버전을 기준으로 한다.
1. 전제 조건(미리 확인)
다음이 갖춰져 있어야 한다. 기종마다 메뉴 이름은 다를 수 있다.
- Docker(및 Docker Compose 플러그인 또는
docker compose명령)가 NAS에서 동작한다. - SSH로 NAS에 접속할 수 있다. (예: Mac 터미널에서
ssh nas처럼 호스트 별칭을 써도 된다.) - NAS에 Git이 설치되어 있거나, Git 없이 배포하려면 12절의 대안을 본다.
원격 저장소 URL(예시, 실제 주소는 저장소 관리 화면과 동일해야 한다):
https://git.sori.studio/zenn/todo.sori.studio.git
2. NAS에 둘 폴더 경로 정하기
이 프로젝트는 예시로 다음 경로를 쓴다.
/volume1/docker/projects/apps/todo
의미: Docker용 프로젝트를 모아 두는 apps 아래에 todo 전용 디렉터리를 둔다.
다른 경로를 써도 되지만, 이후 명령의 cd 경로만 본인 환경에 맞게 바꾸면 된다.
3. SSH로 NAS 접속 후 폴더로 이동
ssh <NAS에_설정한_호스트이름>
접속 후:
cd /volume1/docker/projects/apps
apps 목록에 todo가 보이는지 확인한다.
ls
4. Git으로 가져오기(권장: 빈 폴더에 클론)
4-1. todo 폴더가 아직 없거나, 비어 있게 만들 수 있는 경우
apps에서:
git clone https://git.sori.studio/zenn/todo.sori.studio.git todo
cd todo
이렇게 하면 todo 안에 저장소 파일이 채워진다.
4-2. 이미 todo 폴더로 들어가 있는데 안이 비어 있는 경우
todo 안에서 현재 디렉터리에 클론한다.
cd /volume1/docker/projects/apps/todo
git clone https://git.sori.studio/zenn/todo.sori.studio.git .
마지막의 .은 “현재 폴더에 받아라”는 뜻이다.
4-3. 폴더 안에 다른 파일이 이미 있는 경우
git clone ... .은 비어 있지 않으면 실패한다. 다음 중 하나로 정리한다.
- 다른 곳으로 파일을 옮기거나 이름을 바꾼 뒤 빈 폴더에서 4-2를 실행한다.
- 또는 상위에서 새 이름으로 클론한다.
git clone https://git.sori.studio/zenn/todo.sori.studio.git todo-app
이후docker compose는 클론된 디렉터리 안에서만 실행하면 된다.
4-4. 클론 후 한 번 확인
cd /volume1/docker/projects/apps/todo
ls
docker-compose.yaml, Dockerfile, package.json 등이 보이면 된다.
5. NAS의 LAN IP 확인(중요)
웹앱은 사용자의 브라우저에서 PocketBase 주소로 직접 요청한다.
그래서 빌드에 넣는 주소는 **“브라우저가 NAS에 접속할 때 쓰는 주소”**여야 한다.
예시:
docker-compose.yaml에 적어 둔 호스트 포트(웹42881, PocketBase42917)는 다른 스택과 겹치지 않게 잡은 값이다. 겹치면 해당 파일의ports만 바꾼 뒤, 아래export·.env의 포트도 같이 맞춘다.- 같은 NAS 안의 브라우저만:
http://127.0.0.1:42917처럼 호스트에 열린 포트로 접근한다. 폰이나 다른 PC에서는127.0.0.1대신 NAS LAN IP를 써야 한다. - 집 안에서 여러 기기:
http://192.168.x.x:42917(API),http://192.168.x.x:42881(웹) 형태로 쓴다.
IP 확인은 NAS 관리 화면의 네트워크 정보를 보거나, SSH에서 NAS OS에 맞는 명령으로 확인한다. (기종별로 명령이 다를 수 있다.)
아래에서는 예시로 192.168.0.50을 쓴다. 본인 NAS IP로 바꿔야 한다.
6. 환경 변수 설정 후 Compose 실행
docker compose는 프로젝트 루트에 .env 파일이 있으면 자동으로 읽는다. (Git에 올리지 말 것. .gitignore에 포함됨.) .env.example은 읽지 않는다. 적용하려면 cp .env.example .env 후 .env만 수정한다.
6-0. 운영 도메인을 쓰는 경우(예: todo.sori.studio)
/volume1/docker/projects/apps/todo/.env 예시:
VITE_PUBLIC_APP_URL=https://todo.sori.studio
VITE_POCKETBASE_URL=https://api.todo.sori.studio
그다음:
cd /volume1/docker/projects/apps/todo
docker compose up -d --build
리버스 프록시에서 todo.sori.studio → 웹, api.todo.sori.studio → PocketBase 로 각각 연결해야 한다. (경로·TLS는 사용 중인 NAS/프록시 제품 문서를 따른다.)
6-1. LAN IP만 쓰는 경우
프로젝트 루트에서 export로 한 세션에만 줄 수도 있다.
cd /volume1/docker/projects/apps/todo
export VITE_PUBLIC_APP_URL="http://192.168.0.50:42881"
export VITE_POCKETBASE_URL="http://192.168.0.50:42917"
docker compose up -d --build
- 첫 실행은 이미지 다운로드·빌드로 시간이 걸릴 수 있다.
- 백그라운드 실행은
-d때문이다.
6-2. 잘 떴는지 확인
docker compose ps
pocketbase-todo, todo-web(컨테이너 이름·docker-compose.yaml의 container_name)이 running에 가깝게 보이면 된다.
6-3. 로그가 궁금할 때
docker compose logs -f --tail=100
7. 브라우저로 접속해 보기
예시 IP 기준:
- 할 일 웹앱:
http://192.168.0.50:42881 - PocketBase 관리자 UI:
http://192.168.0.50:42917/_/
접속이 안 되면:
- NAS 방화벽 또는 보안 앱에서 42881, 42917 포트가 막혀 있지 않은지 확인한다.
docker compose ps로 컨테이너가 떠 있는지 다시 본다.
8. PocketBase 최초 설정(한 번)
- 브라우저에서
http://<NAS_IP>:42917/_/를 연다. (호스트 포트를 바꿨다면 그 번호로 연다.) - 관리자 계정(이메일·비밀번호)을 만들고 마법사를 끝낸다.
- 컬렉션
todos생성- 필드
title: 타입 Text - 필드
done: 타입 Bool
- 필드
- API 규칙
처음에는 테스트로 느슨하게 두었다가, 나중에 로그인 기반으로 조이는 것을 권장한다. (운영 정책에 맞게 조정.) - 설정 → CORS
웹앱 출처를 허용 목록에 넣는다. 예:http://192.168.0.50:42881또는https://todo.sori.studio
포트·프로토콜·호스트가 실제로 주소창에 쓰는 것과 한 글자라도 다르면 브라우저가 차단한다.
이후 http://<NAS_IP>:42881에서 목록·추가·완료 토글이 동작하는지 본다.
9. 왜 export로 주소를 줄이는가
VITE_POCKETBASE_URL은 Docker 이미지를 빌드할 때 프런트 번들에 박힌다.
NAS IP나 도메인이 바뀌면, 다시 빌드해야 한다.
cd /volume1/docker/projects/apps/todo
export VITE_PUBLIC_APP_URL="http://새로운_주소:42881"
export VITE_POCKETBASE_URL="http://새로운_주소:42917"
docker compose up -d --build
10. 코드를 최신으로 갱신할 때(이미 클론해 둔 경우)
주의: apps 폴더 자체(/volume1/docker/projects/apps)에는 .git이 없다. 여기서 git pull을 하면 fatal: not a git repository가 난다. 항상 클론해 둔 프로젝트 디렉터리 안으로 들어간 뒤 Git 명령을 실행한다.
git pull 한 줄만으로는 원격에 없는 브랜치에 붙어 있거나, 로컬 수정이 있어 병합이 막히거나, 처음 한 번도 fetch를 안 한 상태면 기대한 대로 안 받아질 수 있다. 아래 순서를 권장한다.
cd /volume1/docker/projects/apps/todo
ls -a .git
.git이 보이면 이 경로가 저장소 루트가 맞다. 없다면 상위 apps에 있지 않은지, 폴더 이름이 todo가 맞는지 확인한다.
cd /volume1/docker/projects/apps/todo
# 1) 작업 트리 확인(로컬에서 고친 파일이 있으면 pull이 거부되거나 충돌한다)
git status
# 2) 원격 정보를 가져온 뒤, 추적 브랜치(보통 main)를 최신으로 맞춘다
git fetch origin
git checkout main
git pull --ff-only origin main
git pull만 쓰고 싶다면 이미main을 추적 중이어야 한다. 첫 클론 후 한 번도 브랜치를 안 바꿨다면git branch -vv로origin/main을 따라가는지 본다.- 로컬에서 수정한 파일이 있으면
git pull이 실패한다. NAS에서 직접 고친 내용을 버려도 된다면git restore .등으로 되돌린 뒤 다시 2)를 실행한다. 유지해야 하면git stash후 pull하고git stash pop(충돌 가능)을 쓴다. - 비공개 저장소면 NAS에 SSH 키·자격 증명(credential helper)이 등록돼 있어야
fetch/pull이 된다.
이후 이미지·번들을 갱신한다.
cd /volume1/docker/projects/apps/todo
# 3) .env로 주소를 두었다면 export는 생략해도 된다. 없으면 6-1과 같이 export 한다.
export VITE_PUBLIC_APP_URL="http://192.168.0.50:42881"
export VITE_POCKETBASE_URL="http://192.168.0.50:42917"
# 4) compose에서 image로 받는 서비스(PocketBase 등) 최신화가 필요하면(선택)
docker compose pull
# 5) 웹 이미지는 저장소 코드로 다시 빌드해야 반영된다
docker compose up -d --build
VITE_* 값이 이전과 같아도, 프런트나 Dockerfile이 바뀌었으면 --build로 다시 빌드하는 편이 안전하다.
11. HTTPS와 PWA(나중에 해도 됨)
집 안 HTTP만으로도 개발·가족용으로는 쓸 수 있다.
다만 PWA 설치·서비스 워커는 브라우저·환경에 따라 HTTPS를 요구하는 경우가 많다.
공유기 뒤에서 도메인을 달거나, NAS 리버스 프록시로 TLS 종료하는 구성을 최종 목표로 두면 좋다. (구체 도메인·인증서 발급은 NAS/OS마다 다르다.)
11-1. ERR_SSL_VERSION_OR_CIPHER_MISMATCH(api 서브도메인)
PocketBase 컨테이너는 기본적으로 HTTPS(TLS)를 제공하지 않는다. https://api.todo.sori.studio는 리버스 프록시가 443에서 TLS를 종료하고, 뒤쪽은 http://<NAS>:42917처럼 평문 HTTP로 PocketBase에 넘기는 구조가 일반적이다.
이 오류는 브라우저↔443 구간에서 TLS 협상이 깨질 때 난다. 앱·Compose 코드 문제라기보다 프록시·인증서·포트 쪽을 본다.
-
PocketBase 자체 확인
브라우저에서http://<NAS_LAN_IP>:42917/_/(호스트 포트는docker-compose.yaml기준)로 관리자가 열리면 PB는 정상이다. -
DNS
api.todo.sori.studio가 지금 TLS를 설정한 그 NAS(또는 프록시) IP를 가리키는지 확인한다. -
리버스 프록시 443 설정
api용 서버 블록에 유효한 인증서(Let’s Encrypt 등)가 붙어 있는지, 만료·체인 누락이 없는지.proxy_pass등 업스트림은http://127.0.0.1:42917처럼 PB의 HTTP 포트로 보내는지(HTTPS로 백엔드에 붙이면 PB는 응답하지 않는다).- 443에서 다른 서비스의 기본 인증서·잘못된 가상 호스트가 응답하고 있지 않은지.
-
Cloudflare 등 CDN을 쓰는 경우
SSL 모드가 Full(strict) 인데 오리진에 맞는 인증서가 없으면 비슷한 증상이 날 수 있다. 오리진은 HTTP만 둘 거면 Flexible(보안상 트레이드오프 있음) 또는 오리진에 정식 TLS를 올리고 Full에 맞추는 식으로 정책을 맞춘다. -
터미널 확인(선택)
curl -vI https://api.todo.sori.studio로 응답 헤더·인증서 이름이 기대와 같은지 본다.
11-2. Cloudflare Tunnel + NPM, SSL 모드 Full
Full의 의미(오렌지 구름·일반 프록시 기준):
사용자↔Cloudflare는 HTTPS이고, Cloudflare↔오리진(집의 서버) 도 HTTPS로 연결하려 한다. 그래서 오리진 쪽(예: NPM이 443에서 받는 호스트)에는 TLS가 켜져 있고, Cloudflare가 신뢰할 수 있는지는 모드에 따라 다르다(Full은 자체서명도 허용, Full (strict) 는 정식 체인 필요).
PocketBase는 HTTP만 제공하므로, Cloudflare가 최종적으로 붙는 대상은 “PB 컨테이너”가 아니라 그 앞의 NPM(또는 터널) 이어야 한다. NPM이 api.todo.sori.studio에 대해 443에서 인증서를 내고, 내부 업스트림만 http://192.168.50.146:42917 같은 HTTP로 넘기면 된다.
Cloudflare Tunnel을 같이 쓰는 경우 흔한 오해:
- 터널의 Public Hostname 서비스 URL은 PocketBase에 맞게
http://+ LAN IP + 포트(예:http://192.168.50.146:42917)로 두는 경우가 많다. 여기를https://…로 잘못 두면 PB는 TLS를 못 하므로 협상 오류로 이어질 수 있다. - Tunnel로 끝내는 호스트와 NPM으로만 노출하는 호스트를 같은 서브도메인에 이중으로 겹치면 경로가 꼬이기 쉽다.
api한 줄은 터널만 또는 NPM만 중 하나가 실제로 받도록 정리하는 것이 좋다. - 대시보드 SSL/TLS → 개요의
Full은, DNS가 터널(CNAME)만 타고 오렌지 프록시를 안 쓰는 구성과는 동작 맥락이 다를 수 있다. 문제가 나면 Zero Trust에서 해당 Public Hostname의 스킴·포트·대상 IP를 다시 확인한다.
12. NAS에 Git이 없을 때
- Mac/PC에서 클론한 뒤
rsync나 압축(zip)으로todo폴더 전체를 NAS에 올리고, NAS에서는docker compose up -d --build만 실행한다. - 이 경우에도 **6절의
export VITE_POCKETBASE_URL**은 동일하게 필요하다.
13. 자주 막히는 지점
| 증상 | 점검 |
|---|---|
| 웹은 뜨는데 데이터가 안 됨 | PocketBase todos 컬렉션·필드명, API 규칙, CORS에 웹 출처(http://…:42881 또는 https://todo.sori.studio)가 들어갔는지 |
| 폰에서만 안 됨 | VITE_POCKETBASE_URL이 127.0.0.1이 아닌지, 폰이 같은 Wi‑Fi인지, NAS IP가 맞는지 |
| 빌드 후에도 주소가 이상함 | docker compose build --no-cache 후 다시 up, 또는 export를 잊지 않았는지 |
| ARM NAS에서 이미지 오류 | 사용 중인 이미지가 해당 CPU 아키텍처를 지원하는지 로그로 확인한다. |
| 컨테이너 이름 충돌 | pocketbase-todo·todo-web이 이미 있으면 docker-compose.yaml의 container_name을 바꾼다. |
fatal: not a git repository |
apps가 아니라 cd todo(클론한 폴더) 안에서 실행했는지 확인한다. |
git pull이 안 되거나 안 받아짐 |
10절 순서대로 git status·fetch·checkout main·pull --ff-only, 로컬 수정·비공개 저장소 자격 확인 |
untracked working tree files would be overwritten (docker-compose.yaml 등) |
그 파일이 추적되지 않은 로컬 파일이라 pull이 덮어쓰기를 거부한 것이다. 백업 후 mv docker-compose.yaml docker-compose.yaml.bak 또는 삭제한 뒤 git pull한다. 필요하면 diff로 원격과 맞춘 뒤 수동 병합한다. |
ERR_SSL_VERSION_OR_CIPHER_MISMATCH (https://api…) |
11-1절. 프록시 443·인증서·proxy_pass가 PB HTTP 포트로 가는지 확인 |
PocketBase unable to open database file (14) |
./pb_data 쓰기 권한. docker-compose.yaml에서 user: "1000:10"을 켠 상태면 호스트에서 sudo chown -R 1000:10 pb_data. 또는 user 줄을 주석으로 둔 채 기동해 DB 생성 후 chown하고 user 재적용. |
14. 한 페이지로 순서만 보기
- SSH 접속
cd /volume1/docker/projects/appsgit clone … todo또는cd todo후git clone … .- NAS LAN IP 확인
- LAN만 쓸 때:
export VITE_PUBLIC_APP_URL="http://<NAS_IP>:42881"및export VITE_POCKETBASE_URL="http://<NAS_IP>:42917"(또는 6-0과 같이.env에 기록) docker compose up -d --build- 브라우저:
:42881앱,:42917/_/관리자 todos컬렉션·CORS·API 규칙 설정
이후 작업은 cd …/apps/todo → git fetch → git pull --ff-only origin main(또는 추적 브랜치) → 필요 시 export 또는 .env 확인 → docker compose pull(선택) → docker compose up -d --build를 반복하면 된다.