41 KiB
배포 상세 참고
일상적인 재배포·재시작·로그 확인 명령은 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검증을 통과했다.
빌드 유형
| 유형 | 명령어 | 용도 |
|---|---|---|
| 개발 | 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이 실행 중인지 확인한다.
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은 비활성화된다.
운영 마이그레이션과 미디어 카탈로그 전환
migrate-production-db.sh는 미적용 SQL 파일 하나와 해당 적용 이력을 같은 트랜잭션으로 실행한다. SQL 실패 시 해당 파일의 변경과 이력을 함께 취소하고 중단하며, 이미 완료된 이전 파일은 유지한다. ENV_FILE에는 상대·절대 경로를 사용할 수 있다.
운영 PostgreSQL 볼륨은 Compose 프로젝트명 변화에 영향받지 않도록 POSTGRES_VOLUME_NAME 또는 기본값 soristudio_sori-studio-postgres로 실제 이름을 고정하고, 외부 볼륨(external: true)으로 선언한다. UGREEN Docker 앱에서 프로젝트 이름이 달라지면 sori-studio_sori-studio-postgres처럼 새 빈 볼륨이 붙고 PostgreSQL init 스크립트가 다시 실행될 수 있으므로, 배포 전후 아래 명령으로 DB 컨테이너가 기존 운영 볼륨을 물고 있는지 확인한다.
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를 실행한 뒤 배포한다.
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.dSQL 로그가 운영 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
실행 단계
# 프로젝트 클론
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 서비스만 실행한다.
# 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을 사용한다.
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 접속
ssh [NAS_IP]
프로젝트 설치
# 프로젝트 디렉토리로 이동
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을 명시한다.
이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다.
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 → 앱 재배포만으로 새 코드를 빌드할 수 있다는 안내는 이 환경에 적용하지 않는다. 현재 경로는 빌드와 앱 실행을 분리한다.
- 프로젝트의 실제 소스 폴더에서 다음 명령으로 최신 소스를 받고 로컬 이미지를 만든다. 빌드 스크립트는 기존 컨테이너·네트워크·DB를 재생성하지 않고
docker compose build sori-studio만 실행한다..env.production이 없으면ENV_FILE을 명시해야 하며, 빌드 실패 시 재배포로 진행하지 않는다.
처음 한 번은 NAS 앱이 만들었던 비추적 docker-compose.yaml을 백업한다. 기존 파일이 Git 관리 중이면 이동하지 않는다. 이는 새로 추적하는 동일 이름 파일 때문에 git pull이 중단되는 것을 피하기 위한 절차다.
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과 빌드 스크립트를 실행한다.
- Docker 앱 →
프로젝트→ 기존sori-studio→Compose 구성에서 저장소의docker-compose.yaml내용을 반영한다. 처음 한 번 GUI가 관리하는 구성에 적용하고 이후 같은 프로젝트를 사용한다.docker-compose.yaml은 앱이 기준으로 사용하는 독립 실행용 파일이며 빌드용.yml뒤에 병합하는 override가 아니다. 빌드 폴더와 같은 위치에서 상대 업로드·마이그레이션 경로를 사용해야 한다..env.production과 기존 DB 볼륨을 유지한다. - 이 구성은 앱 서비스의
build를 제거하고image: sori-studio:latest,pull_policy: never를 사용한다. NAS에 미리 만든 로컬 이미지만 실행한다. 이미지가 없으면 실패하며 Docker Hub에서 해당 앱 이미지를 찾지 않는다. DB 이미지는 기존postgres:16-alpine을 사용한다. 재배포를 실행한다. 이미지 다시 다운로드·업데이트 같은 옵션이 있다면 앱 로컬 이미지에 대해 해제한다. GUI가 명시적인 pull을 강제하면 Compose 정책보다 우선할 수 있으므로, 같은pull access denied가 재발할 때는 실제 재배포 옵션과 사용 중인 Compose 사본을 확인해야 한다. 현재 GUI의 옵션 제공 여부·정확한 명칭은 확인되지 않았다.- 이미지를 먼저 빌드했으므로 앱 재배포 자체가 짧게 끝나는 것은 정상이다. 필요하면 실행 중인 앱 이미지 ID와 빌드한 이미지 ID를 비교해 새 이미지 적용을 확인한다.
sudo docker image inspect sori-studio:latest --format '{{.Id}}'
sudo docker inspect sori-studio --format '{{.Image}}'
위 두 값이 같아야 방금 빌드한 이미지로 실행 중이다. GUI가 로컬 이미지 배포를 강제 pull로 막는 경우의 CLI 보조 실행은 다음과 같다.
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 이미지 빌드, Compose pull 정책. NAS 앱의 실제 옵션·새 이미지 반영은 운영 화면에서 추가 확인해야 한다.
운영 업데이트 (CLI 보조 경로)
기본 Compose로 빌드와 배포를 한 번에 CLI에서 처리하려면 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 git clone은 하지 않는다.
# 프로젝트 루트로 이동 (경로는 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 빌드·배포용으로 유지한다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
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로 빌드한다.
sudo docker buildx version
명령이 unknown command를 출력하면 Debian 12 x86_64 기준으로 Buildx 바이너리를 CLI 플러그인 경로에 설치한다.
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가 다른 플러그인 경로를 보는 구성이므로 공용 경로에 링크한다.
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 클라이언트를 써도 되지만, 장기적으로는 로그인용 클라이언트를 별도로 두는 편이 관리하기 쉽다.
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이 필요하다.
# 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에서 예를 들어 아래처럼 바꾼 뒤 다시 실행한다.
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 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 이름, 파일명만 대조한다.
.env.development확인.
# 로컬 개발 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.1DATABASE_URL포트가43081DB_PORT=43081- 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
.env.production확인.
# 운영 파일은 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=43080DB_PORT=43081MEMBER_SESSION_SECRET이 비어 있지 않고ADMIN_PASSWORD와 다름.env.development와 DB 비밀번호, 관리자 비밀번호가 서로 다름
- 로컬 개발 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 current_database(), current_user;'
기준:
accepting connections표시current_database가 로컬 개발 DB 이름current_user가 로컬 개발 DB 계정
- 로컬 개발 서버 연결 확인.
npm run dev
기준:
- 출력 주소가
http://127.0.0.1:43080 - 관리자 API 요청에서
127.0.0.1:43081연결 오류가 발생하지 않음
- 커밋 전 민감 정보 확인.
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가 반복 시도하는 상태다. 원인은 로그에 나온다.
- 어느 서비스인지 확인 (
docker-compose.build.yml기준 이름은sori-studio,sori-studio-db).
docker ps -a --filter "name=sori-studio"
- 해당 컨테이너 로그 (가장 중요).
docker logs sori-studio --tail 150
docker logs sori-studio-db --tail 150
Compose로 올렸다면:
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
- 자주 나오는 원인
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권한 절차를 확인한다.
- 로그를 고친 뒤에는
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로 다음을 적용한다. 비밀번호는 바꾸지 않으며, 읽기·디렉터리 통과만 연다.
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 컨테이너만 재시작한다.
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.yaml과 CLI 빌드용docker-compose.build.yml의./public/uploads:/app/public/uploads볼륨으로 업로드 파일을 유지한다. 업로드는 Docker 빌드 입력과 Nitro 정적 복사에서 제외하므로 새 이미지에 기존 파일을 넣지 않는다. 재배포 시 같은 업로드 폴더를 연결해야 하며 원본 폴더는 삭제하지 않는다. - 운영 빌드에서는
/uploads/**서버 라우트가.output/public이 아니라/app/public/uploads볼륨의 실제 파일을 직접 제공한다. /uploads/**응답은ETag와Last-Modified를 제공한다. 고유 URL로 저장되는 원본·시스템·회원 썸네일 파일은 1년 immutable 캐시를 사용하고, 같은 URL로 재생성될 수 있는 게시물thumbs파생 파일은no-cache로 재검증한다.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 연결 방식 확정.