Files
todo.sori.studio/docs/nas-deploy-guide.md
2026-04-13 14:36:16 +09:00

341 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NAS 배포 상세 가이드 (UGREEN 등 Docker 호스트)
이 문서는 **다른 PC에서 작업한 뒤 NAS에 올리는 경우**까지 포함해, 처음부터 끝까지 순서대로 따라 할 수 있도록 적었다.
요약·변수 설명은 `docs/deploy.md`를 참고하고, 여기서는 **SSH 기준으로 한 단계씩** 기술한다.
앱·문서의 **공식 버전**은 `package.json``version``docs/spec.md``현재 버전`을 기준으로 한다.
---
## 1. 전제 조건(미리 확인)
다음이 갖춰져 있어야 한다. 기종마다 메뉴 이름은 다를 수 있다.
1. **Docker**(및 **Docker Compose** 플러그인 또는 `docker compose` 명령)가 NAS에서 동작한다.
2. **SSH**로 NAS에 접속할 수 있다. (예: Mac 터미널에서 `ssh nas`처럼 호스트 별칭을 써도 된다.)
3. 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 접속 후 폴더로 이동
```bash
ssh <NAS에_설정한_호스트이름>
```
접속 후:
```bash
cd /volume1/docker/projects/apps
```
`apps` 목록에 `todo`가 보이는지 확인한다.
```bash
ls
```
---
## 4. Git으로 가져오기(권장: 빈 폴더에 클론)
### 4-1. `todo` 폴더가 **아직 없거나**, 비어 있게 만들 수 있는 경우
`apps`에서:
```bash
git clone https://git.sori.studio/zenn/todo.sori.studio.git todo
cd todo
```
이렇게 하면 `todo` 안에 저장소 파일이 채워진다.
### 4-2. 이미 `todo` 폴더로 들어가 있는데 **안이 비어 있는** 경우
`todo` 안에서 현재 디렉터리에 클론한다.
```bash
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. 클론 후 한 번 확인
```bash
cd /volume1/docker/projects/apps/todo
ls
```
`docker-compose.yaml`, `Dockerfile`, `package.json` 등이 보이면 된다.
---
## 5. NAS의 LAN IP 확인(중요)
웹앱은 **사용자의 브라우저**에서 PocketBase 주소로 직접 요청한다.
그래서 빌드에 넣는 주소는 **“브라우저가 NAS에 접속할 때 쓰는 주소”**여야 한다.
예시:
- `docker-compose.yaml`에 적어 둔 **호스트 포트**(웹 `42881`, PocketBase `42917`)는 다른 스택과 겹치지 않게 잡은 값이다. 겹치면 해당 파일의 `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` 예시:
```bash
VITE_PUBLIC_APP_URL=https://todo.sori.studio
VITE_POCKETBASE_URL=https://api.todo.sori.studio
```
그다음:
```bash
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`로 한 세션에만 줄 수도 있다.
```bash
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. 잘 떴는지 확인
```bash
docker compose ps
```
`pocketbase-todo`, `todo-web`(컨테이너 이름·`docker-compose.yaml``container_name`)이 `running`에 가깝게 보이면 된다.
### 6-3. 로그가 궁금할 때
```bash
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 최초 설정(한 번)
1. 브라우저에서 `http://<NAS_IP>:42917/_/` 를 연다. (호스트 포트를 바꿨다면 그 번호로 연다.)
2. 관리자 계정(이메일·비밀번호)을 만들고 마법사를 끝낸다.
3. **컬렉션 `todos` 생성**
- 필드 `title`: 타입 **Text**
- 필드 `done`: 타입 **Bool**
4. **API 규칙**
처음에는 테스트로 느슨하게 두었다가, 나중에 로그인 기반으로 조이는 것을 권장한다. (운영 정책에 맞게 조정.)
5. **설정 → CORS**
웹앱 출처를 허용 목록에 넣는다. 예: `http://192.168.0.50:42881` 또는 `https://todo.sori.studio`
포트·프로토콜·호스트가 **실제로 주소창에 쓰는 것과 한 글자라도 다르면** 브라우저가 차단한다.
이후 `http://<NAS_IP>:42881`에서 목록·추가·완료 토글이 동작하는지 본다.
---
## 9. 왜 `export`로 주소를 줄이는가
`VITE_POCKETBASE_URL`**Docker 이미지를 빌드할 때** 프런트 번들에 박힌다.
NAS IP나 도메인이 바뀌면, **다시 빌드**해야 한다.
```bash
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`를 안 한 상태**면 기대한 대로 안 받아질 수 있다. 아래 순서를 권장한다.
```bash
cd /volume1/docker/projects/apps/todo
ls -a .git
```
`.git`이 보이면 이 경로가 저장소 루트가 맞다. 없다면 상위 `apps`에 있지 않은지, 폴더 이름이 `todo`가 맞는지 확인한다.
```bash
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`이 된다.
이후 **이미지·번들**을 갱신한다.
```bash
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 코드 문제라기보다 **프록시·인증서·포트** 쪽을 본다.
1. **PocketBase 자체 확인**
브라우저에서 `http://<NAS_LAN_IP>:42917/_/`(호스트 포트는 `docker-compose.yaml` 기준)로 관리자가 열리면 PB는 정상이다.
2. **DNS**
`api.todo.sori.studio`**지금 TLS를 설정한 그 NAS(또는 프록시)** IP를 가리키는지 확인한다.
3. **리버스 프록시 443 설정**
- `api`용 서버 블록에 **유효한 인증서**(Lets Encrypt 등)가 붙어 있는지, 만료·체인 누락이 없는지.
- `proxy_pass`**업스트림은 `http://127.0.0.1:42917`** 처럼 PB의 **HTTP** 포트로 보내는지(HTTPS로 백엔드에 붙이면 PB는 응답하지 않는다).
- 443에서 다른 서비스의 기본 인증서·잘못된 가상 호스트가 응답하고 있지 않은지.
4. **Cloudflare 등 CDN을 쓰는 경우**
SSL 모드가 **Full(strict)** 인데 오리진에 맞는 인증서가 없으면 비슷한 증상이 날 수 있다. 오리진은 HTTP만 둘 거면 **Flexible**(보안상 트레이드오프 있음) 또는 오리진에 정식 TLS를 올리고 **Full**에 맞추는 식으로 정책을 맞춘다.
5. **터미널 확인(선택)**
`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`이 아닌지, 폰이 같은 WiFi인지, 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`, 로컬 수정·비공개 저장소 자격 확인 |
| `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. 한 페이지로 순서만 보기
1. SSH 접속
2. `cd /volume1/docker/projects/apps`
3. `git clone … todo` **또는** `cd todo``git clone … .`
4. NAS LAN IP 확인
5. LAN만 쓸 때: `export VITE_PUBLIC_APP_URL="http://<NAS_IP>:42881"``export VITE_POCKETBASE_URL="http://<NAS_IP>:42917"` (또는 6-0과 같이 `.env`에 기록)
6. `docker compose up -d --build`
7. 브라우저: `:42881` 앱, `:42917/_/` 관리자
8. `todos` 컬렉션·CORS·API 규칙 설정
이후 작업은 **`cd …/apps/todo`** → `git fetch``git pull --ff-only origin main`(또는 추적 브랜치) → 필요 시 `export` 또는 `.env` 확인 → `docker compose pull`(선택) → `docker compose up -d --build`를 반복하면 된다.