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

View File

@@ -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`가 앱 한도보다 작지 않은지 함께 확인한다.