diff --git a/.env.example b/.env.example index c60983f..e6129a6 100644 --- a/.env.example +++ b/.env.example @@ -52,5 +52,6 @@ NUXT_PUBLIC_SITE_TITLE=sori.studio # EMAIL_OTP_PEPPER= ← 선택. OTP를 DB에 해시해 저장할 때 섞는 서버 전용 비밀(긴 난문자열 권장, 예: openssl rand -hex 32). 비우면 MEMBER_SESSION_SECRET을 대신 사용. # Server +COMPOSE_PROJECT_NAME=sori-studio APP_PORT=43080 DOCKER_SUBNET=10.250.50.0/24 diff --git a/docker-compose.yml b/docker-compose.yml index c82270e..cb6a540 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +name: ${COMPOSE_PROJECT_NAME:-sori-studio} + services: sori-studio: image: sori-studio:latest diff --git a/docs/deploy.md b/docs/deploy.md index 57829f1..63e7a45 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -42,9 +42,9 @@ sudo docker volume ls | grep -i sori 운영 볼륨이 이미 존재하지 않는 완전 신규 설치에서는 먼저 Docker 앱에서 `soristudio_sori-studio-postgres` 이름의 빈 PostgreSQL 볼륨을 만들거나 `sudo docker volume create soristudio_sori-studio-postgres`를 실행한 뒤 배포한다. ```bash -sh scripts/migrate-production-db.sh status -sh scripts/migrate-production-db.sh migrate -sh scripts/migrate-production-db.sh status +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 보고 집계 테이블을 만든다. @@ -192,25 +192,52 @@ cp .env.example .env.production # Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용 # Docker CLI 빌드 및 실행 -docker compose --env-file .env.production up -d --build +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build # UGREEN NAS Docker 앱처럼 --env-file 옵션을 넣기 어려운 GUI 배포에서는 # docker-compose.yml과 .env.production을 같은 프로젝트 위치에 두고 Compose 빌드를 실행한다. # 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다. # 운영 DB 마이그레이션 상태 확인 -sh scripts/migrate-production-db.sh status +sudo sh scripts/migrate-production-db.sh status # schema_migrations 도입 전 운영 DB가 이미 최신이면 최초 1회 기준점 기록(실제 SQL 실행 없음) -sh scripts/migrate-production-db.sh baseline +sudo sh scripts/migrate-production-db.sh baseline # 이후 배포에서는 아직 적용되지 않은 SQL만 순서대로 실행 -sh scripts/migrate-production-db.sh migrate +sudo sh scripts/migrate-production-db.sh migrate ``` -### 운영 업데이트 (코드 반영) +### Compose 프로젝트명과 네트워크 일치 (v1.12.26) -이미 한 번 올려 둔 NAS에서 **새 커밋을 받아 반영**할 때는 보통 아래 순서를 따른다. 최초 설치 절차와 달리 `git clone`은 하지 않는다. +운영 기본 프로젝트명은 `sori-studio`다. `docker-compose.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.yml`과 `docker-compose.yaml`이 둘 다 있어도 `-f docker-compose.yml`로 저장소 파일을 명시한다. 운영 마이그레이션·썸네일 백필 스크립트도 같은 파일을 명시한다. + +이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다. + +```bash +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --no-build --pull never +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production ps +``` + +### 운영 업데이트 (Docker 앱 우선) + +기본 운영 경로는 터미널에서 소스만 `git pull`하고, UGREEN Docker 앱의 기존 `sori-studio` 프로젝트에서 재배포하는 방식이다. 같은 Docker 엔진을 쓰므로 GUI와 CLI 자체가 충돌하는 것은 아니며, 프로젝트 이름과 구성 파일이 다를 때 별도 네트워크 생성 충돌이 발생한다. + +1. 프로젝트의 실제 빌드 폴더에서 `git pull`한다. GUI 프로젝트의 구성 파일 경로·빌드 context가 이 폴더를 가리켜야 한다. +2. Docker 앱 → `프로젝트` → 기존 `sori-studio` → `Compose 구성`을 연다. +3. 앱이 관리하는 구성을 저장소의 최신 `docker-compose.yml`과 맞춘다. `.yaml`과 `.yml`이 함께 있는 NAS에서는 앱의 구성 파일 경로가 어느 파일인지 확인한다. 앱이 별도 사본을 저장하면 최신 `.yml` 내용을 Compose 편집기에 반영하고 저장한다. 기존 운영 `.env.production`은 유지한다. +4. 같은 화면의 `재배포`를 실행한다. `image: sori-studio:latest`, `build.context: .`, `pull_policy: build`가 있어야 소스 기반 재빌드를 요청한다. 컨테이너 시작/중지 토글은 코드 빌드를 수행하는 배포 절차와 다르다. +5. 배포 로그에서 Dockerfile의 `npm run build` 단계가 실행됐는지와 새 컨테이너의 실행 상태를 확인한다. GUI 버전별 실제 재빌드 동작은 NAS에서 확인해야 한다. Docker 앱이 빌드 정책을 무시하면 아래 CLI 경로를 사용한다. + +v1.12.24~26에는 새 DB 마이그레이션 파일이 없으므로 이 버전 범위만 업데이트할 때는 DB 마이그레이션 실행이 필요하지 않다. 이후 `db/migrations`가 변경되는 배포에서는 아래 스크립트로 미적용 SQL을 처리한다. 운영자 요청에 따라 소스 동기화 이후 앱 재배포를 우선하며 GUI에 없는 자동 Git 동기화·자동 마이그레이션을 가정하지 않는다. + +근거: [UGREEN Docker/Compose 안내](https://ai.ugreen.com/blogs/knowledge/docker-docker-compose-ugreen-nas), [Compose build 정책](https://docs.docker.com/reference/compose-file/services/#pull_policy). + +### 운영 업데이트 (CLI 보조 경로) + +Docker 앱 대신 CLI로 배포해야 할 때는 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다. ```bash # 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정) @@ -220,11 +247,11 @@ 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 +sudo sh scripts/migrate-production-db.sh status +sudo sh scripts/migrate-production-db.sh migrate # 앱 이미지 재빌드 후 컨테이너 재기동 -docker compose --env-file .env.production up -d --build +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build ``` | 단계 | 설명 | @@ -238,12 +265,12 @@ docker compose --env-file .env.production up -d --build - `.env.production`은 Git에 포함하지 않는다. `git pull`로 덮어쓰이지 않는다. 값을 바꿀 때만 파일을 직접 수정한다. - `public/uploads/` 업로드 파일은 Docker 볼륨(`./public/uploads`)에 있으므로, **이미지 파일만 추가·수정한 경우** 앱 재빌드 없이도 URL로 바로 보인다. - 로컬에서 미리 확인하려면 `npm run verify` 후 NAS에서 위 명령을 실행하면 된다. -- UGREEN Docker 앱의 재배포가 너무 빨리 끝나고 앱 빌드 로그가 보이지 않으면 기존 이미지를 재사용한 것이다. 현재 Compose는 `sori-studio:latest`와 `pull_policy: build`로 앱 이미지를 다시 빌드하도록 선언하지만, NAS Docker 앱 버전이 이 값을 무시하면 터미널에서 `docker compose --env-file .env.production up -d --build`를 실행해야 한다. +- UGREEN Docker 앱의 재배포가 너무 빨리 끝나고 앱 빌드 로그가 보이지 않으면 기존 이미지를 재사용한 것이다. 현재 Compose는 `sori-studio:latest`와 `pull_policy: build`로 앱 이미지를 다시 빌드하도록 선언하지만, NAS Docker 앱 버전이 이 값을 무시하면 터미널에서 `sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build`를 실행해야 한다. 컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등): ```bash -docker compose --env-file .env.production up -d +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --no-build ``` ### UGREEN NAS Docker 앱 Buildx 확인 @@ -272,7 +299,7 @@ sudo ln -sf /usr/local/lib/docker/cli-plugins/docker-buildx /usr/lib/docker/cli- sudo docker buildx version ``` -코드 변경 없이 `.env.production`만 수정했다면 `--build` 없이 `up -d`만으로 충분하다. +코드 변경 없이 `.env.production`만 수정했다면 GUI 프로젝트 재배포 또는 CLI `up -d --no-build`를 사용한다. `pull_policy: build`가 있으므로 CLI에서 재빌드를 생략하려면 `--no-build`를 명시한다. ### Google 로그인 설정 @@ -311,7 +338,7 @@ NAS에 Docker 컨테이너가 많이 실행 중이면 `could not find an availab ```bash DOCKER_SUBNET=10.250.51.0/24 -docker compose --env-file .env.production up -d --build +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build ``` ### 포트 @@ -327,7 +354,7 @@ docker compose --env-file .env.production up -d --build - `docker ps`에서 `sori-studio`, `sori-studio-db`가 `Up` 상태인지 확인한다. - 공개 사이트와 `/admin` 로그인이 정상 동작하는지 확인한다. -- 스키마 변경 배포였다면 `sh scripts/migrate-production-db.sh status`로 미적용 마이그레이션이 없는지 확인한다. +- 스키마 변경 배포였다면 `sudo sh scripts/migrate-production-db.sh status`로 미적용 마이그레이션이 없는지 확인한다. - `/sitemap.xml`, `/robots.txt`, `/rss.xml`이 정상 응답하는지 확인한다. Google Search Console에는 `https://도메인/sitemap.xml`을 제출한다. --- @@ -352,9 +379,9 @@ docker compose --env-file .env.production up -d --build - 스키마 변경은 `db/migrations/`의 SQL 파일로 관리한다. 버전별 변경 내역은 `docs/update.md`를 참고한다. - 로컬: `npm run db:migrate:dev` -- NAS 운영: `sh scripts/migrate-production-db.sh status` → `migrate` +- NAS 운영: `sudo 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`. +- `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, 선택) @@ -485,8 +512,8 @@ 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 +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production logs sori-studio --tail 200 +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production logs sori-studio-db --tail 200 ``` 3. **자주 나오는 원인** @@ -495,7 +522,7 @@ docker compose --env-file .env.production logs sori-studio-db --tail 200 - **`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` 상태인지 확인한다. +4. 로그를 고친 뒤에는 `sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다. ### NAS·호스트에서 `db/migrations` 권한 @@ -514,7 +541,7 @@ sudo chmod a+x . db db/migrations 그다음 DB 컨테이너만 재시작한다. ```bash -docker compose --env-file .env.production restart sori-studio-db +sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production restart sori-studio-db ``` 여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다. @@ -530,14 +557,14 @@ docker compose --env-file .env.production restart sori-studio-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 커버 썸네일 일괄 생성 | +| `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 운영에서는 `sh scripts/backfill-post-body-thumbnails-production.sh`로 일괄 생성할 수 있다. production 래퍼는 `docker compose up -d sori-studio` 후 실행 중인 앱 컨테이너 안의 Node로 백필 스크립트를 실행하므로 NAS 호스트에 npm이 없어도 된다. +본문 표시용 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 운영에서는 `sh scripts/backfill-page-cover-thumbnails-production.sh`로 일괄 생성할 수 있다. +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`을 사용한다. diff --git a/docs/history.md b/docs/history.md index 6a35323..26252fd 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,3 +1,7 @@ +## 2026-10-21 v1.12.26 — 운영 Compose 프로젝트명과 파일 선택을 일치시킨다 + +제공된 NAS 로그에서 빌드는 성공했지만 CLI가 `soristudio` 프로젝트의 네트워크를 새로 만들려다 기존 `sori-studio` 프로젝트의 동일 대역과 충돌했다. 기존 운영 네트워크를 재사용하도록 Compose 기본 이름을 환경 변수 기반 `sori-studio`로 맞추고, 배포 안내에는 프로젝트명을 명시한다. `.yaml`과 `.yml` 중복 경고를 줄이고 운영 스크립트도 같은 구성을 사용하도록 파일 경로를 명시한다. DB 외부 볼륨의 이름과 보관 방식은 유지한다. 사용자가 Docker 앱 배포를 선호하므로 소스 git pull 후 기존 프로젝트의 Compose 구성에서 재배포하는 경로를 기본 안내로 둔다. 앱 버전의 실제 재빌드 수행 여부는 배포 로그로 확인하며, CLI는 보조 경로로 구분한다. + ## 2026-10-21 v1.12.25 — 접속자 목록은 최근 페이지만 요약한다 한 방문자가 여러 탭을 열면 접속자 행 높이가 크게 늘어나므로 이름·가장 최근 확인된 페이지·나머지 고유 페이지 수·체류·IP를 한 줄로 표시한다. 상세 목록은 hover뿐 아니라 키보드와 클릭으로도 접근할 수 있게 하며, heartbeat의 마지막 확인 순서를 사용한다. 같은 경로 중복은 표시 목록에서 제거해 `외 N개`와 펼친 목록의 수를 일치시킨다. 기존 API 집계 기준은 유지하고, 자주 확인할 수입·추이·상세·댓글을 접속자 목록 위에 배치한다. diff --git a/docs/map.md b/docs/map.md index e9c491c..448fd52 100644 --- a/docs/map.md +++ b/docs/map.md @@ -637,7 +637,7 @@ | Dockerfile | NAS 운영 이미지 빌드, `.output/server` 기반 앱 런타임과 이미지 백필 스크립트 포함 | | tests/markdown-html-block.test.js | 게시물 HTML sanitizer 공격 우회·허용 HTML 회귀 테스트 | | tests/multipart-file-upload.test.js | 다중 multipart 디스크 스트리밍·파일 크기 제한 회귀 테스트 | -| docker-compose.yml | NAS 컨테이너 실행 초안, PostgreSQL 호스트 기본 loopback 바인딩 | +| docker-compose.yml | 운영 프로젝트명 환경 변수/기본 sori-studio 선언, NAS 컨테이너 실행, PostgreSQL 호스트 기본 loopback 바인딩 | | components/admin/AdminCommentList.vue | 대시보드 최신 댓글 5개 작은 목록·게시글/작성자/시각/한 줄 내용·30초 갱신·조회 오류 재시도 | | server/routes/admin/api/comments.get.js | 관리자 댓글 조회 인증·댓글 내용/게시물 제목 검색·Zod query 검증 | | server/repositories/comment-repository.js | 공개 댓글 조회·작성·좋아요와 관리자 댓글 최신순 페이지 조회 | @@ -650,3 +650,4 @@ | components/admin/AdminRealtimePages.vue | 접속자 최근 페이지 요약·화면 안 팝오버 위치·열린 페이지 링크·hover/클릭/키보드 접근 | | tests/analytics-display.test.js | 천 단위 표시·최근순 중복 제거·공개 경로 검증 | | tests/playwright/e2e/admin-dashboard.spec.js | 대시보드 쉼표·섹션 순서·한 줄 접속자·페이지 목록 접근 검증 | +| tests/production-compose.test.js | 운영 마이그레이션·썸네일 백필 스크립트의 Compose 파일·환경 파일 전달 격리 검증 | diff --git a/docs/spec.md b/docs/spec.md index d8314ce..7683156 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -1114,3 +1114,9 @@ APP_PORT=43080 - 방문자·게시물 수·조회·스크롤·유입·디바이스·차트 수치·댓글/작성 이력 건수는 `ko-KR` 천 단위 구분(예: `10,413`)으로 표시. API 숫자 형식은 유지. - 접속자 대표 페이지는 포커스 여부가 아닌 마지막 확인 시각 기준. 현재 API의 회원별·익명 IP별 묶음은 유지. 인원 표시는 반환된 목록 수와 전체 접속자 수를 구분. - 열린 페이지 팝오버는 hover 시 일시 표시, 클릭/Enter로 고정 전환. 포인터를 목록으로 이동해도 링크 접근을 유지하며, 화면 범위에 맞춰 위/아래 배치하고 긴 목록은 내부 스크롤 제공. + +## 운영 Compose 식별 (v1.12.26) + +- 기본 프로젝트명은 `COMPOSE_PROJECT_NAME` 또는 `sori-studio`. Compose 최상위 `name`으로 선언하고 운영 명령은 `-p sori-studio -f docker-compose.yml` 사용. +- 운영 마이그레이션·썸네일 백필 스크립트는 루트의 `docker-compose.yml`을 명시적으로 선택한다. 환경 파일 선택 및 DB 외부 볼륨 설정은 유지. +- 운영 업데이트 기본 경로는 Git 소스 동기화 후 UGREEN Docker 앱의 기존 `sori-studio` 프로젝트에서 Compose 구성 확인·재배포. CLI는 보조 경로이며 GUI의 별도 Compose 사본과 원본 `.yml` 차이를 확인한다. diff --git a/docs/update.md b/docs/update.md index 56d42e7..d502099 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,14 @@ # 업데이트 이력 +## v1.12.26 + +- 운영 Compose 기본 프로젝트명을 기존 sori-studio로 고정해 중복 네트워크 생성 충돌 보정. +- 마이그레이션·썸네일 백필 운영 스크립트의 docker-compose.yml 파일 지정 추가. +- 환경 변수 예시와 sudo·프로젝트명·Compose 파일 지정 기반 운영 배포 명령 정리. +- git pull 후 UGREEN 기존 프로젝트의 Compose 구성·재배포를 사용하는 GUI 우선 절차 정리. +- 운영 스크립트 Compose 파일·환경 파일 전달을 실제 Docker 대신 대체 실행기로 검증 추가. +- Compose 기본/사용자 지정 프로젝트명·기존 외부 DB 볼륨·셸 문법·JavaScript 문법·단위 테스트 280건 검증 진행. + ## v1.12.25 - 대시보드 요약·통계·차트·목록 건수에 천 단위 쉼표 표시 적용. diff --git a/package-lock.json b/package-lock.json index 26e8d75..d4838b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.12.25", + "version": "1.12.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.12.25", + "version": "1.12.26", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 4a15b8c..5176762 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.12.25", + "version": "1.12.26", "private": true, "type": "module", "imports": { diff --git a/scripts/backfill-page-cover-thumbnails-production.sh b/scripts/backfill-page-cover-thumbnails-production.sh index b42cc72..cc79e0d 100755 --- a/scripts/backfill-page-cover-thumbnails-production.sh +++ b/scripts/backfill-page-cover-thumbnails-production.sh @@ -28,11 +28,11 @@ pick_compose_env_file() { compose() { if [ -n "$COMPOSE_ENV_FILE" ]; then - docker compose --env-file "$COMPOSE_ENV_FILE" "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" return 0 fi - docker compose "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" } pick_compose_env_file diff --git a/scripts/backfill-post-body-thumbnails-production.sh b/scripts/backfill-post-body-thumbnails-production.sh index 9f52588..d2054fb 100755 --- a/scripts/backfill-post-body-thumbnails-production.sh +++ b/scripts/backfill-post-body-thumbnails-production.sh @@ -28,11 +28,11 @@ pick_compose_env_file() { compose() { if [ -n "$COMPOSE_ENV_FILE" ]; then - docker compose --env-file "$COMPOSE_ENV_FILE" "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" return 0 fi - docker compose "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" } pick_compose_env_file diff --git a/scripts/backfill-post-thumbnails-production.sh b/scripts/backfill-post-thumbnails-production.sh index 1602dc5..d2933f8 100755 --- a/scripts/backfill-post-thumbnails-production.sh +++ b/scripts/backfill-post-thumbnails-production.sh @@ -28,11 +28,11 @@ pick_compose_env_file() { compose() { if [ -n "$COMPOSE_ENV_FILE" ]; then - docker compose --env-file "$COMPOSE_ENV_FILE" "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" return 0 fi - docker compose "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" } pick_compose_env_file diff --git a/scripts/migrate-production-db.sh b/scripts/migrate-production-db.sh index 8281a5b..5e78a99 100644 --- a/scripts/migrate-production-db.sh +++ b/scripts/migrate-production-db.sh @@ -52,11 +52,11 @@ load_env_file() { compose() { if [ -n "$COMPOSE_ENV_FILE" ]; then - docker compose --env-file "$COMPOSE_ENV_FILE" "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" return 0 fi - docker compose "$@" + docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" } # 실행 중인 DB 컨테이너에서 PostgreSQL 계정 정보 읽기 diff --git a/tests/production-compose.test.js b/tests/production-compose.test.js new file mode 100644 index 0000000..35828f1 --- /dev/null +++ b/tests/production-compose.test.js @@ -0,0 +1,32 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, relative, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' + +const root = resolve('.') + +for (const script of ['migrate-production-db.sh', 'backfill-post-thumbnails-production.sh', 'backfill-post-body-thumbnails-production.sh', 'backfill-page-cover-thumbnails-production.sh']) { + test(`운영 ${script}는 명시한 Compose 파일·환경 파일만 사용한다`, () => { + const directory = mkdtempSync(join(tmpdir(), 'sori-compose-test-')) + try { + const logPath = join(directory, 'docker.log') + const envPath = join(directory, 'production.env') + writeFileSync(envPath, 'COMPOSE_PROJECT_NAME=sori-studio\n') + writeFileSync(join(directory, 'docker'), '#!/bin/sh\nprintf "<%s>" "$@" >> "$SORI_TEST_DOCKER_LOG"\nprintf "\\n" >> "$SORI_TEST_DOCKER_LOG"\n', { mode: 0o755 }) + const result = spawnSync('sh', [`scripts/${script}`, 'status'], { + cwd: root, encoding: 'utf8', + env: { ...process.env, PATH: `${directory}:${process.env.PATH}`, ENV_FILE: relative(root, envPath), SORI_TEST_DOCKER_LOG: logPath } + }) + assert.equal(result.status, 0, result.stderr) + const calls = readFileSync(logPath, 'utf8').trim().split('\n') + assert.ok(calls.length > 0) + for (const call of calls) { + assert.ok(call.startsWith(`<-f><${join(root, 'docker-compose.yml')}><--env-file><${relative(root, envPath)}>`)) + } + } finally { + rmSync(directory, { recursive: true, force: true }) + } + }) +}