441 lines
23 KiB
Markdown
441 lines
23 KiB
Markdown
# 배포 가이드
|
||
|
||
> 로컬 기준 v1.10.43에서 `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
|
||
|
||
### 실행 단계
|
||
|
||
```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`부터 `065_public_search_index.sql`까지 순차 적용되어야 한다. 일반 배포에서는 `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와 다른 긴 난수 문자열로 반드시 입력
|
||
# 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`은 하지 않는다.
|
||
|
||
```bash
|
||
# 프로젝트 루트로 이동 (경로는 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에서 위 명령을 실행하면 된다.
|
||
|
||
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
|
||
|
||
```bash
|
||
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`에서 예를 들어 아래처럼 바꾼 뒤 다시 실행한다.
|
||
|
||
```bash
|
||
DOCKER_SUBNET=10.250.51.0/24
|
||
docker compose --env-file .env.production up -d --build
|
||
```
|
||
|
||
### 포트
|
||
|
||
- 로컬 개발: 43080
|
||
- NAS Docker 외부: 43080
|
||
- 컨테이너 내부: 3000
|
||
- PostgreSQL 외부: 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`
|
||
- `.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`
|
||
- `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](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개까지 처리한다.
|
||
|
||
- 관리 도구: 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_active_sessions`: 현재 접속자 목록용 임시 데이터이며 90초 초과 행은 조회·수집 시 삭제한다.
|
||
- 관리자 대시보드 차트는 최대 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.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
|
||
docker compose --env-file .env.production logs sori-studio --tail 200
|
||
docker compose --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. 로그를 고친 뒤에는 `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로 다음을 적용한다. **비밀번호는 바꾸지 않으며**, 읽기·디렉터리 통과만 연다.
|
||
|
||
```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
|
||
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 연결 방식 확정.
|