v1.12.27 NAS 실행 yaml 및 명시적 이미지 빌드 분리

This commit is contained in:
2026-10-21 17:36:47 +09:00
parent 345de36bd1
commit 2d4431e698
17 changed files with 195 additions and 46 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ public/uploads/
.env .env
.env.* .env.*
!.env.example !.env.example
docker-compose.yaml.backup.*
# IDE # IDE
.idea/ .idea/

45
docker-compose.yaml Normal file
View File

@@ -0,0 +1,45 @@
name: ${COMPOSE_PROJECT_NAME:-sori-studio}
services:
sori-studio:
image: sori-studio:latest
pull_policy: never
container_name: sori-studio
env_file:
- ${ENV_FILE:-.env.production}
ports:
- "${APP_PORT:-43080}:3000"
volumes:
- ./public/uploads:/app/public/uploads
depends_on:
- sori-studio-db
networks:
- sori-studio-network
restart: unless-stopped
sori-studio-db:
image: postgres:16-alpine
container_name: sori-studio-db
env_file:
- ${ENV_FILE:-.env.production}
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-43081}:5432"
volumes:
- sori-studio-postgres:/var/lib/postgresql/data
# NAS 등: 호스트 db/migrations 가 다른 UID만 읽을 수 있으면 컨테이너에서 Permission denied → DB 재시작 루프. 프로젝트 루트에서 chmod -R a+rX db/migrations 및 상위 경로 통과 권한 확인.
- ./db/migrations:/docker-entrypoint-initdb.d:ro
networks:
- sori-studio-network
restart: unless-stopped
volumes:
sori-studio-postgres:
name: ${POSTGRES_VOLUME_NAME:-soristudio_sori-studio-postgres}
external: true
networks:
sori-studio-network:
driver: bridge
ipam:
config:
- subnet: ${DOCKER_SUBNET:-10.250.50.0/24}

View File

@@ -192,10 +192,10 @@ cp .env.example .env.production
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용 # Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용
# Docker CLI 빌드 및 실행 # Docker CLI 빌드 및 실행
sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
# UGREEN NAS Docker 앱처럼 --env-file 옵션을 넣기 어려운 GUI 배포에서는 # UGREEN NAS Docker 앱처럼 --env-file 옵션을 넣기 어려운 GUI 배포에서는
# docker-compose.yml과 .env.production을 같은 프로젝트 위치에 두고 Compose 빌드를 실행한다. # 이미지를 사전 빌드한 뒤 docker-compose.yaml과 .env.production을 같은 프로젝트 위치에서 사용한다.
# 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다. # 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다.
# 운영 DB 마이그레이션 상태 확인 # 운영 DB 마이그레이션 상태 확인
@@ -210,34 +210,58 @@ sudo sh scripts/migrate-production-db.sh migrate
### Compose 프로젝트명과 네트워크 일치 (v1.12.26) ### Compose 프로젝트명과 네트워크 일치 (v1.12.26)
운영 기본 프로젝트명은 `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`/외부 볼륨 설정을 유지한다. 운영 기본 프로젝트명은 `sori-studio`다. `docker-compose.build.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`로 저장소 파일을 명시한다. 운영 마이그레이션·썸네일 백필 스크립트도 같은 파일을 명시한다. `Pool overlaps`와 `soristudio_sori-studio-network` 생성 실패가 함께 나오고 기존 `sori-studio_sori-studio-network`가 같은 대역을 쓰고 있다면 프로젝트명 불일치다. 기존 네트워크 삭제나 다른 대역 변경 전에 프로젝트명을 맞춘다. 실행용 `docker-compose.yaml`과 빌드용 `docker-compose.build.yml`을 구분하고 CLI는 `-f`로 목적에 맞는 파일을 선택한다. 운영 마이그레이션·썸네일 백필 스크립트는 실행용 yaml을 명시한다.
이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다. 이미 빌드가 성공한 직후 네트워크 생성만 실패했다면 다음 명령으로 해당 이미지를 실행한다.
```bash ```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.build.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 sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production ps
``` ```
### 운영 업데이트 (Docker 앱 우선) ### 운영 업데이트 (Docker 앱: 로컬 빌드 후 재배포, v1.12.27)
기본 운영 경로는 터미널에서 소스만 `git pull`하고, UGREEN Docker 앱의 기존 `sori-studio` 프로젝트에서 재배포하는 방식이다. 같은 Docker 엔진을 쓰므로 GUI와 CLI 자체가 충돌하는 것은 아니며, 프로젝트 이름과 구성 파일이 다를 때 별도 네트워크 생성 충돌이 발생한다. 제공된 NAS 로그에서는 Docker 앱 재배포가 소스 빌드 없이 `sori-studio` 원격 pull을 시도했고, pull 오류 뒤 컨테이너 재생성을 표시했다. 기존 `git pull → 앱 재배포`만으로 새 코드를 빌드할 수 있다는 안내는 이 환경에 적용하지 않는다. 현재 경로는 빌드와 앱 실행을 분리한다.
1. 프로젝트의 실제 빌드 폴더에서 `git pull`한다. GUI 프로젝트의 구성 파일 경로·빌드 context가 이 폴더를 가리켜야 한다. 1. 프로젝트의 실제 소스 폴더에서 다음 명령으로 최신 소스를 받고 로컬 이미지를 만든다. 빌드 스크립트는 기존 컨테이너·네트워크·DB를 재생성하지 않고 `docker compose build sori-studio`만 실행한다. `.env.production`이 없으면 `ENV_FILE`을 명시해야 하며, 빌드 실패 시 재배포로 진행하지 않는다.
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 동기화·자동 마이그레이션을 가정하지 않는다. 처음 한 번은 NAS 앱이 만들었던 비추적 `docker-compose.yaml`을 백업한다. 기존 파일이 Git 관리 중이면 이동하지 않는다. 이는 새로 추적하는 동일 이름 파일 때문에 `git pull`이 중단되는 것을 피하기 위한 절차다.
근거: [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). ```bash
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
mv docker-compose.yaml "docker-compose.yaml.backup.$(date +%Y%m%d-%H%M%S)"
fi
git pull
sudo sh scripts/build-production-image.sh
```
이후에는 백업 분기 없이 `git pull`과 빌드 스크립트를 실행한다.
2. Docker 앱 → `프로젝트` → 기존 `sori-studio` → `Compose 구성`에서 저장소의 **`docker-compose.yaml` 내용**을 반영한다. 처음 한 번 GUI가 관리하는 구성에 적용하고 이후 같은 프로젝트를 사용한다. `docker-compose.yaml`은 앱이 기준으로 사용하는 독립 실행용 파일이며 빌드용 `.yml` 뒤에 병합하는 override가 아니다. 빌드 폴더와 같은 위치에서 상대 업로드·마이그레이션 경로를 사용해야 한다. `.env.production`과 기존 DB 볼륨을 유지한다.
3. 이 구성은 앱 서비스의 `build`를 제거하고 `image: sori-studio:latest`, `pull_policy: never`를 사용한다. NAS에 미리 만든 로컬 이미지만 실행한다. 이미지가 없으면 실패하며 Docker Hub에서 해당 앱 이미지를 찾지 않는다. DB 이미지는 기존 `postgres:16-alpine`을 사용한다.
4. `재배포`를 실행한다. 이미지 다시 다운로드·업데이트 같은 옵션이 있다면 앱 로컬 이미지에 대해 해제한다. GUI가 명시적인 pull을 강제하면 Compose 정책보다 우선할 수 있으므로, 같은 `pull access denied`가 재발할 때는 실제 재배포 옵션과 사용 중인 Compose 사본을 확인해야 한다. 현재 GUI의 옵션 제공 여부·정확한 명칭은 확인되지 않았다.
5. 이미지를 먼저 빌드했으므로 앱 재배포 자체가 짧게 끝나는 것은 정상이다. 필요하면 실행 중인 앱 이미지 ID와 빌드한 이미지 ID를 비교해 새 이미지 적용을 확인한다.
```bash
sudo docker image inspect sori-studio:latest --format '{{.Id}}'
sudo docker inspect sori-studio --format '{{.Image}}'
```
위 두 값이 같아야 방금 빌드한 이미지로 실행 중이다. GUI가 로컬 이미지 배포를 강제 pull로 막는 경우의 CLI 보조 실행은 다음과 같다.
```bash
sudo docker compose -p sori-studio -f docker-compose.yaml --env-file .env.production up -d --pull never
```
이후 업데이트도 `git pull → 이미지 빌드 스크립트 → 앱 재배포` 순서다. 앱만으로 모든 과정을 수행하려면 별도 이미지 빌드·저장소 배포 자동화가 필요하며 현재 구성에는 포함하지 않는다. v1.12.24~27에는 새 DB 마이그레이션 파일이 없으므로 이 버전 범위만 업데이트할 때는 DB 마이그레이션 실행이 필요하지 않다.
근거: [Compose 이미지 빌드](https://docs.docker.com/reference/cli/docker/compose/build/), [Compose pull 정책](https://docs.docker.com/reference/compose-file/services/#pull_policy). NAS 앱의 실제 옵션·새 이미지 반영은 운영 화면에서 추가 확인해야 한다.
### 운영 업데이트 (CLI 보조 경로) ### 운영 업데이트 (CLI 보조 경로)
Docker 앱 대신 CLI로 배포해야 할 때는 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다. 기본 Compose로 빌드와 배포를 한 번에 CLI에서 처리하려면 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
```bash ```bash
# 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정) # 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정)
@@ -251,7 +275,7 @@ sudo sh scripts/migrate-production-db.sh status
sudo sh scripts/migrate-production-db.sh migrate sudo sh scripts/migrate-production-db.sh migrate
# 앱 이미지 재빌드 후 컨테이너 재기동 # 앱 이미지 재빌드 후 컨테이너 재기동
sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
``` ```
| 단계 | 설명 | | 단계 | 설명 |
@@ -265,17 +289,17 @@ sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.product
- `.env.production`은 Git에 포함하지 않는다. `git pull`로 덮어쓰이지 않는다. 값을 바꿀 때만 파일을 직접 수정한다. - `.env.production`은 Git에 포함하지 않는다. `git pull`로 덮어쓰이지 않는다. 값을 바꿀 때만 파일을 직접 수정한다.
- `public/uploads/` 업로드 파일은 Docker 볼륨(`./public/uploads`)에 있으므로, **이미지 파일만 추가·수정한 경우** 앱 재빌드 없이도 URL로 바로 보인다. - `public/uploads/` 업로드 파일은 Docker 볼륨(`./public/uploads`)에 있으므로, **이미지 파일만 추가·수정한 경우** 앱 재빌드 없이도 URL로 바로 보인다.
- 로컬에서 미리 확인하려면 `npm run verify` 후 NAS에서 위 명령을 실행하면 된다. - 로컬에서 미리 확인하려면 `npm run verify` 후 NAS에서 위 명령을 실행하면 된다.
- 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`를 실행해야 한다. - 제공된 UGREEN 재배포 로그에서는 소스 빌드가 실행되지 않았다. Docker 앱에는 로컬 이미지를 사전 빌드한 뒤 `docker-compose.yaml`로 재배포하는 절차를 사용한다. 기본 `docker-compose.build.yml`은 CLI 빌드·배포용으로 유지한다.
컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등): 컨테이너만 재시작하고 이미지는 그대로 두려면(환경 변수만 바꾼 경우 등):
```bash ```bash
sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --no-build sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --no-build
``` ```
### UGREEN NAS Docker 앱 Buildx 확인 ### UGREEN NAS Docker 앱 Buildx 확인
UGREEN NAS Docker 앱에서 `Docker Compose requires buildx plugin to be installed` 경고가 뜨면 NAS의 Docker CLI 플러그인 경로에 Buildx가 설치되지 않은 상태다. Buildx는 프로젝트별 파일이 아니라 NAS Docker 환경 단위 플러그인이므로 한 번 설치한 뒤 기본 `docker-compose.yml`로 빌드한다. UGREEN NAS Docker 앱에서 `Docker Compose requires buildx plugin to be installed` 경고가 뜨면 NAS의 Docker CLI 플러그인 경로에 Buildx가 설치되지 않은 상태다. Buildx는 프로젝트별 파일이 아니라 NAS Docker 환경 단위 플러그인이므로 한 번 설치한 뒤 기본 `docker-compose.build.yml`로 빌드한다.
```bash ```bash
sudo docker buildx version sudo docker buildx version
@@ -338,7 +362,7 @@ NAS에 Docker 컨테이너가 많이 실행 중이면 `could not find an availab
```bash ```bash
DOCKER_SUBNET=10.250.51.0/24 DOCKER_SUBNET=10.250.51.0/24
sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d --build sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d --build
``` ```
### 포트 ### 포트
@@ -496,7 +520,7 @@ git diff -- . ':!package-lock.json'
`Error response from daemon: Container … is restarting, wait until the container is running`은 **프로세스가 곧바로 종료**되어 `restart: unless-stopped`가 반복 시도하는 상태다. 원인은 로그에 나온다. `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`). 1. **어느 서비스인지 확인** (`docker-compose.build.yml` 기준 이름은 `sori-studio`, `sori-studio-db`).
```bash ```bash
docker ps -a --filter "name=sori-studio" docker ps -a --filter "name=sori-studio"
@@ -512,8 +536,8 @@ docker logs sori-studio-db --tail 150
Compose로 올렸다면: Compose로 올렸다면:
```bash ```bash
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.build.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 sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production logs sori-studio-db --tail 200
``` ```
3. **자주 나오는 원인** 3. **자주 나오는 원인**
@@ -522,11 +546,11 @@ sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.product
- **`sori-studio-db`**: 이미 초기화된 볼륨과 다른 `POSTGRES_PASSWORD`로 다시 올린 경우, `docker-entrypoint-initdb.d` 마이그레이션 SQL 오류, 디스크/권한 문제. - **`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` 권한** 절차를 확인한다. - **`sori-studio-db` 로그에 `ls: can't open '/docker-entrypoint-initdb.d/': Permission denied`**: 아래 **NAS·호스트에서 `db/migrations` 권한** 절차를 확인한다.
4. 로그를 고친 뒤에는 `sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다. 4. 로그를 고친 뒤에는 `sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production up -d`로 다시 올리고, `docker ps`에서 `Up` 상태인지 확인한다.
### NAS·호스트에서 `db/migrations` 권한 ### 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.build.yml`은 `./db/migrations`를 Postgres 이미지의 `/docker-entrypoint-initdb.d`에 **읽기 전용**으로 붙인다. 공식 엔트리포인트는 이 디렉터리를 `ls`로 읽는데, NAS(UGREEN 등)나 SSH로 복사한 트리에서 **폴더·파일이 700/600만 허용**이거나 **상위 디렉터리에 실행(x) 비트가 없으면** 컨테이너 안 `postgres` 사용자가 경로를 통과하지 못해 `Permission denied`가 반복되고 DB 컨테이너가 재시작 루프에 들어갈 수 있다.
프로젝트 루트( `docker compose` 를 실행하는 디렉터리)에서 SSH로 다음을 적용한다. **비밀번호는 바꾸지 않으며**, 읽기·디렉터리 통과만 연다. 프로젝트 루트( `docker compose` 를 실행하는 디렉터리)에서 SSH로 다음을 적용한다. **비밀번호는 바꾸지 않으며**, 읽기·디렉터리 통과만 연다.
@@ -541,7 +565,7 @@ sudo chmod a+x . db db/migrations
그다음 DB 컨테이너만 재시작한다. 그다음 DB 컨테이너만 재시작한다.
```bash ```bash
sudo docker compose -p sori-studio -f docker-compose.yml --env-file .env.production restart sori-studio-db sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production restart sori-studio-db
``` ```
여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다. 여전히 동일하면 프로젝트가 **SMB 공유 폴더 위**에만 있지 않은지 확인한다. Docker 데몬이 네이티브 경로(ext4 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
@@ -577,7 +601,7 @@ Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에
- 게시물 Export ZIP 산출물은 `public/uploads/exports/YYYY/MM/{jobId}/` 아래 생성되며, 관리자 다운로드 API를 통해 내려받는다. - 게시물 Export ZIP 산출물은 `public/uploads/exports/YYYY/MM/{jobId}/` 아래 생성되며, 관리자 다운로드 API를 통해 내려받는다.
- 완료·실패한 게시물 Export 작업을 관리자 화면에서 삭제하면 연결된 ZIP 파일도 함께 삭제된다. - 완료·실패한 게시물 Export 작업을 관리자 화면에서 삭제하면 연결된 ZIP 파일도 함께 삭제된다.
- `public/uploads/`는 Git에 포함하지 않는다. - `public/uploads/`는 Git에 포함하지 않는다.
- NAS 운영에서는 `docker-compose.yml`의 `./public/uploads:/app/public/uploads` 볼륨으로 업로드 파일을 유지한다. - NAS 운영에서는 `docker-compose.build.yml`의 `./public/uploads:/app/public/uploads` 볼륨으로 업로드 파일을 유지한다.
- 운영 빌드에서는 `/uploads/**` 서버 라우트가 `.output/public`이 아니라 `/app/public/uploads` 볼륨의 실제 파일을 직접 제공한다. - 운영 빌드에서는 `/uploads/**` 서버 라우트가 `.output/public`이 아니라 `/app/public/uploads` 볼륨의 실제 파일을 직접 제공한다.
- `/uploads/**` 응답은 `ETag`와 `Last-Modified`를 제공한다. 고유 URL로 저장되는 원본·시스템·회원 썸네일 파일은 1년 immutable 캐시를 사용하고, 같은 URL로 재생성될 수 있는 게시물 `thumbs` 파생 파일은 1시간 캐시와 `stale-while-revalidate`를 사용한다. - `/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`가 앱 한도보다 작지 않은지 함께 확인한다. - `MAX_FILE_SIZE`, `MAX_VIDEO_FILE_SIZE`, `MAX_AUDIO_FILE_SIZE`, `MAX_DOCUMENT_FILE_SIZE` 환경 변수로 관리자 미디어 업로드 파일별 최대 크기를 제한한다. 1회 업로드 최대 파일 수와 전체 용량은 관리자 Settings > POST 설정 > 업로드 제한에서 조절한다. 리버스 프록시(Nginx 등)를 쓰면 `client_max_body_size`가 앱 한도보다 작지 않은지 함께 확인한다.

View File

@@ -1,3 +1,7 @@
## 2026-10-21 v1.12.27 — NAS 앱 재배포와 이미지 빌드를 분리한다
제공된 재배포 로그에는 소스 빌드가 없고 원격 `sori-studio` pull 오류와 컨테이너 재생성만 있어, Compose의 `pull_policy: build`를 지정하면 앱이 로컬 소스를 빌드할 것이라는 이전 전제를 폐기한다. 앱 재배포를 유지하려는 사용자 선호에 맞춰 명시적인 이미지 빌드 스크립트와 로컬 이미지 전용 GUI 구성을 나눈다. 사용자가 확인한 앱 기준 파일명 `docker-compose.yaml`을 실행용으로 관리하고 기존 정의는 `docker-compose.build.yml`로 옮긴다. 운영 마이그레이션·백필도 실행용 파일을 선택한다. 처음 갱신할 때 NAS의 비추적 yaml이 Git과 충돌하지 않도록 기존 파일 백업 절차를 둔다. GUI 구성은 원격 앱 pull을 금지하고, 실제 GUI가 이를 덮어쓰는지는 NAS 옵션·로그로 확인해야 한다. 기존 DB·네트워크·업로드 설정은 동일하게 유지한다.
## 2026-10-21 v1.12.26 — 운영 Compose 프로젝트명과 파일 선택을 일치시킨다 ## 2026-10-21 v1.12.26 — 운영 Compose 프로젝트명과 파일 선택을 일치시킨다
제공된 NAS 로그에서 빌드는 성공했지만 CLI가 `soristudio` 프로젝트의 네트워크를 새로 만들려다 기존 `sori-studio` 프로젝트의 동일 대역과 충돌했다. 기존 운영 네트워크를 재사용하도록 Compose 기본 이름을 환경 변수 기반 `sori-studio`로 맞추고, 배포 안내에는 프로젝트명을 명시한다. `.yaml`과 `.yml` 중복 경고를 줄이고 운영 스크립트도 같은 구성을 사용하도록 파일 경로를 명시한다. DB 외부 볼륨의 이름과 보관 방식은 유지한다. 사용자가 Docker 앱 배포를 선호하므로 소스 git pull 후 기존 프로젝트의 Compose 구성에서 재배포하는 경로를 기본 안내로 둔다. 앱 버전의 실제 재빌드 수행 여부는 배포 로그로 확인하며, CLI는 보조 경로로 구분한다. 제공된 NAS 로그에서 빌드는 성공했지만 CLI가 `soristudio` 프로젝트의 네트워크를 새로 만들려다 기존 `sori-studio` 프로젝트의 동일 대역과 충돌했다. 기존 운영 네트워크를 재사용하도록 Compose 기본 이름을 환경 변수 기반 `sori-studio`로 맞추고, 배포 안내에는 프로젝트명을 명시한다. `.yaml`과 `.yml` 중복 경고를 줄이고 운영 스크립트도 같은 구성을 사용하도록 파일 경로를 명시한다. DB 외부 볼륨의 이름과 보관 방식은 유지한다. 사용자가 Docker 앱 배포를 선호하므로 소스 git pull 후 기존 프로젝트의 Compose 구성에서 재배포하는 경로를 기본 안내로 둔다. 앱 버전의 실제 재빌드 수행 여부는 배포 로그로 확인하며, CLI는 보조 경로로 구분한다.

View File

@@ -136,7 +136,7 @@
| 파일 | 용도 | | 파일 | 용도 |
|------|------| |------|------|
| Dockerfile | Nuxt 프로덕션 앱 이미지 빌드 | | Dockerfile | Nuxt 프로덕션 앱 이미지 빌드 |
| docker-compose.yml | Docker CLI·Buildx 가능 환경의 앱 빌드 포함 Compose 실행, `sori-studio:latest` 앱 이미지 재빌드 유도, 운영 PostgreSQL 고정 외부 볼륨 연결 | | docker-compose.build.yml | Docker CLI·Buildx 환경 앱 이미지 빌드 및 명시적 CLI 배포, 운영 PostgreSQL 고정 외부 볼륨 연결 |
## Nuxt 모듈 ## Nuxt 모듈
@@ -637,7 +637,7 @@
| Dockerfile | NAS 운영 이미지 빌드, `.output/server` 기반 앱 런타임과 이미지 백필 스크립트 포함 | | Dockerfile | NAS 운영 이미지 빌드, `.output/server` 기반 앱 런타임과 이미지 백필 스크립트 포함 |
| tests/markdown-html-block.test.js | 게시물 HTML sanitizer 공격 우회·허용 HTML 회귀 테스트 | | tests/markdown-html-block.test.js | 게시물 HTML sanitizer 공격 우회·허용 HTML 회귀 테스트 |
| tests/multipart-file-upload.test.js | 다중 multipart 디스크 스트리밍·파일 크기 제한 회귀 테스트 | | tests/multipart-file-upload.test.js | 다중 multipart 디스크 스트리밍·파일 크기 제한 회귀 테스트 |
| docker-compose.yml | 운영 프로젝트명 환경 변수/기본 sori-studio 선언, NAS 컨테이너 실행, PostgreSQL 호스트 기본 loopback 바인딩 | | docker-compose.build.yml | 운영 앱 이미지 빌드·CLI 빌드/배포용 Compose 정의 |
| components/admin/AdminCommentList.vue | 대시보드 최신 댓글 5개 작은 목록·게시글/작성자/시각/한 줄 내용·30초 갱신·조회 오류 재시도 | | components/admin/AdminCommentList.vue | 대시보드 최신 댓글 5개 작은 목록·게시글/작성자/시각/한 줄 내용·30초 갱신·조회 오류 재시도 |
| server/routes/admin/api/comments.get.js | 관리자 댓글 조회 인증·댓글 내용/게시물 제목 검색·Zod query 검증 | | server/routes/admin/api/comments.get.js | 관리자 댓글 조회 인증·댓글 내용/게시물 제목 검색·Zod query 검증 |
| server/repositories/comment-repository.js | 공개 댓글 조회·작성·좋아요와 관리자 댓글 최신순 페이지 조회 | | server/repositories/comment-repository.js | 공개 댓글 조회·작성·좋아요와 관리자 댓글 최신순 페이지 조회 |
@@ -651,3 +651,5 @@
| tests/analytics-display.test.js | 천 단위 표시·최근순 중복 제거·공개 경로 검증 | | tests/analytics-display.test.js | 천 단위 표시·최근순 중복 제거·공개 경로 검증 |
| tests/playwright/e2e/admin-dashboard.spec.js | 대시보드 쉼표·섹션 순서·한 줄 접속자·페이지 목록 접근 검증 | | tests/playwright/e2e/admin-dashboard.spec.js | 대시보드 쉼표·섹션 순서·한 줄 접속자·페이지 목록 접근 검증 |
| tests/production-compose.test.js | 운영 마이그레이션·썸네일 백필 스크립트의 Compose 파일·환경 파일 전달 격리 검증 | | tests/production-compose.test.js | 운영 마이그레이션·썸네일 백필 스크립트의 Compose 파일·환경 파일 전달 격리 검증 |
| docker-compose.yaml | NAS Docker 앱용 독립 실행 구성·사전 빌드한 로컬 앱 이미지 사용·앱 원격 pull 금지 |
| scripts/build-production-image.sh | 운영 환경 파일을 사용해 앱 이미지만 빌드·Docker 오류 반환·컨테이너/DB 미변경 |

View File

@@ -1115,8 +1115,15 @@ APP_PORT=43080
- 접속자 대표 페이지는 포커스 여부가 아닌 마지막 확인 시각 기준. 현재 API의 회원별·익명 IP별 묶음은 유지. 인원 표시는 반환된 목록 수와 전체 접속자 수를 구분. - 접속자 대표 페이지는 포커스 여부가 아닌 마지막 확인 시각 기준. 현재 API의 회원별·익명 IP별 묶음은 유지. 인원 표시는 반환된 목록 수와 전체 접속자 수를 구분.
- 열린 페이지 팝오버는 hover 시 일시 표시, 클릭/Enter로 고정 전환. 포인터를 목록으로 이동해도 링크 접근을 유지하며, 화면 범위에 맞춰 위/아래 배치하고 긴 목록은 내부 스크롤 제공. - 열린 페이지 팝오버는 hover 시 일시 표시, 클릭/Enter로 고정 전환. 포인터를 목록으로 이동해도 링크 접근을 유지하며, 화면 범위에 맞춰 위/아래 배치하고 긴 목록은 내부 스크롤 제공.
## 운영 Compose 식별 (v1.12.26) ## 운영 Compose 식별 (v1.12.27)
- 기본 프로젝트명은 `COMPOSE_PROJECT_NAME` 또는 `sori-studio`. Compose 최상위 `name`으로 선언하고 운영 명령은 `-p sori-studio -f docker-compose.yml` 사용. - 기본 프로젝트명은 `COMPOSE_PROJECT_NAME` 또는 `sori-studio`. Compose 최상위 `name`으로 선언하고 실행 명령은 `-p sori-studio -f docker-compose.yaml`, 빌드 명령은 `-f docker-compose.build.yml` 사용.
- 운영 마이그레이션·썸네일 백필 스크립트는 루트의 `docker-compose.yml`을 명시적으로 선택한다. 환경 파일 선택 및 DB 외부 볼륨 설정은 유지. - 운영 마이그레이션·썸네일 백필 스크립트는 루트의 실행용 `docker-compose.yaml`을 명시적으로 선택한다. 환경 파일 선택 및 DB 외부 볼륨 설정은 유지.
- 운영 업데이트 기본 경로는 Git 소스 동기화 후 UGREEN Docker 앱의 기존 `sori-studio` 프로젝트에서 Compose 구성 확인·재배포. CLI는 보조 경로이며 GUI의 별도 Compose 사본과 원본 `.yml` 차이를 확인한다. - v1.12.27부터 운영 업데이트 기본 경로는 Git 소스 동기화 → 터미널 이미지 빌드 → UGREEN Docker 앱의 기존 `sori-studio` 프로젝트 재배포. 제공된 재배포 로그에 빌드가 없어 앱 단독 소스 빌드를 전제하지 않는다.
## 로컬 이미지 사전 빌드·NAS 앱 실행 (v1.12.27)
- `sh scripts/build-production-image.sh`: `ENV_FILE` 또는 `.env.production`을 선택한 뒤 `docker-compose.build.yml`의 앱 서비스만 `build`. 운영 환경 파일 누락/읽기 실패는 종료 코드 1, Docker 빌드 오류는 그대로 반환. 컨테이너 시작·삭제·네트워크·DB 변경 명령은 실행하지 않는다.
- `docker-compose.yaml`: GUI에 독립 구성으로 적용. 앱은 기본 빌드 이미지와 동일한 `sori-studio:latest`, `pull_policy: never`, `build` 없음. 프로젝트명·네트워크·외부 DB 볼륨·포트·업로드/마이그레이션 경로는 기본 Compose와 동일.
- GUI가 pull 정책을 덮어쓰는 실제 동작은 NAS에서 확인해야 하며 이미지 다운로드/업데이트 옵션이 있으면 앱 이미지에 대해 해제. `docker-compose.build.yml`은 CLI 빌드·배포용으로 유지.
- NAS에 기존 비추적 `docker-compose.yaml`이 있으면 첫 git pull 전에 이름을 바꿔 백업한다. 두 구성을 모두 저장소에서 관리하며 실행용 기본 파일과 빌드용 파일의 이름을 구분한다.

View File

@@ -20,3 +20,5 @@
## 데이터베이스 ## 데이터베이스
## 배포 ## 배포
- UGREEN Docker 앱에서 로컬 이미지 전용 `docker-compose.yaml` 재배포 실기 확인. 이미지 업데이트/다운로드 옵션 유무를 확인하고, 원격 pull 오류가 재발하는지·실행 컨테이너 이미지 ID가 사전 빌드 이미지 ID와 같은지 대조. GUI의 정책 덮어쓰기와 별도 Compose 사본 적용 여부는 현재 미확인.

View File

@@ -1,5 +1,14 @@
# 업데이트 이력 # 업데이트 이력
## v1.12.27
- UGREEN 재배포의 원격 이미지 pull 오류를 기준으로 앱 단독 소스 빌드 안내 정정.
- Docker 앱 기준 docker-compose.yaml을 로컬 이미지 실행용으로 관리하고 원격 pull 금지 정책 추가.
- 기존 Compose를 docker-compose.build.yml 빌드용으로 분리하고 운영 스크립트를 실행용 yaml 기준으로 정리.
- 운영 이미지만 빌드하는 스크립트와 빌드 성공/실패·환경 파일 누락 검증 추가.
- 소스 갱신·터미널 이미지 빌드·Docker 앱 재배포 단계 분리 및 NAS 기존 yaml 최초 백업 문서화.
- 기본 yaml 선택·두 Compose의 이미지/DB/네트워크/업로드 일치·셸 문법·JavaScript 문법·단위 테스트 283건 검증 진행.
## v1.12.26 ## v1.12.26
- 운영 Compose 기본 프로젝트명을 기존 sori-studio로 고정해 중복 네트워크 생성 충돌 보정. - 운영 Compose 기본 프로젝트명을 기존 sori-studio로 고정해 중복 네트워크 생성 충돌 보정.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "1.12.26", "version": "1.12.27",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sori.studio", "name": "sori.studio",
"version": "1.12.26", "version": "1.12.27",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "sori.studio", "name": "sori.studio",
"version": "1.12.26", "version": "1.12.27",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "imports": {

View File

@@ -28,11 +28,11 @@ pick_compose_env_file() {
compose() { compose() {
if [ -n "$COMPOSE_ENV_FILE" ]; then if [ -n "$COMPOSE_ENV_FILE" ]; then
docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" --env-file "$COMPOSE_ENV_FILE" "$@"
return 0 return 0
fi fi
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
} }
pick_compose_env_file pick_compose_env_file

View File

@@ -28,11 +28,11 @@ pick_compose_env_file() {
compose() { compose() {
if [ -n "$COMPOSE_ENV_FILE" ]; then if [ -n "$COMPOSE_ENV_FILE" ]; then
docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" --env-file "$COMPOSE_ENV_FILE" "$@"
return 0 return 0
fi fi
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
} }
pick_compose_env_file pick_compose_env_file

View File

@@ -28,11 +28,11 @@ pick_compose_env_file() {
compose() { compose() {
if [ -n "$COMPOSE_ENV_FILE" ]; then if [ -n "$COMPOSE_ENV_FILE" ]; then
docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" --env-file "$COMPOSE_ENV_FILE" "$@"
return 0 return 0
fi fi
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
} }
pick_compose_env_file pick_compose_env_file

View File

@@ -0,0 +1,22 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
APP_SERVICE=${APP_SERVICE:-sori-studio}
cd "$ROOT_DIR"
if [ -n "${ENV_FILE:-}" ]; then
COMPOSE_ENV_FILE=$ENV_FILE
elif [ -r .env.production ]; then
COMPOSE_ENV_FILE=.env.production
else
printf '%s\n' '운영 환경 파일이 없습니다. .env.production 또는 ENV_FILE을 지정하세요.' >&2
exit 1
fi
if [ ! -r "$COMPOSE_ENV_FILE" ]; then
printf '%s\n' '지정한 환경 파일을 읽을 수 없습니다.' >&2
exit 1
fi
exec docker compose -f "$ROOT_DIR/docker-compose.build.yml" --env-file "$COMPOSE_ENV_FILE" build "$APP_SERVICE"

View File

@@ -52,11 +52,11 @@ load_env_file() {
compose() { compose() {
if [ -n "$COMPOSE_ENV_FILE" ]; then if [ -n "$COMPOSE_ENV_FILE" ]; then
docker compose -f "$ROOT_DIR/docker-compose.yml" --env-file "$COMPOSE_ENV_FILE" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" --env-file "$COMPOSE_ENV_FILE" "$@"
return 0 return 0
fi fi
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@" docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
} }
# 실행 중인 DB 컨테이너에서 PostgreSQL 계정 정보 읽기 # 실행 중인 DB 컨테이너에서 PostgreSQL 계정 정보 읽기

View File

@@ -23,10 +23,43 @@ for (const script of ['migrate-production-db.sh', 'backfill-post-thumbnails-prod
const calls = readFileSync(logPath, 'utf8').trim().split('\n') const calls = readFileSync(logPath, 'utf8').trim().split('\n')
assert.ok(calls.length > 0) assert.ok(calls.length > 0)
for (const call of calls) { for (const call of calls) {
assert.ok(call.startsWith(`<compose><-f><${join(root, 'docker-compose.yml')}><--env-file><${relative(root, envPath)}>`)) assert.ok(call.startsWith(`<compose><-f><${join(root, 'docker-compose.yaml')}><--env-file><${relative(root, envPath)}>`))
} }
} finally { } finally {
rmSync(directory, { recursive: true, force: true }) rmSync(directory, { recursive: true, force: true })
} }
}) })
} }
for (const exitCode of [0, 19]) {
test(`운영 이미지 빌드는 컨테이너 변경 없이 build만 실행하고 종료 코드 ${exitCode}를 전달한다`, () => {
const directory = mkdtempSync(join(tmpdir(), 'sori-image-build-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"\nexit ${exitCode}\n`, { mode: 0o755 })
const result = spawnSync('sh', ['scripts/build-production-image.sh'], {
cwd: root, encoding: 'utf8',
env: { ...process.env, PATH: `${directory}:${process.env.PATH}`, ENV_FILE: envPath, SORI_TEST_DOCKER_LOG: logPath, APP_SERVICE: 'sori-studio' }
})
assert.equal(result.status, exitCode, result.stderr)
assert.equal(readFileSync(logPath, 'utf8'), `<compose><-f><${join(root, 'docker-compose.build.yml')}><--env-file><${envPath}><build><sori-studio>`)
} finally {
rmSync(directory, { recursive: true, force: true })
}
})
}
test('운영 이미지 빌드는 읽을 수 없는 환경 파일을 거부한다', () => {
const directory = mkdtempSync(join(tmpdir(), 'sori-image-build-test-'))
try {
const result = spawnSync('sh', ['scripts/build-production-image.sh'], {
cwd: root, encoding: 'utf8', env: { ...process.env, ENV_FILE: join(directory, 'missing.env') }
})
assert.equal(result.status, 1)
assert.match(result.stderr, /환경 파일을 읽을 수 없습니다/)
} finally {
rmSync(directory, { recursive: true, force: true })
}
})