v1.12.28 배포 문서를 상황별 복사 실행 명령어로 정리

This commit is contained in:
2026-10-21 17:46:06 +09:00
parent 2d4431e698
commit 184a971863
8 changed files with 705 additions and 594 deletions

View File

@@ -35,6 +35,11 @@
<main class="site-main w-full max-w-full lg:max-w-[720px]">
```
## 운영 문서
- `docs/deploy.md`는 상황별로 한 코드 블록을 복사해 실행할 수 있는 운영 명령어만 유지한다. 설치·고급 설정·장애 원인 설명은 `docs/deploy-reference.md`에 기록한다.
- 재배포처럼 순서가 중요한 명령은 실패 시 다음 단계로 진행하지 않도록 `&&`로 연결한다.
## 문서 (update-log)
- `docs/update-log/`는 블로그 방문자용 업데이트 안내 초안이다.

618
docs/deploy-reference.md Normal file
View File

@@ -0,0 +1,618 @@
# 배포 상세 참고
일상적인 재배포·재시작·로그 확인 명령은 [NAS 운영 명령어](deploy.md)를 사용한다. 이 문서는 최초 설치·설정·장애 분석용 상세 참고다.
> v1.12.11은 Google 로그인/계정 연동, 통계 콘텐츠 세션 원장, CSP 관찰 표와 허용 후보 표시를 포함한다. 운영 배포 전 `068_csp_violation_aggregates.sql`, `069_media_catalog.sql`, `070_post_import_jobs.sql`, `071_users_google_oauth.sql`, `072_analytics_content_sessions.sql`을 순서대로 적용하고 미디어 카탈로그는 reconcile 완료 후 `MEDIA_CATALOG_MODE=database` 고정 순서를 지킨다. 게시물 Export·Import·일반 업로드는 디스크·임시 디렉터리 스트리밍을 사용하므로 NAS 앱 컨테이너의 임시 저장 공간과 `public/uploads/exports`, `public/uploads/_import-jobs` 쓰기 권한을 확인한다.
> 로컬 기준 v1.12.11에서 `npm run lint`, `npm test`, `npm run build` 검증을 통과했다.
## 빌드 유형
| 유형 | 명령어 | 용도 |
|------|--------|------|
| 개발 | `npm run dev` | 로컬 테스트, 개발 서버 |
| 프로덕션 | `npm run build` | NAS 배포, 운영 서버 |
| 단위 검증 | `npm run verify` | JavaScript 문법 점검 + Node 단위 테스트 + 프로덕션 빌드 |
| CI 전체 검증 | `npm run verify:ci` | 문법 + 단위 + Playwright API/E2E + 프로덕션 빌드 |
> `npm run dev`는 프로젝트 전용 실행 스크립트를 통해 개발 서버, Admin, Tailwind Viewer 링크만 요약 출력한다.
## v1.12.11 배포 전 검증
Playwright API 테스트도 브라우저 바이너리는 필요하지 않지만 `verify:ci`에는 Chromium E2E가 포함된다. 최초 1회 의존성과 Chromium을 설치하고 Docker daemon이 실행 중인지 확인한다.
```bash
npm install
npx playwright install chromium
npm run test:unit
npm run test:api
npm run test:e2e
npm run verify:ci
```
Linux CI 이미지에서 브라우저 시스템 의존성도 함께 준비해야 하면 `npx playwright install --with-deps chromium`을 사용한다. API/E2E 명령은 `docker-compose.test.yml`의 `sori_studio_test` DB와 임시 업로드 루트만 사용하고 종료 시 제거한다. 테스트 중 Export worker와 추천 사이트 파비콘 cron은 비활성화된다.
### 운영 마이그레이션과 미디어 카탈로그 전환
운영 PostgreSQL 볼륨은 Compose 프로젝트명 변화에 영향받지 않도록 `POSTGRES_VOLUME_NAME` 또는 기본값 `soristudio_sori-studio-postgres`로 실제 이름을 고정하고, 외부 볼륨(`external: true`)으로 선언한다. UGREEN Docker 앱에서 프로젝트 이름이 달라지면 `sori-studio_sori-studio-postgres`처럼 새 빈 볼륨이 붙고 PostgreSQL init 스크립트가 다시 실행될 수 있으므로, 배포 전후 아래 명령으로 DB 컨테이너가 기존 운영 볼륨을 물고 있는지 확인한다.
```bash
sudo docker inspect sori-studio-db --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
sudo docker volume ls | grep -i sori
```
운영 볼륨이 이미 존재하지 않는 완전 신규 설치에서는 먼저 Docker 앱에서 `soristudio_sori-studio-postgres` 이름의 빈 PostgreSQL 볼륨을 만들거나 `sudo docker volume create soristudio_sori-studio-postgres`를 실행한 뒤 배포한다.
```bash
sudo sh scripts/migrate-production-db.sh status
sudo sh scripts/migrate-production-db.sh migrate
sudo sh scripts/migrate-production-db.sh status
```
- `068_csp_violation_aggregates.sql`은 CSP 보고 집계 테이블을 만든다.
- `069_media_catalog.sql`은 미디어 카탈로그·참조·상태 계약을 만든다. 최초 상태는 `ready=false`이므로 마이그레이션 직후 곧바로 `MEDIA_CATALOG_MODE=database`만 켠 채 운영하지 않는다. 반드시 reconcile을 먼저 끝낸다.
- `070_post_import_jobs.sql`은 게시물 Import 작업/항목 상태 테이블을 만든다.
- `071_users_google_oauth.sql`은 회원 Google 로그인·계정 연동용 식별자 컬럼과 고유 인덱스를 만든다.
- `072_analytics_content_sessions.sql`은 콘텐츠 단위 통계 세션 원장을 만들고 실시간 세션에 현재 콘텐츠 세션 해시를 추가한다.
- `/docker-entrypoint-initdb.d` SQL 로그가 운영 DB 재기동 때 다시 보이면 기존 볼륨이 아니라 빈 새 볼륨이 붙은 것이므로 즉시 컨테이너를 중지하고 기존 볼륨명을 확인한다.
- 배포 직후(또는 `filesystem`) 상태에서 관리자 인증 후 `POST /admin/api/media/reconcile`을 1회 실행한다. 이 API는 파일을 삭제하거나 이동하지 않고 발견 파일 upsert, 참조 백필, 사라진 파일 `missing` 표시만 수행한다.
- `media_catalog_state`의 `ready=true`, 파일시스템/카탈로그 `count`와 `bytes` 일치, `staging`·`error` 행 0을 확인한다.
- 확인이 끝나면 운영 env를 `MEDIA_CATALOG_MODE=database`로 고정하고 재기동한다. 개발 환경도 동일하게 `database`로 고정한다. 준비 미완료·불일치·DB 조회 오류가 발생하면 애플리케이션은 자동으로 파일시스템 목록으로 fallback한다.
- 관리자 화면의 임시 전환 버튼·런타임 모드 덮어쓰기 API는 제거했다. 모드 변경은 환경 변수만 사용한다.
- `npm run media:reconcile:dev`는 loopback 비운영 DB 전용 안전장치가 있으므로 개발 DB 확인에만 사용한다. 운영에서는 관리자 API를 사용한다.
- 개발 DB에서는 124개·77,019,754 bytes, 개수·용량 차이 0으로 reconcile을 확인했다. 운영 볼륨은 운영 수치로 별도 확인해야 한다.
### CSP 관찰
v1.11.0은 `base-uri`, `object-src`, `frame-ancestors`만 강제한다. 광고 코드, 사용자 사이트 코드, 외부 이미지·미디어·iframe에 영향을 줄 수 있는 출처 정책은 계속 Report-Only이므로 기존 광고와 사용자 코드 실행을 차단하지 않는다. 전체 HTML 문서 `/pages/**`는 강제 CSP에서 제외하고 Report-Only 보고만 유지한다.
배포 후 관리자 진단 API `/admin/api/security/csp-violations` 또는 `csp_violation_aggregates`의 `policy_version='compat-v1'`, `effective_directive`, `blocked_origin`, `document_path`, `user_agent_family`, `violation_count`, `last_seen_at`을 관찰한다. URL 쿼리·fragment·차단 자산 경로와 User-Agent 원문은 저장되지 않는다. `empty`, `about:`, 브라우저 확장 프로그램 scheme은 실제 외부 origin 허용 후보가 아니라 브라우저·확장 프로그램·빈 iframe 계열 신호로 본다. `허용 후보`는 지시어별 외부 origin을 빠르게 묶어 보여주는 보조 정보일 뿐이므로, 반복 위반이 광고·사이트 코드의 정상 출처인지 확인한 뒤에만 강제 정책 확대를 별도 배포한다.
v1.12.21은 `최근 관찰 항목 누적` 표기로 집계 의미를 바로잡는다. 이는 최근 30일 발생 건수가 아니라 최근 보고된 행의 전체 누적값이다. `empty`·`unknown`만으로 iframe 보고의 원인을 확정하지 않는다. 현재 정책은 HTTPS 출처를 폭넓게 허용하므로 후보 부재를 정상 출처 조사 완료로 해석하지 않는다. 이번 변경은 관리자 UI·설명 개선으로 DB 마이그레이션이나 CSP 강제 정책 변경은 없다.
---
## 로컬 개발
### 필수 조건
- 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:43081 사용
# 개발 서버 실행 (127.0.0.1:43080)
npm run dev
```
### 로컬 개발 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;'
```
> 지난 버전 마이그레이션을 아직 적용하지 않았다면 `062_site_settings_display_counts.sql`부터 `069_media_catalog.sql`까지 순차 적용되어야 한다. 앱 재기동 전에 운영 DB 마이그레이션을 먼저 완료한다. 일반 배포에서는 `npm run db:migrate:prod` 또는 NAS 컨테이너 내부 마이그레이션 명령이 이 순서를 자동 처리한다.
### 확인 주소
- 개발 서버: http://127.0.0.1:43080
- 관리자: http://127.0.0.1:43080/admin
- Tailwind Viewer: http://127.0.0.1:43080/_tailwind/
### 로컬 DB 확인 방법
로컬 개발 DB는 PostgreSQL이며 호스트에서는 `127.0.0.1:43081`로 접근한다. 접속 정보는 Git에 포함하지 않는 `.env.development` 값을 사용한다.
| 항목 | 값 |
|------|----|
| Host | `127.0.0.1` |
| Port | `43081` |
| Database | `.env.development`의 `POSTGRES_DB` |
| User | `.env.development`의 `POSTGRES_USER` |
| Password | `.env.development`의 `POSTGRES_PASSWORD` |
터미널에서 바로 확인할 때는 컨테이너 내부 `psql`을 사용한다.
```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 count(*) AS posts_count FROM posts;'
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 배포
> 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=43080, DB_PORT=43081 입력
# 운영 DB에 owner/admin이 없으면 /admin/login에서 ADMIN_EMAIL/ADMIN_PASSWORD로 최초 owner 계정이 생성됨
# MEMBER_SESSION_SECRET은 ADMIN_PASSWORD와 다른 긴 난수 문자열로 반드시 입력
# Google 로그인을 사용하려면 GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET,
# GOOGLE_OAUTH_REDIRECT_URI=https://sori.studio/api/auth/google/callback 입력
# AdSense 통계를 대시보드에 연동하려면 GOOGLE_ADSENSE_CLIENT_ID, GOOGLE_ADSENSE_CLIENT_SECRET,
# GOOGLE_ADSENSE_REFRESH_TOKEN, GOOGLE_ADSENSE_ACCOUNT_NAME, GOOGLE_ADSENSE_CURRENCY_CODE를 추가로 입력
# Docker 네트워크 대역이 NAS 기존 컨테이너와 겹치면 DOCKER_SUBNET을 다른 사설 대역으로 변경
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용
# Docker CLI 빌드 및 실행
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
# UGREEN NAS Docker 앱처럼 --env-file 옵션을 넣기 어려운 GUI 배포에서는
# 이미지를 사전 빌드한 뒤 docker-compose.yaml과 .env.production을 같은 프로젝트 위치에서 사용한다.
# 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다.
# 운영 DB 마이그레이션 상태 확인
sudo sh scripts/migrate-production-db.sh status
# schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음)
sudo sh scripts/migrate-production-db.sh baseline
# 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행
sudo sh scripts/migrate-production-db.sh migrate
```
### Compose 프로젝트명과 네트워크 일치 (v1.12.26)
운영 기본 프로젝트명은 `sori-studio`다. `docker-compose.build.yml`의 최상위 `name`은 `${COMPOSE_PROJECT_NAME:-sori-studio}`를 사용하고 `.env.production`에도 `COMPOSE_PROJECT_NAME=sori-studio`를 설정한다. CLI `-p`가 환경 변수보다 우선하므로 아래 NAS 명령은 기존 운영 프로젝트를 명시한다. DB 볼륨 이름은 프로젝트명과 독립적으로 기존 `POSTGRES_VOLUME_NAME`/외부 볼륨 설정을 유지한다.
`Pool overlaps`와 `soristudio_sori-studio-network` 생성 실패가 함께 나오고 기존 `sori-studio_sori-studio-network`가 같은 대역을 쓰고 있다면 프로젝트명 불일치다. 기존 네트워크 삭제나 다른 대역 변경 전에 프로젝트명을 맞춘다. 실행용 `docker-compose.yaml`과 빌드용 `docker-compose.build.yml`을 구분하고 CLI는 `-f`로 목적에 맞는 파일을 선택한다. 운영 마이그레이션·썸네일 백필 스크립트는 실행용 yaml을 명시한다.
이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다.
```bash
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --no-build --pull never
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production ps
```
### 운영 업데이트 (Docker 앱: 로컬 빌드 후 재배포, v1.12.27)
제공된 NAS 로그에서는 Docker 앱 재배포가 소스 빌드 없이 `sori-studio` 원격 pull을 시도했고, pull 오류 뒤 컨테이너 재생성을 표시했다. 기존 `git pull → 앱 재배포`만으로 새 코드를 빌드할 수 있다는 안내는 이 환경에 적용하지 않는다. 현재 경로는 빌드와 앱 실행을 분리한다.
1. 프로젝트의 실제 소스 폴더에서 다음 명령으로 최신 소스를 받고 로컬 이미지를 만든다. 빌드 스크립트는 기존 컨테이너·네트워크·DB를 재생성하지 않고 `docker compose build sori-studio`만 실행한다. `.env.production`이 없으면 `ENV_FILE`을 명시해야 하며, 빌드 실패 시 재배포로 진행하지 않는다.
처음 한 번은 NAS 앱이 만들었던 비추적 `docker-compose.yaml`을 백업한다. 기존 파일이 Git 관리 중이면 이동하지 않는다. 이는 새로 추적하는 동일 이름 파일 때문에 `git pull`이 중단되는 것을 피하기 위한 절차다.
```bash
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
mv docker-compose.yaml "docker-compose.yaml.backup.$(date +%Y%m%d-%H%M%S)"
fi
git pull
sudo sh scripts/build-production-image.sh
```
이후에는 백업 분기 없이 `git pull`과 빌드 스크립트를 실행한다.
2. Docker 앱 → `프로젝트` → 기존 `sori-studio` → `Compose 구성`에서 저장소의 **`docker-compose.yaml` 내용**을 반영한다. 처음 한 번 GUI가 관리하는 구성에 적용하고 이후 같은 프로젝트를 사용한다. `docker-compose.yaml`은 앱이 기준으로 사용하는 독립 실행용 파일이며 빌드용 `.yml` 뒤에 병합하는 override가 아니다. 빌드 폴더와 같은 위치에서 상대 업로드·마이그레이션 경로를 사용해야 한다. `.env.production`과 기존 DB 볼륨을 유지한다.
3. 이 구성은 앱 서비스의 `build`를 제거하고 `image: sori-studio:latest`, `pull_policy: never`를 사용한다. NAS에 미리 만든 로컬 이미지만 실행한다. 이미지가 없으면 실패하며 Docker Hub에서 해당 앱 이미지를 찾지 않는다. DB 이미지는 기존 `postgres:16-alpine`을 사용한다.
4. `재배포`를 실행한다. 이미지 다시 다운로드·업데이트 같은 옵션이 있다면 앱 로컬 이미지에 대해 해제한다. GUI가 명시적인 pull을 강제하면 Compose 정책보다 우선할 수 있으므로, 같은 `pull access denied`가 재발할 때는 실제 재배포 옵션과 사용 중인 Compose 사본을 확인해야 한다. 현재 GUI의 옵션 제공 여부·정확한 명칭은 확인되지 않았다.
5. 이미지를 먼저 빌드했으므로 앱 재배포 자체가 짧게 끝나는 것은 정상이다. 필요하면 실행 중인 앱 이미지 ID와 빌드한 이미지 ID를 비교해 새 이미지 적용을 확인한다.
```bash
sudo docker image inspect sori-studio:latest --format '{{.Id}}'
sudo docker inspect sori-studio --format '{{.Image}}'
```
위 두 값이 같아야 방금 빌드한 이미지로 실행 중이다. GUI가 로컬 이미지 배포를 강제 pull로 막는 경우의 CLI 보조 실행은 다음과 같다.
```bash
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --pull never
```
이후 업데이트도 `git pull → 이미지 빌드 스크립트 → 앱 재배포` 순서다. 앱만으로 모든 과정을 수행하려면 별도 이미지 빌드·저장소 배포 자동화가 필요하며 현재 구성에는 포함하지 않는다. v1.12.24~27에는 새 DB 마이그레이션 파일이 없으므로 이 버전 범위만 업데이트할 때는 DB 마이그레이션 실행이 필요하지 않다.
근거: [Compose 이미지 빌드](https://docs.docker.com/reference/cli/docker/compose/build/), [Compose pull 정책](https://docs.docker.com/reference/compose-file/services/#pull_policy). NAS 앱의 실제 옵션·새 이미지 반영은 운영 화면에서 추가 확인해야 한다.
### 운영 업데이트 (CLI 보조 경로)
기본 Compose로 빌드와 배포를 한 번에 CLI에서 처리하려면 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
```bash
# 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정)
cd /volume1/docker/projects/apps/sori.studio
# 원격 저장소 최신 코드 받기
git pull
# DB 스키마 변경이 포함된 배포면 미적용 SQL만 적용 (npm 없이 실행 가능)
sudo sh scripts/migrate-production-db.sh status
sudo sh scripts/migrate-production-db.sh migrate
# 앱 이미지 재빌드 후 컨테이너 재기동
sudo docker compose -p sori-studio -f docker-compose.build.yml --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에서 위 명령을 실행하면 된다.
- 제공된 UGREEN 재배포 로그에서는 소스 빌드가 실행되지 않았다. Docker 앱에는 로컬 이미지를 사전 빌드한 뒤 `docker-compose.yaml`로 재배포하는 절차를 사용한다. 기본 `docker-compose.build.yml`은 CLI 빌드·배포용으로 유지한다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
```bash
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --no-build
```
### UGREEN NAS Docker 앱 Buildx 확인
UGREEN NAS Docker 앱에서 `Docker Compose requires buildx plugin to be installed` 경고가 뜨면 NAS의 Docker CLI 플러그인 경로에 Buildx가 설치되지 않은 상태다. Buildx는 프로젝트별 파일이 아니라 NAS Docker 환경 단위 플러그인이므로 한 번 설치한 뒤 기본 `docker-compose.build.yml`로 빌드한다.
```bash
sudo docker buildx version
```
명령이 `unknown command`를 출력하면 Debian 12 x86_64 기준으로 Buildx 바이너리를 CLI 플러그인 경로에 설치한다.
```bash
sudo mkdir -p /usr/local/lib/docker/cli-plugins
sudo curl -L -o /usr/local/lib/docker/cli-plugins/docker-buildx \
https://github.com/docker/buildx/releases/download/v0.19.3/buildx-v0.19.3.linux-amd64
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
sudo docker buildx version
```
`unknown command`가 계속 나오면 Docker가 다른 플러그인 경로를 보는 구성이므로 공용 경로에 링크한다.
```bash
sudo mkdir -p /usr/lib/docker/cli-plugins
sudo ln -sf /usr/local/lib/docker/cli-plugins/docker-buildx /usr/lib/docker/cli-plugins/docker-buildx
sudo docker buildx version
```
코드 변경 없이 `.env.production`만 수정했다면 GUI 프로젝트 재배포 또는 CLI `up -d --no-build`를 사용한다. `pull_policy: build`가 있으므로 CLI에서 재빌드를 생략하려면 `--no-build`를 명시한다.
### Google 로그인 설정
Google 계정 로그인과 계정 연동을 사용하려면 Google Cloud Console의 OAuth 클라이언트에 아래 리다이렉트 URI를 등록한다.
- 로컬 개발: `http://127.0.0.1:43080/api/auth/google/callback`
- 운영: `https://sori.studio/api/auth/google/callback`
운영 `.env.production`에는 아래 값을 추가한다. AdSense와 같은 OAuth 클라이언트를 써도 되지만, 장기적으로는 로그인용 클라이언트를 별도로 두는 편이 관리하기 쉽다.
```bash
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...
GOOGLE_OAUTH_REDIRECT_URI=https://sori.studio/api/auth/google/callback
```
관리자 Google 로그인은 자동으로 열리지 않는다. 먼저 기존 관리자 이메일/비밀번호로 로그인한 뒤 `/settings`에서 Google 계정을 연결해야 하며, 연결된 `owner` 또는 `admin` 계정만 `/admin/login`의 Google 로그인을 사용할 수 있다.
### AdSense refresh token 발급
AdSense Management API를 관리자 대시보드에서 조회하려면 OAuth 클라이언트 ID/시크릿 외에 1회 발급한 refresh token이 필요하다.
```bash
# credentials.json 경로를 직접 지정해서 실행
GOOGLE_ADSENSE_CREDENTIALS_PATH=/path/to/credentials.json npm run adsense:token
# 또는 env 파일에 GOOGLE_ADSENSE_CLIENT_ID / GOOGLE_ADSENSE_CLIENT_SECRET을 넣고 실행
ENV_FILE=.env.production npm run adsense:token
```
스크립트가 표시하는 `http://127.0.0.1:43082/oauth2callback` 주소는 Google Cloud OAuth 클라이언트의 승인된 리다이렉트 URI에 등록되어 있어야 한다. 브라우저 승인 후 터미널에 출력되는 `GOOGLE_ADSENSE_REFRESH_TOKEN`과 `GOOGLE_ADSENSE_ACCOUNT_NAME`을 `.env.production`에 저장한다. 광고 코드의 `data-ad-client="ca-pub-..."` 값은 API 계정 이름으로 쓸 때 `ca-`를 빼고 `accounts/pub-...` 형식으로 적는다. 대시보드 표시 통화는 필요하면 `GOOGLE_ADSENSE_CURRENCY_CODE=KRW`처럼 지정한다. 이 통화 설정은 보고서 수익 지표에만 적용되며, 잔고는 Google이 지급 계정 통화로 내려주는 값을 그대로 표시한다.
### 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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
```
### 포트
- 로컬 개발: 43080
- NAS Docker 외부: 43080
- 컨테이너 내부: 3000
- PostgreSQL 호스트 로컬 전용: `127.0.0.1:43081`
- Docker 내부 네트워크 기본값: `10.250.50.0/24`
- HTTPS: 3001 (SSL 설정 시)
### 배포 후 확인
- `docker ps`에서 `sori-studio`, `sori-studio-db`가 `Up` 상태인지 확인한다.
- 공개 사이트와 `/admin` 로그인이 정상 동작하는지 확인한다.
- 스키마 변경 배포였다면 `sudo sh scripts/migrate-production-db.sh status`로 미적용 마이그레이션이 없는지 확인한다.
- `/sitemap.xml`, `/robots.txt`, `/rss.xml`이 정상 응답하는지 확인한다. Google Search Console에는 `https://도메인/sitemap.xml`을 제출한다.
---
## 데이터베이스
- 로컬 개발: `.env.development`의 `DATABASE_URL`
- NAS 운영: `.env.production`의 `DATABASE_URL`
- 로컬 개발 예시: `postgres://sori_studio:비밀번호@127.0.0.1:43081/sori_studio`
- NAS Docker 예시: `postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio`
- `DB_BIND_ADDRESS` 기본값은 `127.0.0.1`이다. 특별한 관리망 요구가 없다면 `0.0.0.0`으로 변경하지 않는다.
- `.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`에서 우선 읽는다.
### 마이그레이션
- 스키마 변경은 `db/migrations/`의 SQL 파일로 관리한다. 버전별 변경 내역은 `docs/update.md`를 참고한다.
- 로컬: `npm run db:migrate:dev`
- NAS 운영: `sudo sh scripts/migrate-production-db.sh status` → `migrate`
- 각 SQL 파일 실행과 `schema_migrations` 이력 기록은 같은 `psql --single-transaction`에서 처리되며 오류 시 함께 롤백된다.
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `migrate`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `sudo sh scripts/migrate-production-db.sh baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `sudo sh scripts/migrate-production-db.sh baseline 031`.
### 이메일 인증(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개까지 처리한다. 운영 배포 전 `070_post_import_jobs.sql` 적용과 `public/uploads/_import-jobs` 쓰기 권한을 확인한다.
- 관리 도구: CloudBeaver 등으로 추후 연결 가능하게 설계
- NAS Docker 배포 시 PostgreSQL 초기 스키마는 `db/migrations/`의 SQL로 생성
- 로컬 개발 Docker Compose 실행 시 `ENV_FILE=.env.development`와 `--env-file .env.development`를 함께 사용
- 운영 환경 파일은 프로젝트 루트의 `.env.production`을 우선 사용한다. 없으면 `.env`를 읽고, 둘 다 없으면 실행 중인 `sori-studio-db` 컨테이너의 `POSTGRES_DB`·`POSTGRES_USER`를 사용한다.
### 통계 데이터 보관 정책
- `site_analytics_daily`, `post_analytics_daily`: 사이트 전체 방문자와 게시물별 조회수의 누적 원본이므로 자동 삭제하지 않는다.
- `analytics_traffic_daily`: 유입원·디바이스·키워드 축약 집계 원본이므로 자동 삭제하지 않는다.
- `analytics_daily_visitors`: 일별 중복 방문 제거용 해시만 담으므로 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
- `analytics_content_sessions`: 콘텐츠 세션별 중복 방지와 최근 참여 지표 보정용 원장이며, 방문자 해시와 동일하게 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
- `analytics_active_sessions`: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다. `ip_address`는 현재 접속자 화면에서 익명 방문자 묶음과 보조 식별 정보로 표시한다.
- 관리자 대시보드 차트는 최대 365일 범위를 조회하며, 차트 범위를 넘는 집계도 누적 통계 원본으로 보관한다.
- 관리자 통계 집계 시작일은 `ANALYTICS_COLLECTED_SINCE` 환경 변수로 설정한다. 기본값은 `2026-06-01`이다.
- 관리자 세션으로 공개 글·페이지를 확인하는 요청은 통계 수집에서 제외된다. 운영자가 직접 방문자처럼 검증해야 할 때는 관리자 로그아웃 또는 별도 브라우저 프로필을 사용한다.
### 개발/운영 DB 분리 검증 절차
검증 전제는 실제 비밀번호나 전체 `DATABASE_URL`을 화면 공유, 문서, 커밋 메시지에 노출하지 않는 것이다. 확인할 때는 호스트, 포트, DB 이름, 파일명만 대조한다.
1. `.env.development` 확인.
```bash
# 로컬 개발 DB는 호스트 기준 127.0.0.1:43081을 사용해야 한다.
# DATABASE_URL 전체 값은 공유하지 않는다.
rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|DB_PORT)=" .env.development
```
기준:
- `DATABASE_URL` 호스트가 `127.0.0.1`
- `DATABASE_URL` 포트가 `43081`
- `DB_PORT=43081`
- 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
2. `.env.production` 확인.
```bash
# 운영 파일은 Git에 올리지 않는 운영 전용 파일이다.
# 값이 없으면 NAS 배포 전 작성해야 한다.
test -f .env.production && rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|APP_PORT|DB_PORT)=" .env.production
```
기준:
- NAS Docker 내부 실행 기준이면 `DATABASE_URL` 호스트가 `sori-studio-db`
- NAS 외부 DB를 별도 인스턴스로 쓰는 경우에도 로컬 개발 DB(`127.0.0.1:43081`)를 가리키지 않음
- `APP_PORT=43080`
- `DB_PORT=43081`
- `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:43080`
- 관리자 API 요청에서 `127.0.0.1:43081` 연결 오류가 발생하지 않음
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.build.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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production logs sori-studio --tail 200
sudo docker compose -p sori-studio -f docker-compose.build.yml --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. 로그를 고친 뒤에는 `sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다.
### NAS·호스트에서 `db/migrations` 권한
`docker-compose.build.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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production restart sori-studio-db
```
여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
---
## 운영 보조 명령
배포와 직접 관련 없지만 운영 DB·업로드 볼륨에 대해 **필요할 때만** 실행하는 명령이다.
| 명령 | 용도 |
|------|------|
| `npm run images:backfill-post-thumbnails` | 로컬 Node/npm 환경에서 게시물 대표 이미지용 카드 썸네일(`thumbs/*-card.webp`) 일괄 생성 |
| `npm run images:backfill-post-body-thumbnails` | 로컬 Node/npm 환경에서 게시물 본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`) 일괄 생성 |
| `npm run images:backfill-page-cover-thumbnails` | 로컬 Node/npm 환경에서 홈 Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`) 일괄 생성 |
| `sudo sh scripts/backfill-post-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 카드 썸네일 일괄 생성 |
| `sudo sh scripts/backfill-post-body-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 본문 표시용 WebP 이미지 일괄 생성 |
| `sudo sh scripts/backfill-page-cover-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 Pages 커버 썸네일 일괄 생성 |
| `npm run navigation:refresh-recommended-favicons` | 추천 사이트 파비콘 캐시를 수동으로 전체 갱신 |
본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`)는 게시물 저장 시 생성된다. 기존 게시물의 과거 이미지는 로컬에서는 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서는 `sudo sh scripts/backfill-post-body-thumbnails-production.sh`로 일괄 생성할 수 있다. production 래퍼는 `docker compose up -d sori-studio` 후 실행 중인 앱 컨테이너 안의 Node로 백필 스크립트를 실행하므로 NAS 호스트에 npm이 없어도 된다.
Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`, 450×600)는 페이지 저장 시 생성된다. 기존 홈 노출 페이지의 과거 이미지는 로컬에서는 `npm run images:backfill-page-cover-thumbnails`, NAS 운영에서는 `sudo sh scripts/backfill-page-cover-thumbnails-production.sh`로 일괄 생성할 수 있다.
Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에서 Homepage가 같은 네트워크에 있으면 `http://sori-studio:3000/api/homepage-widget`을 사용한다.
---
## 업로드 파일
- 관리자 글쓰기에서 업로드한 이미지는 `/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.build.yml`의 `./public/uploads:/app/public/uploads` 볼륨으로 업로드 파일을 유지한다.
- 운영 빌드에서는 `/uploads/**` 서버 라우트가 `.output/public`이 아니라 `/app/public/uploads` 볼륨의 실제 파일을 직접 제공한다.
- `/uploads/**` 응답은 `ETag`와 `Last-Modified`를 제공한다. 고유 URL로 저장되는 원본·시스템·회원 썸네일 파일은 1년 immutable 캐시를 사용하고, 같은 URL로 재생성될 수 있는 게시물 `thumbs` 파생 파일은 1시간 캐시와 `stale-while-revalidate`를 사용한다.
- `MAX_FILE_SIZE`, `MAX_VIDEO_FILE_SIZE`, `MAX_AUDIO_FILE_SIZE`, `MAX_DOCUMENT_FILE_SIZE` 환경 변수로 관리자 미디어 업로드 파일별 최대 크기를 제한한다. 1회 업로드 최대 파일 수와 전체 용량은 관리자 Settings > POST 설정 > 업로드 제한에서 조절한다. 리버스 프록시(Nginx 등)를 쓰면 `client_max_body_size`가 앱 한도보다 작지 않은지 함께 확인한다.
- 관리자 미디어 화면은 현재 업로드 파일 시스템을 기준으로 목록, 파일명 변경, 삭제를 처리한다.
## 사용자 액션 필요 항목
- NAS SSH 접속 주소 확인.
- NAS 프로젝트 루트 경로 확정.
- 운영 DB 이름, 계정, 권한 확정.
- 운영 업로드 볼륨 경로 확정.
- 도메인 `sori.studio`의 NAS 연결 방식 확정.

View File

@@ -1,616 +1,90 @@
# 배포 가이드
# NAS 운영 명령어
> v1.12.11은 Google 로그인/계정 연동, 통계 콘텐츠 세션 원장, CSP 관찰 표와 허용 후보 표시를 포함한다. 운영 배포 전 `068_csp_violation_aggregates.sql`, `069_media_catalog.sql`, `070_post_import_jobs.sql`, `071_users_google_oauth.sql`, `072_analytics_content_sessions.sql`을 순서대로 적용하고 미디어 카탈로그는 reconcile 완료 후 `MEDIA_CATALOG_MODE=database` 고정 순서를 지킨다. 게시물 Export·Import·일반 업로드는 디스크·임시 디렉터리 스트리밍을 사용하므로 NAS 앱 컨테이너의 임시 저장 공간과 `public/uploads/exports`, `public/uploads/_import-jobs` 쓰기 권한을 확인한다.
> 로컬 기준 v1.12.11에서 `npm run lint`, `npm test`, `npm run build` 검증을 통과했다.
해당하는 항목의 코드 블록 전체를 NAS 터미널에 복사해 실행하세요. 오류가 나면 다음 단계는 실행되지 않습니다.
## 빌드 유형
| 유형 | 명령어 | 용도 |
|------|--------|------|
| 개발 | `npm run dev` | 로컬 테스트, 개발 서버 |
| 프로덕션 | `npm run build` | NAS 배포, 운영 서버 |
| 단위 검증 | `npm run verify` | JavaScript 문법 점검 + Node 단위 테스트 + 프로덕션 빌드 |
| CI 전체 검증 | `npm run verify:ci` | 문법 + 단위 + Playwright API/E2E + 프로덕션 빌드 |
> `npm run dev`는 프로젝트 전용 실행 스크립트를 통해 개발 서버, Admin, Tailwind Viewer 링크만 요약 출력한다.
## v1.12.11 배포 전 검증
Playwright API 테스트도 브라우저 바이너리는 필요하지 않지만 `verify:ci`에는 Chromium E2E가 포함된다. 최초 1회 의존성과 Chromium을 설치하고 Docker daemon이 실행 중인지 확인한다.
```bash
npm install
npx playwright install chromium
npm run test:unit
npm run test:api
npm run test:e2e
npm run verify:ci
```
Linux CI 이미지에서 브라우저 시스템 의존성도 함께 준비해야 하면 `npx playwright install --with-deps chromium`을 사용한다. API/E2E 명령은 `docker-compose.test.yml`의 `sori_studio_test` DB와 임시 업로드 루트만 사용하고 종료 시 제거한다. 테스트 중 Export worker와 추천 사이트 파비콘 cron은 비활성화된다.
### 운영 마이그레이션과 미디어 카탈로그 전환
운영 PostgreSQL 볼륨은 Compose 프로젝트명 변화에 영향받지 않도록 `POSTGRES_VOLUME_NAME` 또는 기본값 `soristudio_sori-studio-postgres`로 실제 이름을 고정하고, 외부 볼륨(`external: true`)으로 선언한다. UGREEN Docker 앱에서 프로젝트 이름이 달라지면 `sori-studio_sori-studio-postgres`처럼 새 빈 볼륨이 붙고 PostgreSQL init 스크립트가 다시 실행될 수 있으므로, 배포 전후 아래 명령으로 DB 컨테이너가 기존 운영 볼륨을 물고 있는지 확인한다.
```bash
sudo docker inspect sori-studio-db --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
sudo docker volume ls | grep -i sori
```
운영 볼륨이 이미 존재하지 않는 완전 신규 설치에서는 먼저 Docker 앱에서 `soristudio_sori-studio-postgres` 이름의 빈 PostgreSQL 볼륨을 만들거나 `sudo docker volume create soristudio_sori-studio-postgres`를 실행한 뒤 배포한다.
```bash
sudo sh scripts/migrate-production-db.sh status
sudo sh scripts/migrate-production-db.sh migrate
sudo sh scripts/migrate-production-db.sh status
```
- `068_csp_violation_aggregates.sql`은 CSP 보고 집계 테이블을 만든다.
- `069_media_catalog.sql`은 미디어 카탈로그·참조·상태 계약을 만든다. 최초 상태는 `ready=false`이므로 마이그레이션 직후 곧바로 `MEDIA_CATALOG_MODE=database`만 켠 채 운영하지 않는다. 반드시 reconcile을 먼저 끝낸다.
- `070_post_import_jobs.sql`은 게시물 Import 작업/항목 상태 테이블을 만든다.
- `071_users_google_oauth.sql`은 회원 Google 로그인·계정 연동용 식별자 컬럼과 고유 인덱스를 만든다.
- `072_analytics_content_sessions.sql`은 콘텐츠 단위 통계 세션 원장을 만들고 실시간 세션에 현재 콘텐츠 세션 해시를 추가한다.
- `/docker-entrypoint-initdb.d` SQL 로그가 운영 DB 재기동 때 다시 보이면 기존 볼륨이 아니라 빈 새 볼륨이 붙은 것이므로 즉시 컨테이너를 중지하고 기존 볼륨명을 확인한다.
- 배포 직후(또는 `filesystem`) 상태에서 관리자 인증 후 `POST /admin/api/media/reconcile`을 1회 실행한다. 이 API는 파일을 삭제하거나 이동하지 않고 발견 파일 upsert, 참조 백필, 사라진 파일 `missing` 표시만 수행한다.
- `media_catalog_state`의 `ready=true`, 파일시스템/카탈로그 `count`와 `bytes` 일치, `staging`·`error` 행 0을 확인한다.
- 확인이 끝나면 운영 env를 `MEDIA_CATALOG_MODE=database`로 고정하고 재기동한다. 개발 환경도 동일하게 `database`로 고정한다. 준비 미완료·불일치·DB 조회 오류가 발생하면 애플리케이션은 자동으로 파일시스템 목록으로 fallback한다.
- 관리자 화면의 임시 전환 버튼·런타임 모드 덮어쓰기 API는 제거했다. 모드 변경은 환경 변수만 사용한다.
- `npm run media:reconcile:dev`는 loopback 비운영 DB 전용 안전장치가 있으므로 개발 DB 확인에만 사용한다. 운영에서는 관리자 API를 사용한다.
- 개발 DB에서는 124개·77,019,754 bytes, 개수·용량 차이 0으로 reconcile을 확인했다. 운영 볼륨은 운영 수치로 별도 확인해야 한다.
### CSP 관찰
v1.11.0은 `base-uri`, `object-src`, `frame-ancestors`만 강제한다. 광고 코드, 사용자 사이트 코드, 외부 이미지·미디어·iframe에 영향을 줄 수 있는 출처 정책은 계속 Report-Only이므로 기존 광고와 사용자 코드 실행을 차단하지 않는다. 전체 HTML 문서 `/pages/**`는 강제 CSP에서 제외하고 Report-Only 보고만 유지한다.
배포 후 관리자 진단 API `/admin/api/security/csp-violations` 또는 `csp_violation_aggregates`의 `policy_version='compat-v1'`, `effective_directive`, `blocked_origin`, `document_path`, `user_agent_family`, `violation_count`, `last_seen_at`을 관찰한다. URL 쿼리·fragment·차단 자산 경로와 User-Agent 원문은 저장되지 않는다. `empty`, `about:`, 브라우저 확장 프로그램 scheme은 실제 외부 origin 허용 후보가 아니라 브라우저·확장 프로그램·빈 iframe 계열 신호로 본다. `허용 후보`는 지시어별 외부 origin을 빠르게 묶어 보여주는 보조 정보일 뿐이므로, 반복 위반이 광고·사이트 코드의 정상 출처인지 확인한 뒤에만 강제 정책 확대를 별도 배포한다.
v1.12.21은 `최근 관찰 항목 누적` 표기로 집계 의미를 바로잡는다. 이는 최근 30일 발생 건수가 아니라 최근 보고된 행의 전체 누적값이다. `empty`·`unknown`만으로 iframe 보고의 원인을 확정하지 않는다. 현재 정책은 HTTPS 출처를 폭넓게 허용하므로 후보 부재를 정상 출처 조사 완료로 해석하지 않는다. 이번 변경은 관리자 UI·설명 개선으로 DB 마이그레이션이나 CSP 강제 정책 변경은 없다.
---
## 로컬 개발
### 필수 조건
- 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:43081 사용
# 개발 서버 실행 (127.0.0.1:43080)
npm run dev
```
### 로컬 개발 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;'
```
> 지난 버전 마이그레이션을 아직 적용하지 않았다면 `062_site_settings_display_counts.sql`부터 `069_media_catalog.sql`까지 순차 적용되어야 한다. 앱 재기동 전에 운영 DB 마이그레이션을 먼저 완료한다. 일반 배포에서는 `npm run db:migrate:prod` 또는 NAS 컨테이너 내부 마이그레이션 명령이 이 순서를 자동 처리한다.
### 확인 주소
- 개발 서버: http://127.0.0.1:43080
- 관리자: http://127.0.0.1:43080/admin
- Tailwind Viewer: http://127.0.0.1:43080/_tailwind/
### 로컬 DB 확인 방법
로컬 개발 DB는 PostgreSQL이며 호스트에서는 `127.0.0.1:43081`로 접근한다. 접속 정보는 Git에 포함하지 않는 `.env.development` 값을 사용한다.
| 항목 | 값 |
|------|----|
| Host | `127.0.0.1` |
| Port | `43081` |
| Database | `.env.development`의 `POSTGRES_DB` |
| User | `.env.development`의 `POSTGRES_USER` |
| Password | `.env.development`의 `POSTGRES_PASSWORD` |
터미널에서 바로 확인할 때는 컨테이너 내부 `psql`을 사용한다.
```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 count(*) AS posts_count FROM posts;'
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=43080, DB_PORT=43081 입력
# 운영 DB에 owner/admin이 없으면 /admin/login에서 ADMIN_EMAIL/ADMIN_PASSWORD로 최초 owner 계정이 생성됨
# MEMBER_SESSION_SECRET은 ADMIN_PASSWORD와 다른 긴 난수 문자열로 반드시 입력
# Google 로그인을 사용하려면 GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET,
# GOOGLE_OAUTH_REDIRECT_URI=https://sori.studio/api/auth/google/callback 입력
# AdSense 통계를 대시보드에 연동하려면 GOOGLE_ADSENSE_CLIENT_ID, GOOGLE_ADSENSE_CLIENT_SECRET,
# GOOGLE_ADSENSE_REFRESH_TOKEN, GOOGLE_ADSENSE_ACCOUNT_NAME, GOOGLE_ADSENSE_CURRENCY_CODE를 추가로 입력
# Docker 네트워크 대역이 NAS 기존 컨테이너와 겹치면 DOCKER_SUBNET을 다른 사설 대역으로 변경
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용
# Docker CLI 빌드 및 실행
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
# UGREEN NAS Docker 앱처럼 --env-file 옵션을 넣기 어려운 GUI 배포에서는
# 이미지를 사전 빌드한 뒤 docker-compose.yaml과 .env.production을 같은 프로젝트 위치에서 사용한다.
# 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다.
# 운영 DB 마이그레이션 상태 확인
sudo sh scripts/migrate-production-db.sh status
# schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음)
sudo sh scripts/migrate-production-db.sh baseline
# 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행
sudo sh scripts/migrate-production-db.sh migrate
```
### Compose 프로젝트명과 네트워크 일치 (v1.12.26)
운영 기본 프로젝트명은 `sori-studio`다. `docker-compose.build.yml`의 최상위 `name`은 `${COMPOSE_PROJECT_NAME:-sori-studio}`를 사용하고 `.env.production`에도 `COMPOSE_PROJECT_NAME=sori-studio`를 설정한다. CLI `-p`가 환경 변수보다 우선하므로 아래 NAS 명령은 기존 운영 프로젝트를 명시한다. DB 볼륨 이름은 프로젝트명과 독립적으로 기존 `POSTGRES_VOLUME_NAME`/외부 볼륨 설정을 유지한다.
`Pool overlaps`와 `soristudio_sori-studio-network` 생성 실패가 함께 나오고 기존 `sori-studio_sori-studio-network`가 같은 대역을 쓰고 있다면 프로젝트명 불일치다. 기존 네트워크 삭제나 다른 대역 변경 전에 프로젝트명을 맞춘다. 실행용 `docker-compose.yaml`과 빌드용 `docker-compose.build.yml`을 구분하고 CLI는 `-f`로 목적에 맞는 파일을 선택한다. 운영 마이그레이션·썸네일 백필 스크립트는 실행용 yaml을 명시한다.
이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다.
```bash
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --no-build --pull never
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production ps
```
### 운영 업데이트 (Docker 앱: 로컬 빌드 후 재배포, v1.12.27)
제공된 NAS 로그에서는 Docker 앱 재배포가 소스 빌드 없이 `sori-studio` 원격 pull을 시도했고, pull 오류 뒤 컨테이너 재생성을 표시했다. 기존 `git pull → 앱 재배포`만으로 새 코드를 빌드할 수 있다는 안내는 이 환경에 적용하지 않는다. 현재 경로는 빌드와 앱 실행을 분리한다.
1. 프로젝트의 실제 소스 폴더에서 다음 명령으로 최신 소스를 받고 로컬 이미지를 만든다. 빌드 스크립트는 기존 컨테이너·네트워크·DB를 재생성하지 않고 `docker compose build sori-studio`만 실행한다. `.env.production`이 없으면 `ENV_FILE`을 명시해야 하며, 빌드 실패 시 재배포로 진행하지 않는다.
처음 한 번은 NAS 앱이 만들었던 비추적 `docker-compose.yaml`을 백업한다. 기존 파일이 Git 관리 중이면 이동하지 않는다. 이는 새로 추적하는 동일 이름 파일 때문에 `git pull`이 중단되는 것을 피하기 위한 절차다.
## 일반 재배포 — 코드 업데이트
```bash
cd /volume1/docker/projects/apps/sori.studio &&
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
mv docker-compose.yaml "docker-compose.yaml.backup.$(date +%Y%m%d-%H%M%S)"
fi
git pull
fi &&
git pull --ff-only &&
sudo sh scripts/build-production-image.sh &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --pull never &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production ps
```
## DB 변경이 포함됐다고 안내받은 재배포
```bash
cd /volume1/docker/projects/apps/sori.studio &&
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
mv docker-compose.yaml "docker-compose.yaml.backup.$(date +%Y%m%d-%H%M%S)"
fi &&
git pull --ff-only &&
sudo sh scripts/migrate-production-db.sh status &&
sudo sh scripts/migrate-production-db.sh migrate &&
sudo sh scripts/build-production-image.sh &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --pull never &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production ps
```
## Docker 앱에서 재배포할 때 — 이미지 빌드까지
```bash
cd /volume1/docker/projects/apps/sori.studio &&
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
mv docker-compose.yaml "docker-compose.yaml.backup.$(date +%Y%m%d-%H%M%S)"
fi &&
git pull --ff-only &&
sudo sh scripts/build-production-image.sh
```
이후에는 백업 분기 없이 `git pull`과 빌드 스크립트를 실행한다.
빌드 완료 후: **Docker → 프로젝트 → sori-studio → Compose 구성 → 재배포**. 이미지 다운로드·업데이트 옵션이 있다면 해제하세요.
2. Docker 앱 → `프로젝트` → 기존 `sori-studio` → `Compose 구성`에서 저장소의 **`docker-compose.yaml` 내용**을 반영한다. 처음 한 번 GUI가 관리하는 구성에 적용하고 이후 같은 프로젝트를 사용한다. `docker-compose.yaml`은 앱이 기준으로 사용하는 독립 실행용 파일이며 빌드용 `.yml` 뒤에 병합하는 override가 아니다. 빌드 폴더와 같은 위치에서 상대 업로드·마이그레이션 경로를 사용해야 한다. `.env.production`과 기존 DB 볼륨을 유지한다.
3. 이 구성은 앱 서비스의 `build`를 제거하고 `image: sori-studio:latest`, `pull_policy: never`를 사용한다. NAS에 미리 만든 로컬 이미지만 실행한다. 이미지가 없으면 실패하며 Docker Hub에서 해당 앱 이미지를 찾지 않는다. DB 이미지는 기존 `postgres:16-alpine`을 사용한다.
4. `재배포`를 실행한다. 이미지 다시 다운로드·업데이트 같은 옵션이 있다면 앱 로컬 이미지에 대해 해제한다. GUI가 명시적인 pull을 강제하면 Compose 정책보다 우선할 수 있으므로, 같은 `pull access denied`가 재발할 때는 실제 재배포 옵션과 사용 중인 Compose 사본을 확인해야 한다. 현재 GUI의 옵션 제공 여부·정확한 명칭은 확인되지 않았다.
5. 이미지를 먼저 빌드했으므로 앱 재배포 자체가 짧게 끝나는 것은 정상이다. 필요하면 실행 중인 앱 이미지 ID와 빌드한 이미지 ID를 비교해 새 이미지 적용을 확인한다.
## 코드 변경 없이 앱만 다시 시작
```bash
sudo docker image inspect sori-studio:latest --format '{{.Id}}'
sudo docker inspect sori-studio --format '{{.Image}}'
cd /volume1/docker/projects/apps/sori.studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production restart sori-studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production ps
```
위 두 값이 같아야 방금 빌드한 이미지로 실행 중이다. GUI가 로컬 이미지 배포를 강제 pull로 막는 경우의 CLI 보조 실행은 다음과 같다.
## 앱 환경설정(.env.production) 변경 후 반영
```bash
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --pull never
cd /volume1/docker/projects/apps/sori.studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --no-deps --force-recreate --pull never sori-studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production ps
```
이후 업데이트도 `git pull → 이미지 빌드 스크립트 → 앱 재배포` 순서다. 앱만으로 모든 과정을 수행하려면 별도 이미지 빌드·저장소 배포 자동화가 필요하며 현재 구성에는 포함하지 않는다. v1.12.24~27에는 새 DB 마이그레이션 파일이 없으므로 이 버전 범위만 업데이트할 때는 DB 마이그레이션 실행이 필요하지 않다.
근거: [Compose 이미지 빌드](https://docs.docker.com/reference/cli/docker/compose/build/), [Compose pull 정책](https://docs.docker.com/reference/compose-file/services/#pull_policy). NAS 앱의 실제 옵션·새 이미지 반영은 운영 화면에서 추가 확인해야 한다.
### 운영 업데이트 (CLI 보조 경로)
기본 Compose로 빌드와 배포를 한 번에 CLI에서 처리하려면 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
## 현재 실행 상태 확인
```bash
# 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정)
cd /volume1/docker/projects/apps/sori.studio
cd /volume1/docker/projects/apps/sori.studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production ps
```
# 원격 저장소 최신 코드 받기
git pull
## 앱 오류 로그 확인
# DB 스키마 변경이 포함된 배포면 미적용 SQL만 적용 (npm 없이 실행 가능)
```bash
cd /volume1/docker/projects/apps/sori.studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production logs --tail 100 sori-studio
```
## DB 오류 로그 확인
```bash
cd /volume1/docker/projects/apps/sori.studio &&
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production logs --tail 100 sori-studio-db
```
## DB 마이그레이션 상태 확인
```bash
cd /volume1/docker/projects/apps/sori.studio &&
sudo sh scripts/migrate-production-db.sh status
sudo sh scripts/migrate-production-db.sh migrate
# 앱 이미지 재빌드 후 컨테이너 재기동
sudo docker compose -p sori-studio -f docker-compose.build.yml --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에서 위 명령을 실행하면 된다.
- 제공된 UGREEN 재배포 로그에서는 소스 빌드가 실행되지 않았다. Docker 앱에는 로컬 이미지를 사전 빌드한 뒤 `docker-compose.yaml`로 재배포하는 절차를 사용한다. 기본 `docker-compose.build.yml`은 CLI 빌드·배포용으로 유지한다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
```bash
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --no-build
```
### UGREEN NAS Docker 앱 Buildx 확인
UGREEN NAS Docker 앱에서 `Docker Compose requires buildx plugin to be installed` 경고가 뜨면 NAS의 Docker CLI 플러그인 경로에 Buildx가 설치되지 않은 상태다. Buildx는 프로젝트별 파일이 아니라 NAS Docker 환경 단위 플러그인이므로 한 번 설치한 뒤 기본 `docker-compose.build.yml`로 빌드한다.
```bash
sudo docker buildx version
```
명령이 `unknown command`를 출력하면 Debian 12 x86_64 기준으로 Buildx 바이너리를 CLI 플러그인 경로에 설치한다.
```bash
sudo mkdir -p /usr/local/lib/docker/cli-plugins
sudo curl -L -o /usr/local/lib/docker/cli-plugins/docker-buildx \
https://github.com/docker/buildx/releases/download/v0.19.3/buildx-v0.19.3.linux-amd64
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
sudo docker buildx version
```
`unknown command`가 계속 나오면 Docker가 다른 플러그인 경로를 보는 구성이므로 공용 경로에 링크한다.
```bash
sudo mkdir -p /usr/lib/docker/cli-plugins
sudo ln -sf /usr/local/lib/docker/cli-plugins/docker-buildx /usr/lib/docker/cli-plugins/docker-buildx
sudo docker buildx version
```
코드 변경 없이 `.env.production`만 수정했다면 GUI 프로젝트 재배포 또는 CLI `up -d --no-build`를 사용한다. `pull_policy: build`가 있으므로 CLI에서 재빌드를 생략하려면 `--no-build`를 명시한다.
### Google 로그인 설정
Google 계정 로그인과 계정 연동을 사용하려면 Google Cloud Console의 OAuth 클라이언트에 아래 리다이렉트 URI를 등록한다.
- 로컬 개발: `http://127.0.0.1:43080/api/auth/google/callback`
- 운영: `https://sori.studio/api/auth/google/callback`
운영 `.env.production`에는 아래 값을 추가한다. AdSense와 같은 OAuth 클라이언트를 써도 되지만, 장기적으로는 로그인용 클라이언트를 별도로 두는 편이 관리하기 쉽다.
```bash
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...
GOOGLE_OAUTH_REDIRECT_URI=https://sori.studio/api/auth/google/callback
```
관리자 Google 로그인은 자동으로 열리지 않는다. 먼저 기존 관리자 이메일/비밀번호로 로그인한 뒤 `/settings`에서 Google 계정을 연결해야 하며, 연결된 `owner` 또는 `admin` 계정만 `/admin/login`의 Google 로그인을 사용할 수 있다.
### AdSense refresh token 발급
AdSense Management API를 관리자 대시보드에서 조회하려면 OAuth 클라이언트 ID/시크릿 외에 1회 발급한 refresh token이 필요하다.
```bash
# credentials.json 경로를 직접 지정해서 실행
GOOGLE_ADSENSE_CREDENTIALS_PATH=/path/to/credentials.json npm run adsense:token
# 또는 env 파일에 GOOGLE_ADSENSE_CLIENT_ID / GOOGLE_ADSENSE_CLIENT_SECRET을 넣고 실행
ENV_FILE=.env.production npm run adsense:token
```
스크립트가 표시하는 `http://127.0.0.1:43082/oauth2callback` 주소는 Google Cloud OAuth 클라이언트의 승인된 리다이렉트 URI에 등록되어 있어야 한다. 브라우저 승인 후 터미널에 출력되는 `GOOGLE_ADSENSE_REFRESH_TOKEN`과 `GOOGLE_ADSENSE_ACCOUNT_NAME`을 `.env.production`에 저장한다. 광고 코드의 `data-ad-client="ca-pub-..."` 값은 API 계정 이름으로 쓸 때 `ca-`를 빼고 `accounts/pub-...` 형식으로 적는다. 대시보드 표시 통화는 필요하면 `GOOGLE_ADSENSE_CURRENCY_CODE=KRW`처럼 지정한다. 이 통화 설정은 보고서 수익 지표에만 적용되며, 잔고는 Google이 지급 계정 통화로 내려주는 값을 그대로 표시한다.
### 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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
```
### 포트
- 로컬 개발: 43080
- NAS Docker 외부: 43080
- 컨테이너 내부: 3000
- PostgreSQL 호스트 로컬 전용: `127.0.0.1:43081`
- Docker 내부 네트워크 기본값: `10.250.50.0/24`
- HTTPS: 3001 (SSL 설정 시)
### 배포 후 확인
- `docker ps`에서 `sori-studio`, `sori-studio-db`가 `Up` 상태인지 확인한다.
- 공개 사이트와 `/admin` 로그인이 정상 동작하는지 확인한다.
- 스키마 변경 배포였다면 `sudo sh scripts/migrate-production-db.sh status`로 미적용 마이그레이션이 없는지 확인한다.
- `/sitemap.xml`, `/robots.txt`, `/rss.xml`이 정상 응답하는지 확인한다. Google Search Console에는 `https://도메인/sitemap.xml`을 제출한다.
---
## 데이터베이스
- 로컬 개발: `.env.development`의 `DATABASE_URL`
- NAS 운영: `.env.production`의 `DATABASE_URL`
- 로컬 개발 예시: `postgres://sori_studio:비밀번호@127.0.0.1:43081/sori_studio`
- NAS Docker 예시: `postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio`
- `DB_BIND_ADDRESS` 기본값은 `127.0.0.1`이다. 특별한 관리망 요구가 없다면 `0.0.0.0`으로 변경하지 않는다.
- `.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`에서 우선 읽는다.
### 마이그레이션
- 스키마 변경은 `db/migrations/`의 SQL 파일로 관리한다. 버전별 변경 내역은 `docs/update.md`를 참고한다.
- 로컬: `npm run db:migrate:dev`
- NAS 운영: `sudo sh scripts/migrate-production-db.sh status` → `migrate`
- 각 SQL 파일 실행과 `schema_migrations` 이력 기록은 같은 `psql --single-transaction`에서 처리되며 오류 시 함께 롤백된다.
- `schema_migrations`가 없는 기존 운영 DB에서 `posts` 테이블이 감지되면 `migrate`는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 `sudo sh scripts/migrate-production-db.sh baseline`으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: `sudo sh scripts/migrate-production-db.sh baseline 031`.
### 이메일 인증(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개까지 처리한다. 운영 배포 전 `070_post_import_jobs.sql` 적용과 `public/uploads/_import-jobs` 쓰기 권한을 확인한다.
- 관리 도구: CloudBeaver 등으로 추후 연결 가능하게 설계
- NAS Docker 배포 시 PostgreSQL 초기 스키마는 `db/migrations/`의 SQL로 생성
- 로컬 개발 Docker Compose 실행 시 `ENV_FILE=.env.development`와 `--env-file .env.development`를 함께 사용
- 운영 환경 파일은 프로젝트 루트의 `.env.production`을 우선 사용한다. 없으면 `.env`를 읽고, 둘 다 없으면 실행 중인 `sori-studio-db` 컨테이너의 `POSTGRES_DB`·`POSTGRES_USER`를 사용한다.
### 통계 데이터 보관 정책
- `site_analytics_daily`, `post_analytics_daily`: 사이트 전체 방문자와 게시물별 조회수의 누적 원본이므로 자동 삭제하지 않는다.
- `analytics_traffic_daily`: 유입원·디바이스·키워드 축약 집계 원본이므로 자동 삭제하지 않는다.
- `analytics_daily_visitors`: 일별 중복 방문 제거용 해시만 담으므로 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
- `analytics_content_sessions`: 콘텐츠 세션별 중복 방지와 최근 참여 지표 보정용 원장이며, 방문자 해시와 동일하게 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
- `analytics_active_sessions`: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다. `ip_address`는 현재 접속자 화면에서 익명 방문자 묶음과 보조 식별 정보로 표시한다.
- 관리자 대시보드 차트는 최대 365일 범위를 조회하며, 차트 범위를 넘는 집계도 누적 통계 원본으로 보관한다.
- 관리자 통계 집계 시작일은 `ANALYTICS_COLLECTED_SINCE` 환경 변수로 설정한다. 기본값은 `2026-06-01`이다.
- 관리자 세션으로 공개 글·페이지를 확인하는 요청은 통계 수집에서 제외된다. 운영자가 직접 방문자처럼 검증해야 할 때는 관리자 로그아웃 또는 별도 브라우저 프로필을 사용한다.
### 개발/운영 DB 분리 검증 절차
검증 전제는 실제 비밀번호나 전체 `DATABASE_URL`을 화면 공유, 문서, 커밋 메시지에 노출하지 않는 것이다. 확인할 때는 호스트, 포트, DB 이름, 파일명만 대조한다.
1. `.env.development` 확인.
```bash
# 로컬 개발 DB는 호스트 기준 127.0.0.1:43081을 사용해야 한다.
# DATABASE_URL 전체 값은 공유하지 않는다.
rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|DB_PORT)=" .env.development
```
기준:
- `DATABASE_URL` 호스트가 `127.0.0.1`
- `DATABASE_URL` 포트가 `43081`
- `DB_PORT=43081`
- 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
2. `.env.production` 확인.
```bash
# 운영 파일은 Git에 올리지 않는 운영 전용 파일이다.
# 값이 없으면 NAS 배포 전 작성해야 한다.
test -f .env.production && rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|APP_PORT|DB_PORT)=" .env.production
```
기준:
- NAS Docker 내부 실행 기준이면 `DATABASE_URL` 호스트가 `sori-studio-db`
- NAS 외부 DB를 별도 인스턴스로 쓰는 경우에도 로컬 개발 DB(`127.0.0.1:43081`)를 가리키지 않음
- `APP_PORT=43080`
- `DB_PORT=43081`
- `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:43080`
- 관리자 API 요청에서 `127.0.0.1:43081` 연결 오류가 발생하지 않음
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.build.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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production logs sori-studio --tail 200
sudo docker compose -p sori-studio -f docker-compose.build.yml --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. 로그를 고친 뒤에는 `sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다.
### NAS·호스트에서 `db/migrations` 권한
`docker-compose.build.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
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production restart sori-studio-db
```
여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
---
## 운영 보조 명령
배포와 직접 관련 없지만 운영 DB·업로드 볼륨에 대해 **필요할 때만** 실행하는 명령이다.
| 명령 | 용도 |
|------|------|
| `npm run images:backfill-post-thumbnails` | 로컬 Node/npm 환경에서 게시물 대표 이미지용 카드 썸네일(`thumbs/*-card.webp`) 일괄 생성 |
| `npm run images:backfill-post-body-thumbnails` | 로컬 Node/npm 환경에서 게시물 본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`) 일괄 생성 |
| `npm run images:backfill-page-cover-thumbnails` | 로컬 Node/npm 환경에서 홈 Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`) 일괄 생성 |
| `sudo sh scripts/backfill-post-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 카드 썸네일 일괄 생성 |
| `sudo sh scripts/backfill-post-body-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 본문 표시용 WebP 이미지 일괄 생성 |
| `sudo sh scripts/backfill-page-cover-thumbnails-production.sh` | npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 Pages 커버 썸네일 일괄 생성 |
| `npm run navigation:refresh-recommended-favicons` | 추천 사이트 파비콘 캐시를 수동으로 전체 갱신 |
본문 표시용 WebP 이미지(`thumbs/*-body-1600.webp`, `thumbs/*-body-720.webp`, `thumbs/*-body-2560.webp`)는 게시물 저장 시 생성된다. 기존 게시물의 과거 이미지는 로컬에서는 `npm run images:backfill-post-body-thumbnails`, NAS 운영에서는 `sudo sh scripts/backfill-post-body-thumbnails-production.sh`로 일괄 생성할 수 있다. production 래퍼는 `docker compose up -d sori-studio` 후 실행 중인 앱 컨테이너 안의 Node로 백필 스크립트를 실행하므로 NAS 호스트에 npm이 없어도 된다.
Pages 진열장 커버용 WebP 이미지(`thumbs/*-page-cover.webp`, 450×600)는 페이지 저장 시 생성된다. 기존 홈 노출 페이지의 과거 이미지는 로컬에서는 `npm run images:backfill-page-cover-thumbnails`, NAS 운영에서는 `sudo sh scripts/backfill-page-cover-thumbnails-production.sh`로 일괄 생성할 수 있다.
Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에서 Homepage가 같은 네트워크에 있으면 `http://sori-studio:3000/api/homepage-widget`을 사용한다.
---
## 업로드 파일
- 관리자 글쓰기에서 업로드한 이미지는 `/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.build.yml`의 `./public/uploads:/app/public/uploads` 볼륨으로 업로드 파일을 유지한다.
- 운영 빌드에서는 `/uploads/**` 서버 라우트가 `.output/public`이 아니라 `/app/public/uploads` 볼륨의 실제 파일을 직접 제공한다.
- `/uploads/**` 응답은 `ETag`와 `Last-Modified`를 제공한다. 고유 URL로 저장되는 원본·시스템·회원 썸네일 파일은 1년 immutable 캐시를 사용하고, 같은 URL로 재생성될 수 있는 게시물 `thumbs` 파생 파일은 1시간 캐시와 `stale-while-revalidate`를 사용한다.
- `MAX_FILE_SIZE`, `MAX_VIDEO_FILE_SIZE`, `MAX_AUDIO_FILE_SIZE`, `MAX_DOCUMENT_FILE_SIZE` 환경 변수로 관리자 미디어 업로드 파일별 최대 크기를 제한한다. 1회 업로드 최대 파일 수와 전체 용량은 관리자 Settings > POST 설정 > 업로드 제한에서 조절한다. 리버스 프록시(Nginx 등)를 쓰면 `client_max_body_size`가 앱 한도보다 작지 않은지 함께 확인한다.
- 관리자 미디어 화면은 현재 업로드 파일 시스템을 기준으로 목록, 파일명 변경, 삭제를 처리한다.
## 사용자 액션 필요 항목
- NAS SSH 접속 주소 확인.
- NAS 프로젝트 루트 경로 확정.
- 운영 DB 이름, 계정, 권한 확정.
- 운영 업로드 볼륨 경로 확정.
- 도메인 `sori.studio`의 NAS 연결 방식 확정.
최초 설치·고급 설정·장애 원인 설명: [배포 상세 참고](deploy-reference.md).

View File

@@ -1,3 +1,7 @@
## 2026-10-21 v1.12.28 — 일상 배포 문서는 복사할 명령만 제공한다
운영자가 긴 배포 설명에서 필요한 절차를 골라야 했으므로 `docs/deploy.md`를 상황별 명령어 안내로 줄인다. 각 상황은 NAS 경로 이동부터 마지막 상태 확인까지 한 블록으로 복사하며, 실패한 단계 뒤에는 다음 명령이 실행되지 않도록 연결한다. 기존 설치·고급 설정·장애 설명은 `docs/deploy-reference.md`로 옮겨 필요할 때만 참조한다. 실행 스크립트와 Compose 동작은 변경하지 않는다.
## 2026-10-21 v1.12.27 — NAS 앱 재배포와 이미지 빌드를 분리한다
제공된 재배포 로그에는 소스 빌드가 없고 원격 `sori-studio` pull 오류와 컨테이너 재생성만 있어, Compose의 `pull_policy: build`를 지정하면 앱이 로컬 소스를 빌드할 것이라는 이전 전제를 폐기한다. 앱 재배포를 유지하려는 사용자 선호에 맞춰 명시적인 이미지 빌드 스크립트와 로컬 이미지 전용 GUI 구성을 나눈다. 사용자가 확인한 앱 기준 파일명 `docker-compose.yaml`을 실행용으로 관리하고 기존 정의는 `docker-compose.build.yml`로 옮긴다. 운영 마이그레이션·백필도 실행용 파일을 선택한다. 처음 갱신할 때 NAS의 비추적 yaml이 Git과 충돌하지 않도록 기존 파일 백업 절차를 둔다. GUI 구성은 원격 앱 pull을 금지하고, 실제 GUI가 이를 덮어쓰는지는 NAS 옵션·로그로 확인해야 한다. 기존 DB·네트워크·업로드 설정은 동일하게 유지한다.

View File

@@ -653,3 +653,5 @@
| tests/production-compose.test.js | 운영 마이그레이션·썸네일 백필 스크립트의 Compose 파일·환경 파일 전달 격리 검증 |
| docker-compose.yaml | NAS Docker 앱용 독립 실행 구성·사전 빌드한 로컬 앱 이미지 사용·앱 원격 pull 금지 |
| scripts/build-production-image.sh | 운영 환경 파일을 사용해 앱 이미지만 빌드·Docker 오류 반환·컨테이너/DB 미변경 |
| docs/deploy.md | NAS 상황별 복사·실행 명령어: 일반/DB 포함 재배포·앱 재배포 준비·재시작·환경설정·상태·로그 |
| docs/deploy-reference.md | 최초 설치·고급 환경설정·개발 환경·장애 대응 상세 참고 |

View File

@@ -1,5 +1,13 @@
# 업데이트 이력
## v1.12.28
- deploy 문서를 상황별 복사·실행 명령어 중심으로 축소.
- 일반/DB 포함 재배포·앱 재배포 준비·재시작·환경설정 반영·상태/로그 확인 구분.
- 최초 YAML 자동 백업과 실패 시 후속 명령 중단 방식 통일.
- 기존 설치·설정·장애 대응 설명을 deploy-reference 문서로 분리.
- 명령 블록 9개 셸 구문·대체 실행 순서·최초 백업·실패 중단 3경로·문서 링크 검증 진행.
## v1.12.27
- UGREEN 재배포의 원격 이미지 pull 오류를 기준으로 앱 단독 소스 빌드 안내 정정.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.12.27",
"version": "1.12.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.12.27",
"version": "1.12.28",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.12.27",
"version": "1.12.28",
"private": true,
"type": "module",
"imports": {