31 KiB
배포 가이드
v1.12.10은 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.10에서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.10 배포 전 검증
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은 비활성화된다.
운영 마이그레이션과 미디어 카탈로그 전환
sh scripts/migrate-production-db.sh status
sh scripts/migrate-production-db.sh migrate
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은 콘텐츠 단위 통계 세션 원장을 만들고 실시간 세션에 현재 콘텐츠 세션 해시를 추가한다.- 배포 직후(또는
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 보고만 유지한다.
배포 후 관리자 대시보드의 CSP 관찰 표 또는 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 원문은 저장되지 않는다. 허용 후보는 지시어별 외부 origin을 빠르게 묶어 보여주는 보조 정보일 뿐이므로, 반복 위반이 광고·사이트 코드의 정상 출처인지 확인한 뒤에만 강제 정책 확대를 별도 배포한다.
로컬 개발
필수 조건
- 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 배포
Dockerfile과 docker-compose 설정은 초안이며 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 빌드 및 실행
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은 하지 않는다.
# 프로젝트 루트로 이동 (경로는 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에서 위 명령을 실행하면 된다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
docker compose --env-file .env.production up -d
코드 변경 없이 .env.production만 수정했다면 --build 없이 up -d만으로 충분하다.
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
docker compose --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로그인이 정상 동작하는지 확인한다. - 스키마 변경 배포였다면
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 운영:
sh scripts/migrate-production-db.sh status→migrate - 각 SQL 파일 실행과
schema_migrations이력 기록은 같은psql --single-transaction에서 처리되며 오류 시 함께 롤백된다. schema_migrations가 없는 기존 운영 DB에서posts테이블이 감지되면migrate는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회sh scripts/migrate-production-db.sh baseline으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: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.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로 올렸다면:
docker compose --env-file .env.production logs sori-studio --tail 200
docker compose --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권한 절차를 확인한다.
- 로그를 고친 뒤에는
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로 다음을 적용한다. 비밀번호는 바꾸지 않으며, 읽기·디렉터리 통과만 연다.
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 컨테이너만 재시작한다.
docker compose --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) 일괄 생성 |
sh scripts/backfill-post-thumbnails-production.sh |
npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 카드 썸네일 일괄 생성 |
sh scripts/backfill-post-body-thumbnails-production.sh |
npm 없는 NAS 호스트에서 앱 컨테이너 내부 Node로 본문 표시용 WebP 이미지 일괄 생성 |
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 운영에서는 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 운영에서는 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.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 연결 방식 확정.