Files
sori.studio/docs/deploy.md

548 lines
28 KiB
Markdown

# 배포 가이드
> 로컬 기준 v1.5.75에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형
| 유형 | 명령어 | 용도 |
|------|--------|------|
| 개발 | `npm run dev` | 로컬 테스트, 개발 서버 |
| 프로덕션 | `npm run build` | NAS 배포, 운영 서버 |
| 검증 | `npm run verify` | JavaScript 문법 점검 + 프로덕션 빌드 |
> `npm run dev`는 프로젝트 전용 실행 스크립트를 통해 개발 서버, Admin, Tailwind Viewer 링크만 요약 출력한다.
---
## 로컬 개발
### v1.5.75 참고
- 추가 DB 마이그레이션은 없다.
- 게시물 작성·수정 화면 오른쪽 설정 패널 하단에 본문 통계가 표시되는지 확인한다.
- 본문 입력 시 단어 수, 공백 제외 문자 수, 공백 수, 읽기 시간, 블록 수, 이미지 수가 갱신되는지 확인한다.
### v1.5.74 참고
- DB 마이그레이션 `053_site_settings_post_sidebar_ad.sql` 적용 필요.
- 사이트 설정 Ads에서 게시물 왼쪽 사이드 광고 코드를 저장한 뒤 게시물 상세 데스크톱 왼쪽 사이드바 하단에만 표시되는지 확인한다.
- 게시물 상세 오른쪽 사이드바에서는 공통 오른쪽 사이드 광고가 표시되지 않는지 확인한다.
- 긴 게시물에서 인아티클 광고가 본문 길이에 따라 0~2회로 제한되는지 확인한다.
### 필수 조건
- Node.js 22 LTS 권장
- npm 9+
- 개발 DB
### 실행 단계
```bash
# 프로젝트 클론
git clone https://git.sori.studio/zenn/sori.studio.git
# 디렉토리 이동
cd sori.studio
# 의존성 설치
npm install
# 개발 환경 변수 설정
# .env.development는 Git에 올리지 않는 로컬 전용 파일
# 새로 만들 때는 .env.example을 복사한 뒤 비밀번호를 랜덤 값으로 교체
cp .env.example .env.development
openssl rand -hex 32
# 로컬 DB 컨테이너를 호스트에서 접근할 때는 127.0.0.1:43119 사용
# 개발 서버 실행 (127.0.0.1:43117)
npm run dev
```
### v1.5.55 참고
- 추가 DB 마이그레이션은 없다.
- 소스 모드 textarea에서 `Cmd+Shift+K`로 현재 줄이 삭제되는지 확인한다.
- 소스 모드 textarea에서 여러 줄 선택 후 `Cmd+Shift+K`로 선택 범위 줄들이 삭제되는지 확인한다.
- 라이브 모드에서 preview 루트 또는 카드형 블록에 포커스된 상태에서도 `Cmd+Shift+K`로 현재 줄 또는 블록이 삭제되는지 확인한다.
### v1.5.54 참고
- 추가 DB 마이그레이션은 없다.
- `/콜아웃` 삽입 시 기본 선언부가 `emoji=none`으로 들어가고 아이콘이 표시되지 않는지 확인한다.
- 오른쪽 블록 설정 패널에서 콜아웃 제목을 입력하면 선언부 `title` 옵션과 라이브·공개 렌더링에 반영되는지 확인한다.
- 콜아웃 아이콘 또는 제목이 있을 때 헤더가 왼쪽 상단에 표시되고 본문은 아래 줄에서 시작하는지 확인한다.
### v1.5.53 참고
- 추가 DB 마이그레이션은 없다.
- 라이브 콜아웃 본문 5줄 이상에서 `Shift+방향키` 범위 선택이 여러 줄에 걸쳐 유지되는지 확인한다.
- 라이브 콜아웃 선택 범위를 삭제하거나 붙여넣을 때 콜아웃 본문 줄만 갱신되는지 확인한다.
- 콜아웃 아이콘 사용 시 라이브·사용자 화면 모두 왼쪽 상단에 아이콘이 붙는지 확인한다.
- 콜아웃 아이콘 미사용 시 라이브 편집 화면과 사용자 화면 모두 아이콘 자리 표시자가 남지 않는지 확인한다.
### 로컬 개발 DB
로컬 개발 DB는 Docker Compose의 `sori-studio-db` 서비스만 실행한다.
```bash
# Docker daemon 시작
# Docker Desktop을 사용하면 Docker.app을 먼저 실행
# Colima를 사용하면 아래 명령 실행
colima start
# 개발 DB 컨테이너 실행
ENV_FILE=.env.development docker compose --env-file .env.development up -d sori-studio-db
# 개발 DB 마이그레이션 실행
npm run db:migrate:dev
# DB 준비 상태 확인
docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
# 시드 데이터 확인
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
```
### v1.5.52 참고
- 추가 DB 마이그레이션은 없다.
- 연속 콜아웃을 만들고 위 콜아웃에서 한글 입력 후 Enter 시 아래 콜아웃 선언 줄이 유지되는지 확인한다.
- 라이브 콜아웃 안에서 한글 입력 후 Enter 시 본문 줄이 1줄만 추가되는지 확인한다.
- 갤러리·토글·임베드 등 다른 `:::` fenced 블록 편집 시 다음 블록 첫 줄이 교체 범위에 포함되지 않는지 확인한다.
### v1.5.51 참고
- 추가 DB 마이그레이션은 없다.
- 라이브 인용 안에서 한글 입력 후 Enter 시 인용 줄이 1줄만 추가되는지 확인한다.
- 라이브 콜아웃 안에서 한글 입력 후 Enter 시 콜아웃 본문 줄이 1줄만 추가되는지 확인한다.
- 라이브 콜아웃 마지막 줄에서 아래 방향키 입력 시 새 본문 줄이 생성되지 않는지 확인한다.
- 라이브 인용 마지막 줄에서 아래 방향키 입력 시 외부 빈 문단이 생성되고 커서가 이동하는지 확인한다.
### v1.5.50 참고
- 추가 DB 마이그레이션은 없다.
- 라이브 모드에서 한글 `> 텍스트` 입력 후 Enter 시 다음 인용 줄이 생성되고 커서가 내부에 있는지 확인한다.
- 라이브 인용 마지막 줄에서 아래 방향키 입력 시 외부 빈 문단이 생성되고 커서가 이동하는지 확인한다.
- 라이브 콜아웃 본문 여러 줄에서 2번째·3번째 줄 `Cmd+Shift+K`가 해당 줄만 삭제하는지 확인한다.
- 라이브 콜아웃에서 한글 입력 후 Enter 시 본문 줄이 1줄만 추가되는지 확인한다.
- `/콜아웃` Enter 생성 후 콜아웃 본문에 마지막 한글 조합 글자가 남지 않는지 확인한다.
### v1.5.49 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 글쓰기 라이브 모드에서 코드·콜아웃·토글 내부 커서 위치별 `Cmd+Shift+K` 줄 삭제를 확인한다.
- 코드·콜아웃·토글 본문 마지막 1줄에서 `Cmd+Shift+K` 입력 시 블록 전체가 삭제되는지 확인한다.
- 라이브 콜아웃 마지막 줄에서 아래 방향키로 다음 블록으로 빠져나가는지 확인한다.
- `/콜아웃` Enter 생성 후 콜아웃 본문에 마지막 조합 글자가 남지 않는지 확인한다.
### v1.5.48 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 게시물 작성 상단 왼쪽 상태 표시가 텍스트만 표시하고, 외부 이동 아이콘이나 링크 동작이 없는지 확인한다.
- 공개 게시물 이동은 오른쪽 설정 패널의 `View Post` 링크에서 확인한다.
### v1.5.47 참고
- 추가 DB 마이그레이션은 없다.
- 대표 이미지가 있는 공개 게시물이 RSS item에 `media:thumbnail``media:content`를 포함하는지 확인한다.
- 상대 이미지 URL이 RSS에서 절대 URL로 변환되는지 확인한다.
### v1.5.46 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 글쓰기 라이브 모드에서 위에서 아래 방향키로 콜아웃·인용 블록에 진입되는지 확인한다.
- 콜아웃·인용 배경 프리셋에 분홍이 보이지 않고 같은 6색 팔레트를 쓰는지 확인한다.
- 작은 화면에서 게시물 설정 패널을 열어도 본문 폭이 압축되지 않는지 확인한다.
### v1.5.45 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 글쓰기에서 라이브 모드 콜아웃·인용 블록 포커스 시 오른쪽 블록 설정 패널이 열리는지 확인한다.
- 인용 블록 기본 배경이 회색이고 분홍 옵션이 노출되지 않는지 확인한다.
### v1.5.44 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 사이트 설정의 블로그 제목·설명, 사이트 정보, 사이트 코드 읽기 화면만 레이아웃 변경되었다.
### v1.5.43 참고
- 추가 DB 마이그레이션은 없다.
- 배포 후 `/rss.xml`, `/feed.xml`, `/rss``application/rss+xml`로 응답하고 최근 공개 발행글을 포함하는지 확인한다.
- 관리자 SNS 정보에서 RSS 프리셋을 사용할 경우 주소는 `/rss.xml`을 권장한다.
### v1.5.42 참고
- 추가 DB 마이그레이션은 없다.
- 공개 오른쪽 사이드바 FOLLOW 영역의 직접 SVG 아이콘 정렬만 수정한다.
### v1.5.41 마이그레이션
- `049_fix_social_links_jsonb_string.sql`: 기존에 JSONB 문자열로 잘못 저장된 `site_settings.social_links` 값을 JSONB 배열로 복구한다.
- 적용 후 관리자 사이트 설정의 SNS 정보 저장값이 읽기 모드에서 유지되는지 확인한다.
### v1.5.40 참고
- 추가 DB 마이그레이션은 없다.
- 관리자 사이트 설정의 SNS 정보에서 프리셋이 없는 서비스는 `직접 SVG`를 선택해 SVG 아이콘과 주소를 함께 저장한다.
- `https://`를 생략한 SNS 주소는 저장 시 자동 보정된다.
### v1.5.39 마이그레이션
- `048_site_settings_social_links.sql`: `site_settings``social_links` JSONB 컬럼을 추가한다.
- 적용 후 관리자 사이트 설정의 SNS 정보와 공개 오른쪽 사이드바 FOLLOW 노출이 정상 동작하는지 확인한다.
### v1.5.38 마이그레이션
- `047_site_settings_announcement_alignment.sql`: `site_settings``announcement_alignment` 컬럼을 추가한다.
- 적용 후 관리자 사이트 설정의 어나운스 바 정렬(중앙/왼쪽)이 공개 화면에 반영되는지 확인한다.
### v1.5.35 마이그레이션
- `045_analytics_traffic_sources.sql`: 방문자 유입원·디바이스·검색 키워드 일별 축약 집계 테이블과 중복 방문 제거용 컬럼을 추가한다.
- 적용 이후부터 수집되는 페이지뷰에 대해서만 유입 정보가 쌓인다. 과거 방문 데이터는 소급 집계하지 않는다.
- 검색 키워드는 검색엔진이 referrer query를 전달한 경우에만 표시된다.
### v1.5.34 마이그레이션
- `044_site_settings_custom_code.sql`: `site_settings``ads_txt`, `custom_head_code`, `custom_footer_code` 컬럼을 추가한다.
- 배포 후 `/ads.txt`와 공개 페이지 HTML head/body 하단 코드 삽입이 정상 동작하는지 확인한다.
### 확인 주소
- 개발 서버: http://127.0.0.1:43117
- 관리자: http://127.0.0.1:43117/admin
- Tailwind Viewer: http://127.0.0.1:43117/_tailwind/
### 로컬 DB 확인 방법
로컬 개발 DB는 PostgreSQL이며 호스트에서는 `127.0.0.1:43119`로 접근한다. 접속 정보는 Git에 포함하지 않는 `.env.development` 값을 사용한다.
| 항목 | 값 |
|------|----|
| Host | `127.0.0.1` |
| Port | `43119` |
| Database | `.env.development``POSTGRES_DB` |
| User | `.env.development``POSTGRES_USER` |
| Password | `.env.development``POSTGRES_PASSWORD` |
터미널에서 바로 확인할 때는 컨테이너 내부 `psql`을 사용한다.
```bash
# DB 준비 상태 확인
docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
# 게시물 개수 확인
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
# psql 콘솔 접속
docker exec -it sori-studio-db psql -U sori_studio -d sori_studio
```
GUI로 확인할 때는 DBeaver, TablePlus, DataGrip, CloudBeaver 같은 PostgreSQL 클라이언트에서 위 접속 정보를 입력한다. phpMyAdmin은 MySQL/MariaDB용 도구라 이 프로젝트의 PostgreSQL DB 확인 용도로는 사용하지 않는다.
---
## UGREEN NAS Docker 배포
> Dockerfile과 docker-compose 설정은 초안이며 NAS 운영 환경에서는 아직 검증 전이다.
### SSH 접속
```bash
ssh [NAS_IP]
```
### 프로젝트 설치
```bash
# 프로젝트 디렉토리로 이동
cd /volume1/docker/projects/apps/
# 프로젝트 클론
git clone https://git.sori.studio/zenn/sori.studio.git
# 디렉토리 이동
cd sori.studio
# 운영 환경 변수 설정
# .env.production은 Git에 올리지 않는 운영 전용 파일
cp .env.example .env.production
# .env.production 파일에 운영 DB 연결 정보, 운영 전용 랜덤 비밀번호, APP_PORT=43118 입력
# 운영 DB에 owner/admin이 없으면 /admin/login에서 ADMIN_EMAIL/ADMIN_PASSWORD로 최초 owner 계정이 생성됨
# MEMBER_SESSION_SECRET은 ADMIN_PASSWORD와 다른 긴 난수 문자열로 반드시 입력
# Docker 네트워크 대역이 NAS 기존 컨테이너와 겹치면 DOCKER_SUBNET을 다른 사설 대역으로 변경
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용
# Docker 빌드 및 실행
docker compose --env-file .env.production up -d --build
# 운영 DB 마이그레이션 상태 확인
sh scripts/migrate-production-db.sh status
# schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음)
sh scripts/migrate-production-db.sh baseline
# 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행
sh scripts/migrate-production-db.sh migrate
```
### 운영 업데이트 (코드 반영)
이미 한 번 올려 둔 NAS에서 **새 커밋을 받아 반영**할 때는 보통 아래 순서를 따른다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
```bash
# 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정)
cd /volume1/docker/projects/apps/sori.studio
# 원격 저장소 최신 코드 받기
git pull
# DB 스키마 변경이 포함된 배포면 미적용 SQL만 적용 (npm 없이 실행 가능)
sh scripts/migrate-production-db.sh status
sh scripts/migrate-production-db.sh migrate
# 앱 이미지 재빌드 후 컨테이너 재기동
docker compose --env-file .env.production up -d --build
```
| 단계 | 설명 |
|------|------|
| `git pull` | 애플리케이션·Dockerfile·`db/migrations` 등 Git에 있는 변경을 받는다. |
| `migrate` | `db/migrations/`에 새 SQL이 있으면 운영 DB에만 적용한다. 스키마 변경이 없으면 생략해도 된다. |
| `up -d --build` | Nuxt 프로덕션 빌드가 Docker 이미지 안에서 수행되므로, **NAS 호스트에 Node/npm이 없어도** 앱 코드 반영이 가능하다. |
주의:
- `.env.production`은 Git에 포함하지 않는다. `git pull`로 덮어쓰이지 않는다. 값을 바꿀 때만 파일을 직접 수정한다.
- `public/uploads/` 업로드 파일은 Docker 볼륨(`./public/uploads`)에 있으므로, **이미지 파일만 추가·수정한 경우** 앱 재빌드 없이도 URL로 바로 보인다.
- 로컬에서 미리 확인하려면 `npm run verify` 후 NAS에서 위 명령을 실행하면 된다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
```bash
docker compose --env-file .env.production up -d
```
코드 변경 없이 `.env.production`만 수정했다면 `--build` 없이 `up -d`만으로 충분하다.
### Docker 네트워크 충돌 대응
NAS에 Docker 컨테이너가 많이 실행 중이면 `could not find an available, non-overlapping IPv4 address pool` 오류가 날 수 있다. 이 프로젝트는 기본 `DOCKER_SUBNET=10.250.50.0/24`를 사용한다. 해당 대역도 NAS 내부망 또는 다른 Docker 네트워크와 겹치면 `.env.production`에서 예를 들어 아래처럼 바꾼 뒤 다시 실행한다.
```bash
DOCKER_SUBNET=10.250.51.0/24
docker compose --env-file .env.production up -d --build
```
### 포트
- 로컬 개발: 43117
- NAS Docker 외부: 43118
- 컨테이너 내부: 3000
- PostgreSQL 외부: 43119
- Docker 내부 네트워크 기본값: `10.250.50.0/24`
- HTTPS: 3001 (SSL 설정 시)
---
## 데이터베이스
- 로컬 개발: `.env.development``DATABASE_URL`
- NAS 운영: `.env.production``DATABASE_URL`
- 로컬 개발 예시: `postgres://sori_studio:비밀번호@127.0.0.1:43119/sori_studio`
- NAS Docker 예시: `postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio`
- `.env.example`에는 실제 비밀번호나 개인 이메일을 기록하지 않음
- 개발/운영 DB 비밀번호와 관리자 비밀번호는 서로 다른 랜덤 값을 사용
- `ADMIN_EMAIL`/`ADMIN_PASSWORD`는 운영 DB에 owner/admin이 없는 최초 관리자 생성에만 사용한다. 같은 이메일의 일반 회원이 이미 있으면 owner로 승격하고 비밀번호를 `ADMIN_PASSWORD` 기준으로 갱신한다. 첫 owner 계정이 DB에 생성된 뒤에는 관리자 로그인도 DB의 bcrypt 비밀번호를 기준으로 검증한다.
- 회원 세션 서명용 `MEMBER_SESSION_SECRET`은 관리자 비밀번호와 분리된 긴 난수 문자열을 사용
- 개발 DB와 운영 DB는 반드시 별도 인스턴스 또는 별도 데이터베이스로 분리
- 운영 DB는 로컬 개발 서버에서 직접 연결하지 않음
- 운영 환경에서 `DATABASE_URL`이 없으면 샘플 콘텐츠로 대체하지 않고 서버 오류로 실패
- Docker 운영 컨테이너는 `.env.production`의 서버 환경 변수를 런타임 `process.env`에서 우선 읽는다.
### 이메일 인증(Resend, 선택)
회원가입(일반)·비밀번호 찾기에 이메일 OTP를 쓰려면 `npm run db:migrate:dev``018_email_otp_challenges.sql`을 적용하고, `.env`에 다음을 설정한다.
| 변수 | 설명 |
|------|------|
| `RESEND_API_KEY` | [Resend](https://resend.com) API 키 |
| `RESEND_FROM_EMAIL` | 발신 주소(Resend에서 허용된 도메인 또는 테스트 발신자) |
| `MEMBER_SESSION_SECRET` | 회원 세션 쿠키 서명용 비밀값. 운영에서는 필수이며 `ADMIN_PASSWORD`와 분리된 긴 난수 문자열을 사용한다. **OTP 해시에 쓰는 pepper로도 사용**되므로, `EMAIL_OTP_PEPPER`를 비워 두면 이 값이 OTP용 비밀 재료가 된다. |
| `EMAIL_OTP_PEPPER` | **선택.** 이메일로 받은 6자리 숫자를 DB에 넣기 전 SHA256 해시할 때 섞는 **서버 전용 비밀 문자열**이다. DB가 유출돼도 pepper를 모르면 인증번호 역산·무차별 대입이 어렵다. **짧은 숫자 한두 개가 아니라**, `openssl rand -hex 32`처럼 **긴 난수 문자열(32바이트 이상 권장)**을 쓰는 것이 안전하다. 비우면 `MEMBER_SESSION_SECRET`을 pepper로 쓴다. |
`RESEND_API_KEY``RESEND_FROM_EMAIL`이 비어 있으면 기존처럼 이메일 OTP 없이 최초 관리자 가입·일반 가입(OTP 생략)이 동작한다.
### 게시물 Export 설정(선택)
| 변수 | 설명 |
|------|------|
| `POST_EXPORT_MAX_FILE_SIZE_BYTES` | 게시물 Export 분할 ZIP 목표 최대 용량. 기본값은 500MB이며 관리자 설정 화면 요청값이 있으면 그 값을 우선 사용한다. |
- 게시물 Import는 관리자 설정의 Import 패널에서 Export ZIP 파일을 업로드해 실행한다. 1회 업로드 파일은 300MB 이하, Markdown 게시물은 최대 1000개까지 처리한다.
- 관리 도구: CloudBeaver 등으로 추후 연결 가능하게 설계
- NAS Docker 배포 시 PostgreSQL 초기 스키마는 `db/migrations/`의 SQL로 생성
- 로컬 개발 Docker Compose 실행 시 `ENV_FILE=.env.development``--env-file .env.development`를 함께 사용
- 로컬 개발 DB 마이그레이션은 `npm run db:migrate:dev`로 실행
- NAS 운영 DB 마이그레이션은 NAS 호스트에 npm이 없어도 실행할 수 있도록 `sh scripts/migrate-production-db.sh status`로 적용 상태를 확인하고, `sh scripts/migrate-production-db.sh migrate`로 미적용 파일만 실행한다.
- 운영 환경 파일은 프로젝트 루트의 `.env.production`을 우선 사용한다. 없으면 `.env`를 읽고, 둘 다 없으면 실행 중인 `sori-studio-db` 컨테이너의 `POSTGRES_DB`·`POSTGRES_USER`를 사용한다.
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `migrate`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `sh scripts/migrate-production-db.sh baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `sh scripts/migrate-production-db.sh baseline 031`.
- 네비게이션 계층(`parent_id`, `is_folder`)은 `017_navigation_hierarchy.sql` 적용 후 저장 API가 정상 동작한다(미적용 시 `INSERT` 컬럼 불일치).
- 회원 마지막 로그인 표시(`previous_last_seen_at`, `previous_last_seen_ip`)는 `021_add_member_previous_login.sql` 적용 후 정상 동작한다.
- 사이트 로고와 파비콘 저장(`logo_url`, `favicon_url`)은 `022_add_site_logo_urls.sql` 적용 후 정상 동작한다.
### 통계 데이터 보관 정책
- `site_analytics_daily`, `post_analytics_daily`: 사이트 전체 방문자와 게시물별 조회수의 누적 원본이므로 자동 삭제하지 않는다.
- `analytics_traffic_daily`: 유입원·디바이스·키워드 축약 집계 원본이므로 자동 삭제하지 않는다.
- `analytics_daily_visitors`: 일별 중복 방문 제거용 해시만 담으므로 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
- `analytics_active_sessions`: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다.
- 관리자 대시보드 차트는 최대 365일 범위를 조회하며, 차트 범위를 넘는 집계도 누적 통계 원본으로 보관한다.
### 개발/운영 DB 분리 검증 절차
검증 전제는 실제 비밀번호나 전체 `DATABASE_URL`을 화면 공유, 문서, 커밋 메시지에 노출하지 않는 것이다. 확인할 때는 호스트, 포트, DB 이름, 파일명만 대조한다.
1. `.env.development` 확인.
```bash
# 로컬 개발 DB는 호스트 기준 127.0.0.1:43119를 사용해야 한다.
# DATABASE_URL 전체 값은 공유하지 않는다.
rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|DB_PORT)=" .env.development
```
기준:
- `DATABASE_URL` 호스트가 `127.0.0.1`
- `DATABASE_URL` 포트가 `43119`
- `DB_PORT=43119`
- 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
2. `.env.production` 확인.
```bash
# 운영 파일은 Git에 올리지 않는 운영 전용 파일이다.
# 값이 없으면 NAS 배포 전 작성해야 한다.
test -f .env.production && rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|APP_PORT)=" .env.production
```
기준:
- NAS Docker 내부 실행 기준이면 `DATABASE_URL` 호스트가 `sori-studio-db`
- NAS 외부 DB를 별도 인스턴스로 쓰는 경우에도 로컬 개발 DB(`127.0.0.1:43119`)를 가리키지 않음
- `APP_PORT=43118`
- `MEMBER_SESSION_SECRET`이 비어 있지 않고 `ADMIN_PASSWORD`와 다름
- `.env.development`와 DB 비밀번호, 관리자 비밀번호가 서로 다름
3. 로컬 개발 DB 연결 확인.
```bash
docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT current_database(), current_user;'
```
기준:
- `accepting connections` 표시
- `current_database`가 로컬 개발 DB 이름
- `current_user`가 로컬 개발 DB 계정
4. 로컬 개발 서버 연결 확인.
```bash
npm run dev
```
기준:
- 출력 주소가 `http://127.0.0.1:43117`
- 관리자 API 요청에서 `127.0.0.1:43119` 연결 오류가 발생하지 않음
5. 커밋 전 민감 정보 확인.
```bash
git status --short
git diff -- . ':!package-lock.json'
```
기준:
- `.env.development`, `.env.production`이 변경 목록에 포함되지 않음
- 문서와 코드 diff에 실제 DB 비밀번호, 관리자 비밀번호, 운영 접속 주소가 포함되지 않음
### 컨테이너가 `Restarting`일 때
`Error response from daemon: Container … is restarting, wait until the container is running`은 **프로세스가 곧바로 종료**되어 `restart: unless-stopped`가 반복 시도하는 상태다. 원인은 로그에 나온다.
1. **어느 서비스인지 확인** (`docker-compose.yml` 기준 이름은 `sori-studio`, `sori-studio-db`).
```bash
docker ps -a --filter "name=sori-studio"
```
2. **해당 컨테이너 로그** (가장 중요).
```bash
docker logs sori-studio --tail 150
docker logs sori-studio-db --tail 150
```
Compose로 올렸다면:
```bash
docker compose --env-file .env.production logs sori-studio --tail 200
docker compose --env-file .env.production logs sori-studio-db --tail 200
```
3. **자주 나오는 원인**
- **`sori-studio`**: `DATABASE_URL` 누락·오타, `MEMBER_SESSION_SECRET` 미설정, DB 호스트가 컨테이너 기준으로 잘못됨(예: 앱은 Docker 안인데 URL만 `127.0.0.1`로 DB를 가리킴), 애플리케이션 예외로 즉시 종료.
- **`sori-studio-db`**: 이미 초기화된 볼륨과 다른 `POSTGRES_PASSWORD`로 다시 올린 경우, `docker-entrypoint-initdb.d` 마이그레이션 SQL 오류, 디스크/권한 문제.
- **`sori-studio-db` 로그에 `ls: can't open '/docker-entrypoint-initdb.d/': Permission denied`**: 아래 **NAS·호스트에서 `db/migrations` 권한** 절차를 확인한다.
4. 로그를 고친 뒤에는 `docker compose --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다.
### NAS·호스트에서 `db/migrations` 권한
`docker-compose.yml``./db/migrations`를 Postgres 이미지의 `/docker-entrypoint-initdb.d`**읽기 전용**으로 붙인다. 공식 엔트리포인트는 이 디렉터리를 `ls`로 읽는데, NAS(UGREEN 등)나 SSH로 복사한 트리에서 **폴더·파일이 700/600만 허용**이거나 **상위 디렉터리에 실행(x) 비트가 없으면** 컨테이너 안 `postgres` 사용자가 경로를 통과하지 못해 `Permission denied`가 반복되고 DB 컨테이너가 재시작 루프에 들어갈 수 있다.
프로젝트 루트( `docker compose` 를 실행하는 디렉터리)에서 SSH로 다음을 적용한다. **비밀번호는 바꾸지 않으며**, 읽기·디렉터리 통과만 연다.
```bash
cd /volume1/docker/projects/apps/sori.studio
# 마이그레이션 디렉터리와 그 안 SQL: 모두 읽기, 디렉터리는 검색 가능
sudo chmod -R a+rX db/migrations
# 상위 db/, 프로젝트 루트가 다른 사용자만 rwx 인 경우 통과 허용
sudo chmod a+x . db db/migrations
```
그다음 DB 컨테이너만 재시작한다.
```bash
docker compose --env-file .env.production restart sori-studio-db
```
여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
## 업로드 파일
- 관리자 글쓰기에서 업로드한 이미지는 `/uploads/posts/YYYY/MM/` URL로 제공한다.
- 로컬 개발에서는 실제 파일이 `public/uploads/posts/YYYY/MM/` 아래 저장된다.
- 게시물 Export ZIP 산출물은 `public/uploads/exports/YYYY/MM/{jobId}/` 아래 생성되며, 관리자 다운로드 API를 통해 내려받는다.
- 완료·실패한 게시물 Export 작업을 관리자 화면에서 삭제하면 연결된 ZIP 파일도 함께 삭제된다.
- `public/uploads/`는 Git에 포함하지 않는다.
- NAS 운영에서는 `docker-compose.yml``./public/uploads:/app/public/uploads` 볼륨으로 업로드 파일을 유지한다.
- 운영 빌드에서는 `/uploads/**` 서버 라우트가 `.output/public`이 아니라 `/app/public/uploads` 볼륨의 실제 파일을 직접 제공한다.
- `MAX_FILE_SIZE`, `MAX_VIDEO_FILE_SIZE`, `MAX_AUDIO_FILE_SIZE`, `MAX_DOCUMENT_FILE_SIZE` 환경 변수로 관리자 미디어 업로드 최대 크기를 제한한다. 리버스 프록시(Nginx 등)를 쓰면 `client_max_body_size`가 앱 한도보다 작지 않은지 함께 확인한다.
- 관리자 미디어 화면은 현재 업로드 파일 시스템을 기준으로 목록, 파일명 변경, 삭제를 처리한다.
## 사용자 액션 필요 항목
- NAS SSH 접속 주소 확인.
- NAS 프로젝트 루트 경로 확정.
- 운영 DB 이름, 계정, 권한 확정.
- 운영 업로드 볼륨 경로 확정.
- 도메인 `sori.studio`의 NAS 연결 방식 확정.