v1.12.27 NAS 실행 yaml 및 명시적 이미지 빌드 분리
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ public/uploads/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
docker-compose.yaml.backup.*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
45
docker-compose.yaml
Normal file
45
docker-compose.yaml
Normal 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}
|
||||
@@ -192,10 +192,10 @@ cp .env.example .env.production
|
||||
# Docker 내부 앱에서 PostgreSQL에 접근할 때는 sori-studio-db:5432 사용
|
||||
|
||||
# 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 배포에서는
|
||||
# docker-compose.yml과 .env.production을 같은 프로젝트 위치에 두고 Compose 빌드를 실행한다.
|
||||
# 이미지를 사전 빌드한 뒤 docker-compose.yaml과 .env.production을 같은 프로젝트 위치에서 사용한다.
|
||||
# 앱과 DB 컨테이너는 env_file 설정으로 .env.production을 직접 읽는다.
|
||||
|
||||
# 운영 DB 마이그레이션 상태 확인
|
||||
@@ -210,34 +210,58 @@ sudo sh scripts/migrate-production-db.sh migrate
|
||||
|
||||
### 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
|
||||
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
|
||||
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.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가 이 폴더를 가리켜야 한다.
|
||||
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 경로를 사용한다.
|
||||
1. 프로젝트의 실제 소스 폴더에서 다음 명령으로 최신 소스를 받고 로컬 이미지를 만든다. 빌드 스크립트는 기존 컨테이너·네트워크·DB를 재생성하지 않고 `docker compose build sori-studio`만 실행한다. `.env.production`이 없으면 `ENV_FILE`을 명시해야 하며, 빌드 실패 시 재배포로 진행하지 않는다.
|
||||
|
||||
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 보조 경로)
|
||||
|
||||
Docker 앱 대신 CLI로 배포해야 할 때는 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
|
||||
기본 Compose로 빌드와 배포를 한 번에 CLI에서 처리하려면 아래처럼 기존 프로젝트명·Compose 파일을 명시한다. 최초 설치 절차와 달리 `git clone`은 하지 않는다.
|
||||
|
||||
```bash
|
||||
# 프로젝트 루트로 이동 (경로는 NAS 환경에 맞게 조정)
|
||||
@@ -251,7 +275,7 @@ sudo sh scripts/migrate-production-db.sh status
|
||||
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`로 덮어쓰이지 않는다. 값을 바꿀 때만 파일을 직접 수정한다.
|
||||
- `public/uploads/` 업로드 파일은 Docker 볼륨(`./public/uploads`)에 있으므로, **이미지 파일만 추가·수정한 경우** 앱 재빌드 없이도 URL로 바로 보인다.
|
||||
- 로컬에서 미리 확인하려면 `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
|
||||
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 앱에서 `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
|
||||
sudo docker buildx version
|
||||
@@ -338,7 +362,7 @@ NAS에 Docker 컨테이너가 많이 실행 중이면 `could not find an availab
|
||||
|
||||
```bash
|
||||
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`가 반복 시도하는 상태다. 원인은 로그에 나온다.
|
||||
|
||||
1. **어느 서비스인지 확인** (`docker-compose.yml` 기준 이름은 `sori-studio`, `sori-studio-db`).
|
||||
1. **어느 서비스인지 확인** (`docker-compose.build.yml` 기준 이름은 `sori-studio`, `sori-studio-db`).
|
||||
|
||||
```bash
|
||||
docker ps -a --filter "name=sori-studio"
|
||||
@@ -512,8 +536,8 @@ docker logs sori-studio-db --tail 150
|
||||
Compose로 올렸다면:
|
||||
|
||||
```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.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 --tail 200
|
||||
sudo docker compose -p sori-studio -f docker-compose.build.yml --env-file .env.production logs sori-studio-db --tail 200
|
||||
```
|
||||
|
||||
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` 로그에 `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` 권한
|
||||
|
||||
`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로 다음을 적용한다. **비밀번호는 바꾸지 않으며**, 읽기·디렉터리 통과만 연다.
|
||||
|
||||
@@ -541,7 +565,7 @@ sudo chmod a+x . db db/migrations
|
||||
그다음 DB 컨테이너만 재시작한다.
|
||||
|
||||
```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 등)의 디렉터리를 마운트할 때 권한이 더 예측 가능하다.
|
||||
@@ -577,7 +601,7 @@ Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에
|
||||
- 게시물 Export ZIP 산출물은 `public/uploads/exports/YYYY/MM/{jobId}/` 아래 생성되며, 관리자 다운로드 API를 통해 내려받는다.
|
||||
- 완료·실패한 게시물 Export 작업을 관리자 화면에서 삭제하면 연결된 ZIP 파일도 함께 삭제된다.
|
||||
- `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/**` 응답은 `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`가 앱 한도보다 작지 않은지 함께 확인한다.
|
||||
|
||||
@@ -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 프로젝트명과 파일 선택을 일치시킨다
|
||||
|
||||
제공된 NAS 로그에서 빌드는 성공했지만 CLI가 `soristudio` 프로젝트의 네트워크를 새로 만들려다 기존 `sori-studio` 프로젝트의 동일 대역과 충돌했다. 기존 운영 네트워크를 재사용하도록 Compose 기본 이름을 환경 변수 기반 `sori-studio`로 맞추고, 배포 안내에는 프로젝트명을 명시한다. `.yaml`과 `.yml` 중복 경고를 줄이고 운영 스크립트도 같은 구성을 사용하도록 파일 경로를 명시한다. DB 외부 볼륨의 이름과 보관 방식은 유지한다. 사용자가 Docker 앱 배포를 선호하므로 소스 git pull 후 기존 프로젝트의 Compose 구성에서 재배포하는 경로를 기본 안내로 둔다. 앱 버전의 실제 재빌드 수행 여부는 배포 로그로 확인하며, CLI는 보조 경로로 구분한다.
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| Dockerfile | Nuxt 프로덕션 앱 이미지 빌드 |
|
||||
| docker-compose.yml | Docker CLI·Buildx 가능 환경의 앱 빌드 포함 Compose 실행, `sori-studio:latest` 앱 이미지 재빌드 유도, 운영 PostgreSQL 고정 외부 볼륨 연결 |
|
||||
| docker-compose.build.yml | Docker CLI·Buildx 환경 앱 이미지 빌드 및 명시적 CLI 배포, 운영 PostgreSQL 고정 외부 볼륨 연결 |
|
||||
|
||||
## Nuxt 모듈
|
||||
|
||||
@@ -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 | 운영 프로젝트명 환경 변수/기본 sori-studio 선언, NAS 컨테이너 실행, PostgreSQL 호스트 기본 loopback 바인딩 |
|
||||
| docker-compose.build.yml | 운영 앱 이미지 빌드·CLI 빌드/배포용 Compose 정의 |
|
||||
| components/admin/AdminCommentList.vue | 대시보드 최신 댓글 5개 작은 목록·게시글/작성자/시각/한 줄 내용·30초 갱신·조회 오류 재시도 |
|
||||
| server/routes/admin/api/comments.get.js | 관리자 댓글 조회 인증·댓글 내용/게시물 제목 검색·Zod query 검증 |
|
||||
| server/repositories/comment-repository.js | 공개 댓글 조회·작성·좋아요와 관리자 댓글 최신순 페이지 조회 |
|
||||
@@ -651,3 +651,5 @@
|
||||
| tests/analytics-display.test.js | 천 단위 표시·최근순 중복 제거·공개 경로 검증 |
|
||||
| tests/playwright/e2e/admin-dashboard.spec.js | 대시보드 쉼표·섹션 순서·한 줄 접속자·페이지 목록 접근 검증 |
|
||||
| tests/production-compose.test.js | 운영 마이그레이션·썸네일 백필 스크립트의 Compose 파일·환경 파일 전달 격리 검증 |
|
||||
| docker-compose.yaml | NAS Docker 앱용 독립 실행 구성·사전 빌드한 로컬 앱 이미지 사용·앱 원격 pull 금지 |
|
||||
| scripts/build-production-image.sh | 운영 환경 파일을 사용해 앱 이미지만 빌드·Docker 오류 반환·컨테이너/DB 미변경 |
|
||||
|
||||
15
docs/spec.md
15
docs/spec.md
@@ -1115,8 +1115,15 @@ APP_PORT=43080
|
||||
- 접속자 대표 페이지는 포커스 여부가 아닌 마지막 확인 시각 기준. 현재 API의 회원별·익명 IP별 묶음은 유지. 인원 표시는 반환된 목록 수와 전체 접속자 수를 구분.
|
||||
- 열린 페이지 팝오버는 hover 시 일시 표시, 클릭/Enter로 고정 전환. 포인터를 목록으로 이동해도 링크 접근을 유지하며, 화면 범위에 맞춰 위/아래 배치하고 긴 목록은 내부 스크롤 제공.
|
||||
|
||||
## 운영 Compose 식별 (v1.12.26)
|
||||
## 운영 Compose 식별 (v1.12.27)
|
||||
|
||||
- 기본 프로젝트명은 `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` 차이를 확인한다.
|
||||
- 기본 프로젝트명은 `COMPOSE_PROJECT_NAME` 또는 `sori-studio`. Compose 최상위 `name`으로 선언하고 실행 명령은 `-p sori-studio -f docker-compose.yaml`, 빌드 명령은 `-f docker-compose.build.yml` 사용.
|
||||
- 운영 마이그레이션·썸네일 백필 스크립트는 루트의 실행용 `docker-compose.yaml`을 명시적으로 선택한다. 환경 파일 선택 및 DB 외부 볼륨 설정은 유지.
|
||||
- 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 전에 이름을 바꿔 백업한다. 두 구성을 모두 저장소에서 관리하며 실행용 기본 파일과 빌드용 파일의 이름을 구분한다.
|
||||
|
||||
@@ -20,3 +20,5 @@
|
||||
## 데이터베이스
|
||||
|
||||
## 배포
|
||||
|
||||
- UGREEN Docker 앱에서 로컬 이미지 전용 `docker-compose.yaml` 재배포 실기 확인. 이미지 업데이트/다운로드 옵션 유무를 확인하고, 원격 pull 오류가 재발하는지·실행 컨테이너 이미지 ID가 사전 빌드 이미지 ID와 같은지 대조. GUI의 정책 덮어쓰기와 별도 Compose 사본 적용 여부는 현재 미확인.
|
||||
|
||||
@@ -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
|
||||
|
||||
- 운영 Compose 기본 프로젝트명을 기존 sori-studio로 고정해 중복 네트워크 생성 충돌 보정.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.12.26",
|
||||
"version": "1.12.27",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.12.26",
|
||||
"version": "1.12.27",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.12.26",
|
||||
"version": "1.12.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -28,11 +28,11 @@ pick_compose_env_file() {
|
||||
|
||||
compose() {
|
||||
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
|
||||
fi
|
||||
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@"
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
|
||||
}
|
||||
|
||||
pick_compose_env_file
|
||||
|
||||
@@ -28,11 +28,11 @@ pick_compose_env_file() {
|
||||
|
||||
compose() {
|
||||
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
|
||||
fi
|
||||
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@"
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
|
||||
}
|
||||
|
||||
pick_compose_env_file
|
||||
|
||||
@@ -28,11 +28,11 @@ pick_compose_env_file() {
|
||||
|
||||
compose() {
|
||||
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
|
||||
fi
|
||||
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@"
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
|
||||
}
|
||||
|
||||
pick_compose_env_file
|
||||
|
||||
22
scripts/build-production-image.sh
Normal file
22
scripts/build-production-image.sh
Normal 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"
|
||||
@@ -52,11 +52,11 @@ load_env_file() {
|
||||
|
||||
compose() {
|
||||
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
|
||||
fi
|
||||
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yml" "$@"
|
||||
docker compose -f "$ROOT_DIR/docker-compose.yaml" "$@"
|
||||
}
|
||||
|
||||
# 실행 중인 DB 컨테이너에서 PostgreSQL 계정 정보 읽기
|
||||
|
||||
@@ -23,10 +23,43 @@ for (const script of ['migrate-production-db.sh', 'backfill-post-thumbnails-prod
|
||||
const calls = readFileSync(logPath, 'utf8').trim().split('\n')
|
||||
assert.ok(calls.length > 0)
|
||||
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 {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user