공개 상단 어나운스 바와 관리자 맞춤 설정을 추가하고, 스팸 필터에서 가입 금지 닉네임을 관리·검증한다. POST 설정 읽기 모드 비활성 토글과 설정 내비 아이콘 틀을 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
16 KiB
배포 가이드
로컬 기준
npm run build,docker compose --env-file .env.production config --quiet,docker compose --env-file .env.production build sori-studio검증을 통과했다. 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;'
확인 주소
- 개발 서버: http://127.0.0.1:43117
- 관리자: http://127.0.0.1:43117/admin
- Tailwind Viewer: http://127.0.0.1:43117/_tailwind/
로컬 DB 확인 방법
로컬 개발 DB는 PostgreSQL이며 호스트에서는 127.0.0.1:43119로 접근한다. 접속 정보는 Git에 포함하지 않는 .env.development 값을 사용한다.
| 항목 | 값 |
|---|---|
| Host | 127.0.0.1 |
| Port | 43119 |
| Database | .env.development의 POSTGRES_DB |
| User | .env.development의 POSTGRES_USER |
| Password | .env.development의 POSTGRES_PASSWORD |
터미널에서 바로 확인할 때는 컨테이너 내부 psql을 사용한다.
# 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를 유지한 채 새 버전을 올릴 때 추천 글·네비 location 마이그레이션 적용
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/023_add_post_featured.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/024_navigation_recommended_location.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/025_posts_status_no_private.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/026_site_settings_show_post_updated_at.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/027_site_settings_home_cover.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/028_site_settings_announcement.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/029_site_settings_signup_blocked_usernames.sql
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.development의DATABASE_URL - NAS 운영:
.env.production의DATABASE_URL - 로컬 개발 예시:
postgres://sori_studio:비밀번호@127.0.0.1:43119/sori_studio - NAS Docker 예시:
postgres://sori_studio:비밀번호@sori-studio-db:5432/sori_studio .env.example에는 실제 비밀번호나 개인 이메일을 기록하지 않음- 개발/운영 DB 비밀번호와 관리자 비밀번호는 서로 다른 랜덤 값을 사용
ADMIN_EMAIL/ADMIN_PASSWORD는 운영 DB에 owner/admin이 없는 최초 관리자 생성에만 사용한다. 같은 이메일의 일반 회원이 이미 있으면 owner로 승격하고 비밀번호를ADMIN_PASSWORD기준으로 갱신한다. 첫 owner 계정이 DB에 생성된 뒤에는 관리자 로그인도 DB의 bcrypt 비밀번호를 기준으로 검증한다.- 회원 세션 서명용
MEMBER_SESSION_SECRET은 관리자 비밀번호와 분리된 긴 난수 문자열을 사용 - 개발 DB와 운영 DB는 반드시 별도 인스턴스 또는 별도 데이터베이스로 분리
- 운영 DB는 로컬 개발 서버에서 직접 연결하지 않음
- 운영 환경에서
DATABASE_URL이 없으면 샘플 콘텐츠로 대체하지 않고 서버 오류로 실패 - Docker 운영 컨테이너는
.env.production의 서버 환경 변수를 런타임process.env에서 우선 읽는다.
이메일 인증(Resend, 선택)
회원가입(일반)·비밀번호 찾기에 이메일 OTP를 쓰려면 npm run db:migrate:dev로 018_email_otp_challenges.sql을 적용하고, .env에 다음을 설정한다.
| 변수 | 설명 |
|---|---|
RESEND_API_KEY |
Resend 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 생략)이 동작한다.
- 관리 도구: CloudBeaver 등으로 추후 연결 가능하게 설계
- NAS Docker 배포 시 PostgreSQL 초기 스키마는
db/migrations/의 SQL로 생성 - 로컬 개발 Docker Compose 실행 시
ENV_FILE=.env.development와--env-file .env.development를 함께 사용 - 로컬 개발 DB 마이그레이션은
npm run db:migrate:dev로 실행 - 네비게이션 계층(
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적용 후 정상 동작한다.
개발/운영 DB 분리 검증 절차
검증 전제는 실제 비밀번호나 전체 DATABASE_URL을 화면 공유, 문서, 커밋 메시지에 노출하지 않는 것이다. 확인할 때는 호스트, 포트, DB 이름, 파일명만 대조한다.
.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.1DATABASE_URL포트가43119DB_PORT=43119- 운영 NAS 호스트명, 운영 IP, 운영 DB 이름이 포함되지 않음
.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=43118MEMBER_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:43117 - 관리자 API 요청에서
127.0.0.1:43119연결 오류가 발생하지 않음
- 커밋 전 민감 정보 확인.
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 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
업로드 파일
- 관리자 글쓰기에서 업로드한 이미지는
/uploads/posts/YYYY/MM/URL로 제공한다. - 로컬 개발에서는 실제 파일이
public/uploads/posts/YYYY/MM/아래 저장된다. public/uploads/는 Git에 포함하지 않는다.- NAS 운영에서는
docker-compose.yml의./public/uploads:/app/public/uploads볼륨으로 업로드 파일을 유지한다. - 운영 빌드에서는
/uploads/**서버 라우트가.output/public이 아니라/app/public/uploads볼륨의 실제 파일을 직접 제공한다. MAX_FILE_SIZE환경 변수로 관리자 이미지 업로드 최대 크기를 제한한다.- 관리자 미디어 화면은 현재 업로드 파일 시스템을 기준으로 목록, 파일명 변경, 삭제를 처리한다.
사용자 액션 필요 항목
- NAS SSH 접속 주소 확인.
- NAS 프로젝트 루트 경로 확정.
- 운영 DB 이름, 계정, 권한 확정.
- 운영 업로드 볼륨 경로 확정.
- 도메인
sori.studio의 NAS 연결 방식 확정.