Files
sori.studio/docs/deploy.md

22 KiB

배포 가이드

로컬 기준 v1.5.44에서 npm run lint, npm run build 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.

빌드 유형

유형 명령어 용도
개발 npm run dev 로컬 테스트, 개발 서버
프로덕션 npm run build NAS 배포, 운영 서버
검증 npm run verify JavaScript 문법 점검 + 프로덕션 빌드

npm run dev는 프로젝트 전용 실행 스크립트를 통해 개발 서버, Admin, Tailwind Viewer 링크만 요약 출력한다.


로컬 개발

필수 조건

  • 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:43119 사용

# 개발 서버 실행 (127.0.0.1:43117)
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;'

v1.5.44 참고

  • 추가 DB 마이그레이션은 없다.
  • 관리자 사이트 설정의 블로그 제목·설명, 사이트 정보, 사이트 코드 읽기 화면만 레이아웃 변경되었다.

v1.5.43 참고

  • 추가 DB 마이그레이션은 없다.
  • 배포 후 /rss.xml, /feed.xml, /rssapplication/rss+xml로 응답하고 최근 공개 발행글을 포함하는지 확인한다.
  • 관리자 SNS 정보에서 RSS 프리셋을 사용할 경우 주소는 /rss.xml을 권장한다.

v1.5.42 참고

  • 추가 DB 마이그레이션은 없다.
  • 공개 오른쪽 사이드바 FOLLOW 영역의 직접 SVG 아이콘 정렬만 수정한다.

v1.5.41 마이그레이션

  • 049_fix_social_links_jsonb_string.sql: 기존에 JSONB 문자열로 잘못 저장된 site_settings.social_links 값을 JSONB 배열로 복구한다.
  • 적용 후 관리자 사이트 설정의 SNS 정보 저장값이 읽기 모드에서 유지되는지 확인한다.

v1.5.40 참고

  • 추가 DB 마이그레이션은 없다.
  • 관리자 사이트 설정의 SNS 정보에서 프리셋이 없는 서비스는 직접 SVG를 선택해 SVG 아이콘과 주소를 함께 저장한다.
  • https://를 생략한 SNS 주소는 저장 시 자동 보정된다.

v1.5.39 마이그레이션

  • 048_site_settings_social_links.sql: site_settingssocial_links JSONB 컬럼을 추가한다.
  • 적용 후 관리자 사이트 설정의 SNS 정보와 공개 오른쪽 사이드바 FOLLOW 노출이 정상 동작하는지 확인한다.

v1.5.38 마이그레이션

  • 047_site_settings_announcement_alignment.sql: site_settingsannouncement_alignment 컬럼을 추가한다.
  • 적용 후 관리자 사이트 설정의 어나운스 바 정렬(중앙/왼쪽)이 공개 화면에 반영되는지 확인한다.

v1.5.35 마이그레이션

  • 045_analytics_traffic_sources.sql: 방문자 유입원·디바이스·검색 키워드 일별 축약 집계 테이블과 중복 방문 제거용 컬럼을 추가한다.
  • 적용 이후부터 수집되는 페이지뷰에 대해서만 유입 정보가 쌓인다. 과거 방문 데이터는 소급 집계하지 않는다.
  • 검색 키워드는 검색엔진이 referrer query를 전달한 경우에만 표시된다.

v1.5.34 마이그레이션

  • 044_site_settings_custom_code.sql: site_settingsads_txt, custom_head_code, custom_footer_code 컬럼을 추가한다.
  • 배포 후 /ads.txt와 공개 페이지 HTML head/body 하단 코드 삽입이 정상 동작하는지 확인한다.

확인 주소

로컬 DB 확인 방법

로컬 개발 DB는 PostgreSQL이며 호스트에서는 127.0.0.1:43119로 접근한다. 접속 정보는 Git에 포함하지 않는 .env.development 값을 사용한다.

항목
Host 127.0.0.1
Port 43119
Database .env.developmentPOSTGRES_DB
User .env.developmentPOSTGRES_USER
Password .env.developmentPOSTGRES_PASSWORD

터미널에서 바로 확인할 때는 컨테이너 내부 psql을 사용한다.

# DB 준비 상태 확인
docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio

# 게시물 개수 확인
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'

# psql 콘솔 접속
docker exec -it sori-studio-db psql -U sori_studio -d sori_studio

GUI로 확인할 때는 DBeaver, TablePlus, DataGrip, CloudBeaver 같은 PostgreSQL 클라이언트에서 위 접속 정보를 입력한다. phpMyAdmin은 MySQL/MariaDB용 도구라 이 프로젝트의 PostgreSQL DB 확인 용도로는 사용하지 않는다.


UGREEN NAS Docker 배포

Dockerfile과 docker-compose 설정은 초안이며 NAS 운영 환경에서는 아직 검증 전이다.

SSH 접속

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=43118 입력
# 운영 DB에 owner/admin이 없으면 /admin/login에서 ADMIN_EMAIL/ADMIN_PASSWORD로 최초 owner 계정이 생성됨
# MEMBER_SESSION_SECRET은 ADMIN_PASSWORD와 다른 긴 난수 문자열로 반드시 입력
# Docker 네트워크 대역이 NAS 기존 컨테이너와 겹치면 DOCKER_SUBNET을 다른 사설 대역으로 변경
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용

# Docker 빌드 및 실행
docker compose --env-file .env.production up -d --build

# 운영 DB 마이그레이션 상태 확인
sh scripts/migrate-production-db.sh status

# schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음)
sh scripts/migrate-production-db.sh baseline

# 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행
sh scripts/migrate-production-db.sh migrate

운영 업데이트 (코드 반영)

이미 한 번 올려 둔 NAS에서 새 커밋을 받아 반영할 때는 보통 아래 순서를 따른다. 최초 설치 절차와 달리 git clone은 하지 않는다.

# 프로젝트 루트로 이동 (경로는 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만으로 충분하다.

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

포트

  • 로컬 개발: 43117
  • NAS Docker 외부: 43118
  • 컨테이너 내부: 3000
  • PostgreSQL 외부: 43119
  • Docker 내부 네트워크 기본값: 10.250.50.0/24
  • HTTPS: 3001 (SSL 설정 시)

데이터베이스

  • 로컬 개발: .env.developmentDATABASE_URL
  • NAS 운영: .env.productionDATABASE_URL
  • 로컬 개발 예시: postgres://sori_studio:비밀번호@127.0.0.1:43119/sori_studio
  • NAS Docker 예시: postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio
  • .env.example에는 실제 비밀번호나 개인 이메일을 기록하지 않음
  • 개발/운영 DB 비밀번호와 관리자 비밀번호는 서로 다른 랜덤 값을 사용
  • ADMIN_EMAIL/ADMIN_PASSWORD는 운영 DB에 owner/admin이 없는 최초 관리자 생성에만 사용한다. 같은 이메일의 일반 회원이 이미 있으면 owner로 승격하고 비밀번호를 ADMIN_PASSWORD 기준으로 갱신한다. 첫 owner 계정이 DB에 생성된 뒤에는 관리자 로그인도 DB의 bcrypt 비밀번호를 기준으로 검증한다.
  • 회원 세션 서명용 MEMBER_SESSION_SECRET은 관리자 비밀번호와 분리된 긴 난수 문자열을 사용
  • 개발 DB와 운영 DB는 반드시 별도 인스턴스 또는 별도 데이터베이스로 분리
  • 운영 DB는 로컬 개발 서버에서 직접 연결하지 않음
  • 운영 환경에서 DATABASE_URL이 없으면 샘플 콘텐츠로 대체하지 않고 서버 오류로 실패
  • Docker 운영 컨테이너는 .env.production의 서버 환경 변수를 런타임 process.env에서 우선 읽는다.

이메일 인증(Resend, 선택)

회원가입(일반)·비밀번호 찾기에 이메일 OTP를 쓰려면 npm run db:migrate:dev018_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_KEYRESEND_FROM_EMAIL이 비어 있으면 기존처럼 이메일 OTP 없이 최초 관리자 가입·일반 가입(OTP 생략)이 동작한다.

게시물 Export 설정(선택)

변수 설명
POST_EXPORT_MAX_FILE_SIZE_BYTES 게시물 Export 분할 ZIP 목표 최대 용량. 기본값은 500MB이며 관리자 설정 화면 요청값이 있으면 그 값을 우선 사용한다.
  • 게시물 Import는 관리자 설정의 Import 패널에서 Export ZIP 파일을 업로드해 실행한다. 1회 업로드 파일은 300MB 이하, Markdown 게시물은 최대 1000개까지 처리한다.

  • 관리 도구: CloudBeaver 등으로 추후 연결 가능하게 설계

  • NAS Docker 배포 시 PostgreSQL 초기 스키마는 db/migrations/의 SQL로 생성

  • 로컬 개발 Docker Compose 실행 시 ENV_FILE=.env.development--env-file .env.development를 함께 사용

  • 로컬 개발 DB 마이그레이션은 npm run db:migrate:dev로 실행

  • NAS 운영 DB 마이그레이션은 NAS 호스트에 npm이 없어도 실행할 수 있도록 sh scripts/migrate-production-db.sh status로 적용 상태를 확인하고, sh scripts/migrate-production-db.sh migrate로 미적용 파일만 실행한다.

  • 운영 환경 파일은 프로젝트 루트의 .env.production을 우선 사용한다. 없으면 .env를 읽고, 둘 다 없으면 실행 중인 sori-studio-db 컨테이너의 POSTGRES_DB·POSTGRES_USER를 사용한다.

  • schema_migrations가 없는 기존 운영 DB에서 posts 테이블이 감지되면 migrate는 001부터 자동 실행하지 않는다. 현재 코드 기준 최신 DB라면 최초 1회 sh scripts/migrate-production-db.sh baseline으로 기존 파일을 적용 완료로 기록한다. 특정 번호까지만 기록하려면 예: sh scripts/migrate-production-db.sh baseline 031.

  • 네비게이션 계층(parent_id, is_folder)은 017_navigation_hierarchy.sql 적용 후 저장 API가 정상 동작한다(미적용 시 INSERT 컬럼 불일치).

  • 회원 마지막 로그인 표시(previous_last_seen_at, previous_last_seen_ip)는 021_add_member_previous_login.sql 적용 후 정상 동작한다.

  • 사이트 로고와 파비콘 저장(logo_url, favicon_url)은 022_add_site_logo_urls.sql 적용 후 정상 동작한다.

통계 데이터 보관 정책

  • site_analytics_daily, post_analytics_daily: 사이트 전체 방문자와 게시물별 조회수의 누적 원본이므로 자동 삭제하지 않는다.
  • analytics_traffic_daily: 유입원·디바이스·키워드 축약 집계 원본이므로 자동 삭제하지 않는다.
  • analytics_daily_visitors: 일별 중복 방문 제거용 해시만 담으므로 32일 초과 행은 통계 수집·관리자 조회 흐름에서 주기적으로 삭제한다.
  • analytics_active_sessions: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다.
  • 관리자 대시보드 차트는 최대 365일 범위를 조회하며, 차트 범위를 넘는 집계도 누적 통계 원본으로 보관한다.

개발/운영 DB 분리 검증 절차

검증 전제는 실제 비밀번호나 전체 DATABASE_URL을 화면 공유, 문서, 커밋 메시지에 노출하지 않는 것이다. 확인할 때는 호스트, 포트, DB 이름, 파일명만 대조한다.

  1. .env.development 확인.
# 로컬 개발 DB는 호스트 기준 127.0.0.1:43119를 사용해야 한다.
# DATABASE_URL 전체 값은 공유하지 않는다.
rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|DB_PORT)=" .env.development

기준:

  • DATABASE_URL 호스트가 127.0.0.1
  • DATABASE_URL 포트가 43119
  • DB_PORT=43119
  • 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
  1. .env.production 확인.
# 운영 파일은 Git에 올리지 않는 운영 전용 파일이다.
# 값이 없으면 NAS 배포 전 작성해야 한다.
test -f .env.production && rg -n "^(DATABASE_URL|POSTGRES_DB|POSTGRES_USER|APP_PORT)=" .env.production

기준:

  • NAS Docker 내부 실행 기준이면 DATABASE_URL 호스트가 sori-studio-db
  • NAS 외부 DB를 별도 인스턴스로 쓰는 경우에도 로컬 개발 DB(127.0.0.1:43119)를 가리키지 않음
  • APP_PORT=43118
  • MEMBER_SESSION_SECRET이 비어 있지 않고 ADMIN_PASSWORD와 다름
  • .env.development와 DB 비밀번호, 관리자 비밀번호가 서로 다름
  1. 로컬 개발 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 계정
  1. 로컬 개발 서버 연결 확인.
npm run dev

기준:

  • 출력 주소가 http://127.0.0.1:43117
  • 관리자 API 요청에서 127.0.0.1:43119 연결 오류가 발생하지 않음
  1. 커밋 전 민감 정보 확인.
git status --short
git diff -- . ':!package-lock.json'

기준:

  • .env.development, .env.production이 변경 목록에 포함되지 않음
  • 문서와 코드 diff에 실제 DB 비밀번호, 관리자 비밀번호, 운영 접속 주소가 포함되지 않음

컨테이너가 Restarting일 때

Error response from daemon: Container … is restarting, wait until the container is running프로세스가 곧바로 종료되어 restart: unless-stopped가 반복 시도하는 상태다. 원인은 로그에 나온다.

  1. 어느 서비스인지 확인 (docker-compose.yml 기준 이름은 sori-studio, sori-studio-db).
docker ps -a --filter "name=sori-studio"
  1. 해당 컨테이너 로그 (가장 중요).
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
  1. 자주 나오는 원인
  • 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 권한 절차를 확인한다.
  1. 로그를 고친 뒤에는 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 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.

업로드 파일

  • 관리자 글쓰기에서 업로드한 이미지는 /uploads/posts/YYYY/MM/ URL로 제공한다.
  • 로컬 개발에서는 실제 파일이 public/uploads/posts/YYYY/MM/ 아래 저장된다.
  • 게시물 Export ZIP 산출물은 public/uploads/exports/YYYY/MM/{jobId}/ 아래 생성되며, 관리자 다운로드 API를 통해 내려받는다.
  • 완료·실패한 게시물 Export 작업을 관리자 화면에서 삭제하면 연결된 ZIP 파일도 함께 삭제된다.
  • public/uploads/는 Git에 포함하지 않는다.
  • NAS 운영에서는 docker-compose.yml./public/uploads:/app/public/uploads 볼륨으로 업로드 파일을 유지한다.
  • 운영 빌드에서는 /uploads/** 서버 라우트가 .output/public이 아니라 /app/public/uploads 볼륨의 실제 파일을 직접 제공한다.
  • MAX_FILE_SIZE, MAX_VIDEO_FILE_SIZE, MAX_AUDIO_FILE_SIZE, MAX_DOCUMENT_FILE_SIZE 환경 변수로 관리자 미디어 업로드 최대 크기를 제한한다. 리버스 프록시(Nginx 등)를 쓰면 client_max_body_size가 앱 한도보다 작지 않은지 함께 확인한다.
  • 관리자 미디어 화면은 현재 업로드 파일 시스템을 기준으로 목록, 파일명 변경, 삭제를 처리한다.

사용자 액션 필요 항목

  • NAS SSH 접속 주소 확인.
  • NAS 프로젝트 루트 경로 확정.
  • 운영 DB 이름, 계정, 권한 확정.
  • 운영 업로드 볼륨 경로 확정.
  • 도메인 sori.studio의 NAS 연결 방식 확정.