Unify NAS operations on tracked docker-compose.yaml

This commit is contained in:
2026-10-14 11:08:42 +09:00
parent 46ad429cc7
commit ff89daac8c
10 changed files with 118 additions and 104 deletions

1
.gitignore vendored
View File

@@ -10,7 +10,6 @@ test-results/
.DS_Store
tests/e2e/*.png
compose.nas.yaml
/docker-compose.yaml
backups/
/uploads/
hop.proxy.conf

View File

@@ -28,15 +28,15 @@ API 테스트는 격리된 실제 SQLite DB와 임시 로컬 포트의 Socket.IO
## Docker 배포 준비
유그린 NAS에서 Docker 앱 버튼으로 배포하는 순서는 [deploy.md](deploy.md)를 따라 진행하세요. 현재 배포 경로는 Cloudflare Tunnel → 기존 NPM(8080) → NAS의 hop.(50189)입니다.
유그린 NAS의 Docker 앱과 터미널에서 같은 `docker-compose.yaml`로 배포하는 순서는 [deploy.md](deploy.md)를 따라 진행하세요. 현재 배포 경로는 Cloudflare Tunnel → 기존 NPM(8080) → NAS의 hop.(50189)입니다.
`.env.example`을 `.env`로 복사하고 비밀값을 설정합니다. `SESSION_SECRET`과 `DISCOVERY_SECRET`은 서로 다른 32자 이상의 무작위 비밀값이어야 합니다. 예를 들어 `openssl rand -hex 32`로 각각 생성합니다. Git에 실제 환경 파일·DB·파일을 넣지 않습니다.
이번 NAS 배포는 사용자가 지정한 **50189**를 호스트·컨테이너 포트로 사용합니다. `scripts/configure-nas.sh`가 실제 NAS 내부 IP를 물어보고 운영 `.env`와 배포 YAML을 준비합니다. 다른 환경에서는 충돌 없는 포트·바인딩 IP·프록시 경로를 따로 확인하세요. Compose는 필수 값이 비어 있으면 실행을 거절합니다. `.env`는 Node 서버 직접 실행 시 자동 로드되지 않으며 Compose 또는 실행 환경이 주입합니다.
이번 NAS 배포는 사용자가 지정한 **50189**를 호스트·컨테이너 포트로 사용합니다. `scripts/configure-nas.sh`가 실제 NAS 내부 IP를 물어보고 운영 `.env`와 저장 권한을 준비하고 Git의 `docker-compose.yaml`을 검사합니다. Compose 파일은 생성하거나 덮어쓰지 않습니다. 다른 환경에서는 충돌 없는 포트·바인딩 IP·프록시 경로를 따로 확인하세요. Compose는 필수 값이 비어 있으면 실행을 거절합니다. `.env`는 Node 서버 직접 실행 시 자동 로드되지 않으며 Compose 또는 실행 환경이 주입합니다.
```sh
docker compose config
docker compose up -d --build
sudo docker compose -f docker-compose.yaml config --quiet
sudo docker compose -f docker-compose.yaml up -d --build
```
앱 시작 시 버전 관리된 Drizzle 마이그레이션을 적용하며 실패하면 시작하지 않습니다. DB 볼륨과 프로젝트의 uploads 폴더는 재배포·재시작 후 유지됩니다. `docker compose down -v`는 데이터를 삭제하므로 사용하지 마세요.
@@ -55,11 +55,11 @@ QR·4자리 코드는 기본 10분의 임시 공간 초대입니다. 초대 코
## 파일 저장 위치
NAS의 파일 원본은 `/volume1/docker/projects/apps/hop/uploads/originals`에 저장합니다. 앱은 NAS 사용자의 UID/GID로 실행하며 업로드 폴더는 Git·Docker 빌드에서 제외합니다. DB는 `hop_hop_database` 영구 볼륨에 있습니다. 원본 수동 삭제는 조회·다운로드·전송과 1분 주기 정리 시 감지해 공유된 모든 항목과 용량 계산에 반영합니다. 권한 오류는 삭제로 오인하지 않습니다. 기존 설치의 최초 전환·테스트 자료 초기화는 [deploy.md](deploy.md)의 7-1단계를 진행하고, 이후 일반 업데이트에서는 초기화하지 않습니다.
NAS의 파일 원본은 `/volume1/docker/projects/apps/hop/uploads/originals`에 저장합니다. 앱은 NAS 사용자의 UID/GID로 실행하며 업로드 폴더는 Git·Docker 빌드에서 제외합니다. DB는 `hop_hop_database` 영구 볼륨에 있습니다. 원본 수동 삭제는 조회·다운로드·전송과 1분 주기 정리 시 감지해 공유된 모든 항목과 용량 계산에 반영합니다. 권한 오류는 삭제로 오인하지 않습니다. 단일 Compose 파일로 전환하는 순서는 [deploy.md](deploy.md)의 7-0단계를 따르며 기존 계정·DB·파일은 유지합니다.
## 업데이트와 백업
일반 코드·UI 업데이트는 `git pull --ff-only origin main` → Docker 이미지 재빌드 → 재배포로 진행합니다. Docker 구성과 환경변수가 바뀌지 않았다면 기존 YAML을 그대로 사용합니다. DB·업로드는 기존 볼륨에 유지되므로 매번 백업할 필요는 없습니다. DB 구조 변경에 대비하거나 데이터를 따로 보관하려면 앱을 정지한 뒤 DB 볼륨(WAL·SHM 포함), 업로드 볼륨, `.env`를 같은 시점에 백업합니다. 자세한 순서는 [deploy.md의 업데이트 절차](deploy.md#7-다음-버전으로-업데이트)를 참고하세요.
일반 업데이트는 `git pull --ff-only origin main` → `sudo docker compose -f docker-compose.yaml up -d --build`로 진행합니다. 소스와 운영 Compose 설정을 Git에서 함께 관리하며 UGREEN 앱도 동일한 파일을 사용합니다. DB·업로드는 기존 볼륨에 유지되므로 매번 백업할 필요는 없습니다. DB 구조 변경에 대비하거나 데이터를 따로 보관하려면 앱을 정지한 뒤 DB 볼륨(WAL·SHM 포함), 업로드 볼륨, `.env`를 같은 시점에 백업합니다. 자세한 순서는 [deploy.md의 업데이트 절차](deploy.md#7-다음-버전으로-업데이트)를 참고하세요.
파일당 기본 2GiB, 익명 주체당 2GiB, 전체 익명 20GiB 제한과 최소 5GiB 여유 공간을 적용합니다. MB/KB 환경변수는 MiB/KiB 기준입니다. 파일은 수락 후 스트리밍 저장하고 중단 시 임시 파일·예약을 회수합니다. 파일은 8MiB 단위로 나누어 전송하고 서버가 받은 위치를 기록합니다. 중간 오류는 최대 3회 자동 재시도하며, 현재 화면에서 같은 파일로 ‘다시 전송’할 수 있습니다. 백그라운드 완료는 보장하지 않습니다.

View File

@@ -415,7 +415,7 @@ hop/
apps/server/
packages/shared/
docker/
compose.yaml
docker-compose.yaml
Dockerfile
.env.example
SPEC.md

159
deploy.md
View File

@@ -1,12 +1,12 @@
# hop. 유그린 NAS 배포 순서
**SSH에서는 소스와 설정만 준비합니다. 시작·중지·재빌드는 NAS의 Docker 앱에서 합니다.**
**운영 설정은 Git에서 관리하는 `docker-compose.yaml` 하나를 사용합니다. UGREEN Docker 앱과 NAS 터미널 모두 같은 파일을 사용합니다.**
접속 경로: `https://hop.noname.company → 기존 Cloudflare Tunnel → NPM(기존 8080) → NAS 내부 IP:50189 → hop.`
아래 코드 블록을 하나씩 복사하고 Enter를 누르세요. 오류가 나오면 다음 단계로 넘어가지 마세요.
파일은 hop 폴더 안의 `uploads`에 저장합니다. 이 폴더로 전환한 설치는 **7. 다음 버전으로 업데이트**만 진행합니다. 이전 `hop_hop_uploads` 볼륨을 사용하는 설치의 최초 전환은 **7-1. 업로드 폴더 전환과 테스트 자료 초기화**를 따릅니다. 사용자가 삭제를 허용한 기존 테스트 자료의 초기화는 최초 전환 때 한 번만 하며 `.env`, 도메인·포트·메일 설정은 유지합니다.
파일은 hop 폴더 안의 `uploads`, DB는 기존 `hop_hop_database` 볼륨에 저장합니다. 현재 정상 운영 중인 설치에서 Compose 파일만 통합할 때는 **7-0. 기존 파일을 단일 운영 파일로 전환**을 한 번 진행합니다. 이후에는 **7. 다음 버전으로 업데이트**만 진행하며 계정·파일·DB를 초기화하지 않습니다.
**‘허용되지 않은 접속 경로’가 나오면:** 주소창을 `https://hop.noname.company`로 바꾸세요. `http://`는 운영 `APP_URL`과 달라 차단됩니다. 5-1단계에서 HTTP를 HTTPS로 자동 이동시킵니다.
@@ -60,25 +60,26 @@ sudo sh scripts/configure-nas.sh
`SESSION_SECRET`과 `DISCOVERY_SECRET`은 자동 생성됩니다. NAS 포트와 컨테이너 포트는 모두 **50189**를 사용합니다.
마지막에 표시된 `compose.nas.yaml` 경로를 복사해 둡니다. `.env`는 NAS에만 남고 Git에 올라가지 않습니다. 기존 `.env`가 있으면 덮어쓰지 않습니다.
마지막에 표시된 `docker-compose.yaml` 경로를 복사해 둡니다. `.env`는 NAS에만 남고 Git에 올라가지 않습니다. 기존 `.env`의 비밀값은 유지합니다. 이 스크립트는 `.env`와 저장 권한을 준비하고 Compose 문법을 검사하며, Git에서 관리하는 Compose 파일을 생성하거나 덮어쓰지 않습니다.
`compose.yaml`은 Git에서 관리하는 설정 원본이고, `compose.nas.yaml`은 NAS의 절대 경로와 설정을 채워 만든 가져오기용 파일입니다. 유그린 Docker 앱에 내용을 옮겨 저장하면 실제 프로젝트 구성은 hop 폴더의 **`docker-compose.yaml`**에 저장됩니다. 앱에서 수정한 최종 설정을 기준으로 업데이트하려면 이 파일을 사용합니다. 가져오기용 파일만 다시 생성해도 앱의 최종 설정은 자동으로 바뀌지 않습니다.
## 3. Docker 앱과 터미널에서 같은 파일로 배포
## 3. Docker 앱에서 배포
1. NAS의 **Docker 앱 → 프로젝트 → 생성** 누르기.
2. 프로젝트 이름은 **hop**, 프로젝트 폴더는 `/volume1/docker/projects/apps/hop`으로 지정.
3. **`docker-compose.yaml`** 선택. 앱 상단의 구성 파일 경로가 hop 폴더의 이 파일인지 확인.
4. 앱에서 **배포** 누르기. 기존 프로젝트의 시작·중지·로그 확인도 계속 이 프로젝트에서 진행합니다.
1. NAS의 **Docker 앱** 열기.
2. **프로젝트 → 생성** 누르기.
3. 프로젝트 이름에 **hop** 입력.
4. 프로젝트 저장 경로는 방금 `git clone`한 **hop 폴더** 선택.
5. YAML 가져오기에서 **compose.nas.yaml** 선택. 파일 가져오기가 없으면 해당 파일 내용을 Compose 편집기에 붙여넣습니다.
6. 앱에서 **배포** 누르기. 로그에 Dockerfile을 이용하는 빌드 단계가 나오는지 확인하고 완료될 때까지 기다립니다.
7. 프로젝트의 hop 컨테이너가 **실행 중**, 상태가 **healthy**인지 확인.
터미널에서 빌드·배포할 때도 hop 폴더에서 같은 파일을 사용합니다:
`hop.noname.company:latest`는 NAS에서 소스로 빌드하는 로컬 이미지입니다. Docker Hub에 등록된 이미지가 아니며 `docker login`은 필요하지 않습니다. 설정의 **`pull_policy: build`**는 기존 로컬 이미지가 있어도 소스로 다시 빌드하도록 지정합니다. `never`는 기존 로컬 이미지를 사용하는 방식이므로 새 소스의 자동 빌드를 지시하지 않습니다. [Docker 공식 문서](https://docs.docker.com/reference/compose-file/services/#pull_policy).
```sh
sudo docker compose -f docker-compose.yaml up -d --build
```
버전별로 버튼 이름이 다를 수 있습니다. 앱에서 **최신 이미지 다운로드**를 별도로 선택하면 외부에서 hop. 이미지를 찾다가 실패할 수 있습니다. 배포가 실제 빌드를 실행하는지는 로그에서 `Dockerfile`, `COPY`, `RUN`, `exporting` 등 빌드 단계를 확인하세요. `Running 0.0s`만으로 새 코드 적용을 확인할 수 없습니다. 유그린 앱에서 빌드를 생략하거나 설정을 덮어쓰면 아래 설정만으로는 해결되지 않으므로 사용 중인 배포 화면과 로그를 확인해야 합니다.
`name: hop`은 기존 프로젝트를 지정합니다. 컨테이너는 `hop`, 네트워크는 `hop_hop_network`, DB 볼륨은 `hop_hop_database`를 그대로 사용합니다. 포트·주소·UID/GID는 기존 `.env` 값을 읽으며, 현재 NAS의 값은 `192.168.50.146:50189:50189`와 `1000:10`입니다. `build.context: .`, `env_file: .env`, `./uploads`는 Compose 파일이 있는 hop 폴더를 기준으로 해석하므로 NAS의 기존 절대 경로와 같습니다.
생성한 YAML은 소스와 `.env`의 절대 경로를 사용합니다. 별도의 빈 폴더로 소스를 옮기지 마세요. 프로젝트 관리 방식은 [UGREEN 프로젝트 공식 안내](https://support.ugnas.com/detail/article/en-US/411)를 참고했습니다.
`hop.noname.company:latest`는 소스로 빌드하는 로컬 이미지이며 Docker Hub에 등록된 이미지가 아닙니다. **`pull_policy: build`**와 `--build`로 소스를 빌드합니다. 앱의 **최신 이미지 다운로드**는 다른 동작이므로 외부에서 이미지를 찾다가 실패할 수 있습니다. [Docker 공식 문서](https://docs.docker.com/reference/compose-file/services/#pull_policy).
Git으로 받은 파일을 운영 기준으로 사용합니다. 앱 편집기가 YAML을 저장하면서 형식을 바꾼 경우 `git diff -- docker-compose.yaml`로 변경 내용을 확인하세요. 필요한 운영 변경은 이 파일에 반영해 Git에도 함께 기록합니다. 프로젝트 관리 방식은 [UGREEN 프로젝트 공식 안내](https://support.ugnas.com/detail/article/en-US/411)를 참고했습니다.
## 4. Nginx Proxy Manager 설정
@@ -155,12 +156,15 @@ sh scripts/enable-nas-discovery.sh
이 명령은 **현재 운영 주소를 통한 실제 요청**으로 인접 프록시와 Cloudflare 원본 IP 전달을 확인하고, 그 프록시 IP만 `.env`에 지정합니다. 운영 진단 토큰은 설정 스크립트가 생성하며 화면이나 Git에 공개하지 않습니다.
1. 명령이 성공하면 Docker 앱 → **프로젝트 → hop → 편집**.
2. **compose.nas.yaml** 다시 불러오기 → 저장.
3. **재배포**하여 컨테이너를 새 환경설정으로 다시 생성. 단순 재시작은 환경값을 다시 읽지 않습니다.
4. PC와 휴대폰을 같은 Wi-Fi에 연결하고 `https://hop.noname.company`를 열기.
5. QR 없이 상대 기기가 표시되는지 확인.
6. 휴대폰 Wi-Fi를 끄고 모바일 데이터로 전환 → 다른 네트워크 기기가 목록에서 사라지는지 확인.
1. 명령이 성공하면 같은 파일로 새 `.env` 설정을 적용합니다:
```sh
sudo docker compose -f docker-compose.yaml up -d --build
```
2. PC와 휴대폰을 같은 Wi-Fi에 연결하고 `https://hop.noname.company`를 열기.
3. QR 없이 상대 기기가 표시되는지 확인.
4. 휴대폰 Wi-Fi를 끄고 모바일 데이터로 전환 → 다른 네트워크 기기가 목록에서 사라지는지 확인.
확인 중 오류가 나면 자동 발견 설정을 임의로 넓히지 마세요. 오류와 Docker 로그를 알려주세요. 주변 목록은 같은 **공인 IP** 기준이며 Bluetooth 탐색은 아닙니다. IPv6 주소가 서로 다르거나 VPN을 사용하면 같은 Wi-Fi에서도 자동 목록이 다를 수 있습니다.
@@ -230,108 +234,111 @@ NAS 파일 관리자에서 **공유 폴더 → docker → projects → apps →
## 7. 다음 버전으로 업데이트
**일반적인 코드·UI 업데이트는 아래 순서만 진행하세요. 기존 YAML·환경변수·볼륨은 그대로 사용합니다.**
**소스 코드와 운영 Compose 설정을 Git에서 함께 받습니다. 같은 파일로 빌드·재배포하며 기존 DB·업로드는 유지합니다.**
1. SSH에서 hop 폴더로 이동:
1. hop 폴더로 이동:
```sh
cd /volume1/docker/projects/apps/hop
```
2. 최신 코드 받기:
2. 최신 코드와 설정 받기:
```sh
git pull --ff-only origin main
```
3. Docker 앱 → **프로젝트 → hop → 재배포**. 앱에 저장된 최종 `docker-compose.yaml`에서 `pull_policy: build`인지 확인합니다. 이전 `never` 설정을 쓰고 있다면 이번에 한 번 `build`로 변경합니다.
4. 실제 빌드 단계가 로그에 나오는지 확인하고 완료될 때까지 기다립니다. **최신 이미지 다운로드**는 소스 빌드와 다른 동작입니다.
5. `healthy` 확인 → 브라우저 새로고침. PWA는 앱을 닫고 다시 열기.
이 프로젝트의 작업 순서는 코드·설정 수정 및 Git 업로드 → Compose 변경이 있으면 `compose.nas.yaml` 내용 안내 → 사용자가 앱의 `docker-compose.yaml`에 옮겨 빌드·재배포입니다. 앱 버전에 따라 빌드가 생략되거나 다운로드 옵션이 설정을 덮어쓸 수 있으므로 실제 로그로 확인합니다.
앱에서 소스 빌드를 사용할 수 없거나 이전 코드가 계속 실행되는 경우에만, SSH에서 최종 설정으로 빌드와 컨테이너 교체를 함께 실행할 수 있습니다:
3. 빌드·재배포. 완료될 때까지 기다립니다:
```sh
sudo docker compose --project-name hop -f docker-compose.yaml up -d --build --force-recreate --wait --wait-timeout 90 hop
sudo docker compose -f docker-compose.yaml up -d --build
```
현재 인증 코드 적용 여부는 아래 명령으로 확인합니다. `8`이면 비밀번호 최소 8자 변경이 반영됐고, `12`이면 이전 코드입니다:
4. 상태 확인:
```sh
sudo docker exec hop node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/api/auth/me').then(r=>r.json()).then(v=>console.log(v.passwordMinLength))"
sudo docker compose -f docker-compose.yaml ps
```
**저장소 전환을 완료한 뒤의 코드·UI 업데이트는 이 순서만 진행합니다.** 설정 스크립트를 다시 실행하거나 YAML을 다시 불러올 필요가 없습니다. NPM·Cloudflare Tunnel 설정도 그대로 유지합니다. 위치 검색은 양쪽 브라우저에서 위치 권한을 허용하면 자동으로 동작합니다.
5. 브라우저 새로고침. PWA는 앱을 닫고 다시 열기.
DB는 `hop_hop_database` 볼륨, 파일은 hop 폴더의 `uploads`에 있습니다. `uploads`, `.env`, 생성된 `compose.nas.yaml`, 앱이 저장한 `docker-compose.yaml`은 Git에서 제외하므로 일반 코드 업데이트로 덮어쓰지 않습니다. 같은 저장 경로를 사용하는 재배포에서 데이터는 유지됩니다.
일반 업데이트에는 설정 스크립트 실행·YAML 재생성·수동 복사가 필요 없습니다. UGREEN 앱도 같은 `docker-compose.yaml`을 사용하며 프로젝트 관리 기능은 계속 사용할 수 있습니다. NPM·Cloudflare Tunnel 설정은 그대로 유지합니다.
### 7-1. 업로드 폴더 전환과 테스트 자료 초기화
Git pull이 로컬 Compose 변경 때문에 중단되면 `git diff -- docker-compose.yaml`로 차이를 먼저 확인합니다. 앱에서 필요한 운영 설정을 바꾼 경우 그 변경을 Git 설정과 함께 정리하며, 자료나 설정을 강제로 초기화하지 않습니다.
**이번에 한 번만 실행합니다. 기존 테스트 계정·기기 이름·방·전송 기록·보관 파일은 모두 삭제합니다. `.env`와 소스는 유지합니다.** 다음 업데이트에서 반복하지 마세요.
### 7-0. 기존 파일을 단일 운영 파일로 전환
1. Docker 앱 → 프로젝트 → hop → **중지**.
2. SSH에서 hop 폴더로 이동:
**이번에 한 번만 진행합니다. 기존 컨테이너·네트워크·DB 볼륨·업로드·계정을 삭제하지 않습니다. `.env`도 그대로 사용합니다.**
1. hop 폴더로 이동:
```sh
cd /volume1/docker/projects/apps/hop
```
3. 최신 코드 받기:
2. 현재 앱이 만든 파일이 Git 미추적 파일인 경우에만 `/tmp`에 옮겨 둡니다. 현재 실행 중인 컨테이너의 데이터는 변경하지 않습니다:
```sh
if [ -f docker-compose.yaml ] && ! git ls-files --error-unmatch docker-compose.yaml >/dev/null 2>&1; then
sudo mv docker-compose.yaml /tmp/hop-compose.before-unify.yaml
fi
```
3. Git에서 단일 운영 파일 받기:
```sh
git pull --ff-only origin main
```
4. 중지된 hop 컨테이너 제거. 프로젝트 소스 폴더는 남습니다:
4. 기존 `.env` 값이 현재 운영 설정과 맞는지 확인합니다. 아래 명령은 비밀값을 출력하지 않습니다:
```sh
sudo docker rm hop
grep -E '^(APP_URL|HOP_BIND_IP|HOP_HOST_PORT|HOP_CONTAINER_PORT|HOP_RUN_UID|HOP_RUN_GID)=' .env
```
5. 기존 테스트 DB와 업로드 볼륨 삭제:
현재 NAS에서는 `APP_URL=https://hop.noname.company`, `HOP_BIND_IP=192.168.50.146`, 두 포트는 `50189`, UID/GID는 `1000`과 `10`을 사용합니다. 값이 비어 있거나 다르면 기존 운영 값에 맞춰 `.env`를 정리한 뒤 진행합니다. 이 전환에서는 `configure-nas.sh`나 저장소 초기화를 다시 실행하지 않습니다.
5. Compose 문법 검사. 정상일 때는 출력 없이 끝납니다:
```sh
sudo docker volume rm hop_hop_database hop_hop_uploads
sudo docker compose -f docker-compose.yaml config --quiet
```
6. 새 업로드 폴더·권한·YAML 준비. 기존 `.env`의 비밀값은 재발급하지 않습니다:
6. 같은 파일로 빌드·재배포:
```sh
sudo sh scripts/configure-nas.sh
sudo docker compose -f docker-compose.yaml up -d --build
```
7. Docker 앱 → 기존 hop 프로젝트의 **편집 / Compose 설정**에서 새 `compose.nas.yaml`을 다시 불러옵니다. 파일 가져오기가 없으면 아래 내용을 Compose 편집기에 붙여넣습니다:
7. UGREEN 앱 상단의 구성 파일 경로가 `/volume1/docker/projects/apps/hop/docker-compose.yaml`인지 확인합니다. 이후에는 파일 내용을 다른 Compose 파일로 옮기지 않습니다.
8. 더 이상 사용하지 않는 NAS 생성 파일만 정리합니다. 기존 `compose.yaml`은 Git 업데이트에서 제거됩니다:
```sh
cat compose.nas.yaml
rm -f compose.nas.yaml
```
8. **이미지 재빌드 → 재배포 / 시작**. `healthy` 확인.
9. SSH에서 실제 파일 연결 경로 확인:
이후에는 위 7장의 일반 업데이트 명령만 사용합니다. `/tmp`에 옮긴 이전 설정은 전환 확인용이며, 운영에는 Git의 `docker-compose.yaml`만 사용합니다.
### 7-1. 저장 데이터 유지
DB는 기존 `hop_hop_database` 볼륨을 `/data/database`에 연결하며 `nocopy: true`를 유지합니다. 업로드는 같은 hop 폴더의 `uploads`를 `/data/uploads`에 연결합니다. 컨테이너 교체 시 같은 저장소를 사용합니다. `.env`, `uploads`, DB, 백업 파일은 Git에서 제외하고, **`docker-compose.yaml`은 Git에서 관리**합니다.
실제 업로드 연결 경로는 다음 명령으로 확인할 수 있습니다:
```sh
sudo docker inspect --format '{{range .Mounts}}{{if eq .Destination "/data/uploads"}}{{.Type}} → {{.Source}}{{end}}{{end}}' hop
```
다음처럼 나오면 완료입니다:
```text
bind → /volume1/docker/projects/apps/hop/uploads
```
10. 브라우저/PWA 새로고침 → 필요하면 다시 회원가입 → 작은 파일 전송 → NAS 파일 관리자에서 **hop → uploads → originals** 확인.
이후에는 Git pull → 이미지 재빌드 → 재배포만 진행합니다. `uploads` 폴더와 DB는 그대로 유지합니다.
현재 NAS에서는 `bind → /volume1/docker/projects/apps/hop/uploads`가 나와야 합니다. 이전 업로드 볼륨을 아직 사용 중인 다른 설치는 파일 보존 방법부터 확인한 뒤 전환합니다. 이번 Compose 정리는 기존 자료 초기화 작업이 아닙니다.
### 추가 작업이 필요한 업데이트
| 변경 내용 | 필요한 작업 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 코드·화면·CSS 변경 | Git pull → 이미지 재빌드 → 재배포 |
| Docker 구성·포트·볼륨 경로 변경 | Git pull → 설정/YAML 다시 생성 → Docker 앱에 새 YAML 불러오기 → 재빌드 → 재배포 |
| 환경변수 변경 | 필요한 값 수정 → 설정/YAML 다시 생성 → Docker 앱에 새 YAML 불러오기 → 재배포. 코드도 바뀌었다면 재빌드 |
| DB 구조·기존 데이터 변경 | 해당 버전의 변경 안내 확인 → 필요 시 아래 데이터 백업 → Git pull → 재빌드 → 재배포 |
| 변경 내용 | 필요한 작업 |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| 코드·화면·CSS 변경 | Git pull → 이미지 재빌드 → 재배포 |
| Docker 구성·포트·볼륨 경로 변경 | Git pull → 같은 `docker-compose.yaml`로 빌드·재배포. 저장 경로 변경은 기존 자료 연결부터 확인 |
| 환경변수 변경 | NAS의 `.env` 값 수정 → 같은 `docker-compose.yaml`로 재배포 |
| DB 구조·기존 데이터 변경 | 해당 버전의 변경 안내 확인 → 필요 시 아래 데이터 백업 → Git pull → 재빌드 → 재배포 |
YAML이나 DB 변경이 필요한 버전은 업데이트 안내에서 별도로 표시합니다. 코드·CSS 수정 때마다 데이터 백업이나 YAML 재생성을 반복할 필요는 없습니다.
@@ -372,12 +379,12 @@ DB 구조가 바뀐 뒤 이전 상태로 복구하려면 같은 시점의 코드
## 8. 시작 / 중지 / 로그
| 할 일 | 누를 위치 |
| -------------- | ----------------------------------------------------------------------- |
| 시작 | Docker → 프로젝트 → hop → 시작 |
| 중지 | Docker → 프로젝트 → hop → 중지 |
| 설정 변경 적용 | `.env` 수정 → YAML 재생성 → 프로젝트 편집 → YAML 다시 불러오기 → 재배포 |
| 오류 확인 | Docker → 컨테이너 → hop 컨테이너 → 로그 |
| 할 일 | 누를 위치 |
| -------------- | ----------------------------------------------------------------- |
| 시작 | Docker → 프로젝트 → hop → 시작 |
| 중지 | Docker → 프로젝트 → hop → 중지 |
| 설정 변경 적용 | `.env` 또는 Git의 `docker-compose.yaml` 수정 → 같은 파일로 재배포 |
| 오류 확인 | Docker → 컨테이너 → hop 컨테이너 → 로그 |
SSH에서 실제 설정 중 비밀값 없이 주소·포트만 확인하려면:
@@ -394,7 +401,7 @@ curl -fsS "http://${HOP_NAS_IP}:50189/api/health"
HTTP 주소는 상태 확인에만 씁니다. 실제 접속·QR·로그인은 `https://hop.noname.company`를 사용하세요.
환경파일을 YAML에 포함하지 않는 생성 방식은 [Docker Compose 공식 문서](https://docs.docker.com/reference/cli/docker/compose/config/)의 `--no-env-resolution`을 사용합니다. `.env`, 생성된 `compose.nas.yaml`, 앱이 저장한 `docker-compose.yaml`, `backups`는 Git과 Docker 빌드에서 제외됩니다.
`docker-compose.yaml`에는 `.env`의 경로와 환경변수 참조만 기록합니다. 비밀값을 YAML에 펼쳐 저장하지 않습니다. 설정 스크립트는 [Docker Compose 공식 문서](https://docs.docker.com/reference/cli/docker/compose/config/)의 `--no-env-resolution --quiet`로 문법만 검사합니다. `.env`, `uploads`, `backups`는 Git과 Docker 빌드에서 제외됩니다.
## 이미지 다운로드 오류가 나올 때
@@ -408,13 +415,13 @@ pull_policy: build
이전 `never` 설정이면 `build`로 변경하고 재배포합니다. `build` 경로는 `/volume1/docker/projects/apps/hop`, Dockerfile은 `Dockerfile`을 그대로 사용합니다. 기존 DB·업로드 저장 경로도 그대로 유지합니다. 새 코드가 NAS 폴더에 반영돼 있어야 합니다.
다시 실행해도 `Running 0.0s`와 다운로드 오류만 나오면 실제 소스 빌드가 확인되지 않은 상태입니다. 7장의 SSH 명령은 앱에서 빌드를 사용할 수 없는 경우의 대안입니다. 먼저 배포 화면과 전체 로그를 확인하세요. `docker login`이나 볼륨 삭제는 필요 없습니다.
다시 실행해도 `Running 0.0s`와 다운로드 오류만 나오면 실제 소스 빌드가 확인되지 않은 상태입니다. 7장의 `sudo docker compose -f docker-compose.yaml up -d --build`는 같은 운영 파일로 소스를 빌드·배포하는 명령입니다. 먼저 배포 화면과 전체 로그를 확인하세요. `docker login`이나 볼륨 삭제는 필요 없습니다.
## 초대 코드와 모바일 화면
새 초대 코드는 기본 4자리입니다. 기존 `.env`를 수정하지 않아도 새 이미지에서 적용됩니다. 업데이트 전에 발급한 8자리 코드와 QR은 원래 만료 시간까지 유효합니다.
나중에 초대 코드를 6자리로 바꾸려면 `.env`에 `SPACE_INVITE_CODE_LENGTH=6`을 넣고 YAML 다시 생성 → 프로젝트 편집 → YAML 다시 불러오기 → 재배포합니다. DB나 업로드 볼륨은 그대로 사용합니다.
나중에 초대 코드를 6자리로 바꾸려면 `.env`에 `SPACE_INVITE_CODE_LENGTH=6`을 넣고 같은 `docker-compose.yaml`로 재배포합니다. DB나 업로드 볼륨은 그대로 사용합니다.
모바일 기기 설정은 하단 시트로 열리고 입력창을 자동으로 선택하지 않습니다. 사용자가 입력창을 누르면 키보드가 열린 가시 영역에 맞춰 시트 높이를 조정합니다. 로그인·기기명·URL 등 입력 글자는 16px 이상으로 적용하며 화면 확대 기능 자체를 차단하지 않습니다. 파일 전송 중에는 선택한 파일명 아래에 진행률을 표시합니다.
@@ -434,4 +441,4 @@ pull_policy: build
가입 비밀번호는 최소 8자입니다. 인증 메일의 링크를 열고 **이메일 인증 완료하기**를 누르면 해당 계정으로 자동 로그인하며 보관함의 받은 항목을 표시합니다. 다른 계정으로 로그인한 브라우저에서는 이전 세션을 종료하고 인증한 계정으로 전환합니다. 다른 기기의 로그인은 유지합니다. 만료·재사용된 링크로는 현재 로그인을 변경하지 않습니다.
이번 인증 변경은 **Git pull → 이미지 재빌드 → 재배포**로 적용합니다. 기존 계정·파일은 유지하고 YAML·환경변수·DB 구조는 변경하지 않습니다. 업로드 폴더 전환을 완료했다면 7-1의 초기화를 반복하지 마세요.
이번 인증 변경은 **Git pull → 이미지 재빌드 → 재배포**로 적용합니다. 기존 계정·파일은 유지하고 YAML·환경변수·DB 구조는 변경하지 않습니다. 기존 DB·업로드를 초기화하지 않습니다.

View File

@@ -1,3 +1,4 @@
name: hop
services:
hop:
image: hop.noname.company:latest
@@ -7,7 +8,7 @@ services:
context: .
dockerfile: Dockerfile
container_name: hop
user: '${HOP_RUN_UID:-1000}:${HOP_RUN_GID:-1000}'
user: '${HOP_RUN_UID:-1000}:${HOP_RUN_GID:-10}'
restart: unless-stopped
env_file: .env
environment:
@@ -41,4 +42,5 @@ volumes:
name: hop_hop_database
networks:
hop_network:
name: hop_hop_network
driver: bridge

View File

@@ -154,3 +154,9 @@
- 새 인증 테스트 10개를 포함한 API 총 98개 통과. 7자 거절/8자 허용, 자동 로그인·HttpOnly/Secure/SameSite 쿠키, 게스트 기기 유지, 이전 쿠키·소켓 무효화와 보관함 격리, 다른 기기 세션 유지, 만료·미등록·정지·삭제 계정·동시/재사용 토큰 거절, 세션 발급 실패 시 전체 롤백과 재시도를 확인했다. 기존 오프라인 검사는 35ms 대기 전에 실제 서버의 소켓 종료를 기다리도록 안정화했다.
- PC/모바일 실제 Chromium에서 7자 가입 차단·8자 가입·인증 후 추가 로그인 없이 보관함 진입을 확인했다. 다른 계정의 인증 링크를 같은 브라우저에서 열고 인증한 계정으로 전환·이전 쿠키 401·기존 자료 미노출·인증 토큰의 주소창 제거를 확인했다. 지연된 이전 계정 조회를 재현하고 순차 조회 큐가 최종 계정을 유지하는지 검사했다. 기존 보관함·수동 파일 삭제·익명 파일/링크 전송도 통과했다.
- 전체 빌드·타입 검사와 운영 Docker 빌드를 통과했다. 격리한 운영 컨테이너에서도 보호된 인증 쿠키·자동 로그인·이전 세션 무효화·토큰 재사용 거절·재시작 후 로그인 유지, 기존 파일/텍스트 보존·조각 이어 보내기·SQLite integrity를 확인했다. Docker 인증 토큰은 테스트 DB에 생성한 것으로 실제 Resend 발송과 NAS 배포는 수행하지 않았다. 코드 업데이트에 DB 초기화·YAML 재생성은 필요 없다.
## 2026-10-14 운영 Compose 파일 통합
- Git에서 관리하는 `docker-compose.yaml` 하나로 통합했다. 중복 설정 파일을 제거하고 설정·자동 발견 스크립트와 배포 문서에서 같은 파일을 사용한다. 초기 설정은 `.env`와 저장 권한만 준비하며 Compose 파일의 내용을 변경하지 않는다.
- 현재 NAS 값으로 이전 설정과 새 파일을 실제 Docker Compose로 해석해 전체 설정이 동일한지 비교했다. 프로젝트 `hop`, 컨테이너 `hop`, 네트워크 `hop_hop_network`, DB `hop_hop_database`, 사용자 `1000:10`, 포트 `192.168.50.146:50189:50189`, 빌드 정책, 소스·환경파일·업로드 경로와 재시작·로그 설정이 동일했다. 새 파일은 `name: hop`으로 별도 `-p` 없이 기존 프로젝트를 지정한다.
- NAS 설정 검증에서 Compose 파일 미변경, 비밀값 유지, 환경파일 권한, 기존 저장소 식별자, `nocopy`, 실행 중 권한 변경 거절을 확인했다. 기존 운영 이미지로 격리한 실제 Compose 컨테이너를 시작해 health, UID/GID, 임시 DB 볼륨과 업로드 바인드 연결을 확인하고 테스트 자원을 정리했다. 셸 문법·포맷 검사도 통과했다. 운영 NAS의 컨테이너·볼륨·자료에는 접근하거나 재배포하지 않았다.

View File

@@ -14,7 +14,7 @@
"start": "npm run start -w @hop/server",
"db:generate": "npm run db:generate -w @hop/server",
"test:e2e": "node --import tsx tests/e2e/phase1.mjs",
"format": "prettier --write apps packages scripts tests compose.yaml Dockerfile package.json tsconfig.base.json docs README.md .env.example --ignore-unknown",
"format": "prettier --write apps packages scripts tests docker-compose.yaml Dockerfile package.json tsconfig.base.json docs README.md .env.example --ignore-unknown",
"test:e2e:account": "node --import tsx tests/e2e/phase3.mjs",
"test:e2e:devices": "node --import tsx tests/e2e/device-menu.mjs",
"test:e2e:layout": "node --import tsx tests/e2e/layout.mjs",

View File

@@ -5,12 +5,10 @@ cd "$(dirname "$0")/.."
umask 077
hop_env_tmp=''
hop_yaml_tmp=''
hop_echo_hidden=false
cleanup() {
if [ "$hop_echo_hidden" = true ]; then stty echo; fi
if [ -n "$hop_env_tmp" ]; then rm -f "$hop_env_tmp"; fi
if [ -n "$hop_yaml_tmp" ]; then rm -f "$hop_yaml_tmp"; fi
}
trap cleanup 0
trap 'exit 130' INT
@@ -114,14 +112,10 @@ if ! grep -Eq '^PROXY_DIAGNOSTIC_TOKEN=.{32,}$' .env; then
unset hop_diagnostic_token
fi
sh scripts/prepare-nas-storage.sh
hop_yaml_tmp=$(mktemp .compose.nas.XXXXXX)
docker compose --project-name hop --project-directory "$PWD" --env-file .env -f compose.yaml config --no-env-resolution > "$hop_yaml_tmp"
mv "$hop_yaml_tmp" compose.nas.yaml
hop_yaml_tmp=''
chmod 600 compose.nas.yaml
# sudo writes configuration as root; return it to the SSH user for Git/Docker app access.
docker compose -f docker-compose.yaml config --no-env-resolution --quiet
# sudo writes .env as root; return it to the SSH user. Never rewrite the tracked Compose file.
if [ "$(id -u)" = 0 ]; then
chown "$hop_run_uid:$hop_run_gid" .env compose.nas.yaml
chown "$hop_run_uid:$hop_run_gid" .env
fi
printf '\n%s\n' '완료. Docker 앱에서 프로젝트 이름 hop으로 아래 YAML을 불러오세요.'
printf '%s/compose.nas.yaml\n' "$PWD"
printf '\n%s\n' '완료. Git에서 관리하는 아래 파일을 Docker 앱과 터미널에서 함께 사용하세요.'
printf '%s/docker-compose.yaml\n' "$PWD"

View File

@@ -5,7 +5,7 @@ umask 077
hop_settings=$(mktemp .env.discovery.XXXXXX)
hop_environment=$(mktemp .env.discovery.XXXXXX)
trap 'rm -f "$hop_settings" "$hop_environment"' 0
hop_container=$(sudo docker compose --project-name hop -f compose.nas.yaml ps -q hop)
hop_container=$(sudo docker compose -f docker-compose.yaml ps -q hop)
if [ -z "$hop_container" ]; then
printf '%s\n' 'Docker 앱에서 새 버전의 hop 프로젝트를 먼저 시작하세요.' >&2
exit 1
@@ -19,5 +19,5 @@ awk '!/^(TRUSTED_PROXY_CIDRS|CLIENT_IP_SOURCE|SAME_NETWORK_DISCOVERY_ENABLED)=/'
cat "$hop_settings" >> "$hop_environment"
mv "$hop_environment" .env
chmod 600 .env
sh scripts/configure-nas.sh
printf '\n%s\n' '자동 발견 설정 완료. Docker 앱에서 compose.nas.yaml을 다시 불러와 재배포하세요.'
docker compose -f docker-compose.yaml config --no-env-resolution --quiet
printf '\n%s\n' '자동 발견 설정 완료. 같은 docker-compose.yaml로 재배포하면 새 .env 설정이 적용됩니다.'

View File

@@ -23,7 +23,9 @@ try {
mkdirSync(resolve(folder, 'bin'));
for (const name of ['configure-nas.sh', 'prepare-nas-storage.sh'])
cpSync(resolve(root, 'scripts', name), resolve(folder, 'scripts', name));
cpSync(resolve(root, 'compose.yaml'), resolve(folder, 'compose.yaml'));
const composeFile = resolve(folder, 'docker-compose.yaml');
cpSync(resolve(root, 'docker-compose.yaml'), composeFile);
const originalCompose = readFileSync(composeFile, 'utf8');
const envFile = resolve(folder, '.env');
writeFileSync(
envFile,
@@ -74,16 +76,20 @@ esac
assert(updated.includes('SESSION_SECRET=keep-test-session-value-1234567890'));
assert(updated.includes('DISCOVERY_SECRET=keep-test-discovery-value-1234567890'));
assert.equal(statSync(envFile).mode & 0o777, 0o600);
const yaml = readFileSync(resolve(folder, 'compose.nas.yaml'), 'utf8');
const yaml = readFileSync(composeFile, 'utf8');
assert.equal(yaml, originalCompose, 'NAS setup must not overwrite the tracked Compose file');
assert(!yaml.includes('keep-test-session-value'));
const config = JSON.parse(
execFileSync(
realDocker,
['compose', '-f', 'compose.nas.yaml', 'config', '--no-env-resolution', '--format', 'json'],
['compose', '-f', 'docker-compose.yaml', 'config', '--no-env-resolution', '--format', 'json'],
{ cwd: folder, encoding: 'utf8' },
),
);
assert.equal(config.services.hop.user, '12001:12002');
assert.equal(config.name, 'hop');
assert.equal(config.services.hop.container_name, 'hop');
assert.equal(config.networks.hop_network.name, 'hop_hop_network');
const files = config.services.hop.volumes.find((mount) => mount.target === '/data/uploads');
assert.equal(files.type, 'bind');
assert.equal(files.source, realpathSync(resolve(folder, 'uploads')));
@@ -178,7 +184,7 @@ esac
);
}
console.log(
'PASS: NAS UID/GID, secret preservation, private env file, absolute uploads bind path, no DB copy-up, stopped-container guard, no implicit data deletion, uploads excluded from Git/build. Compose parsed with real Docker; storage mutations isolated through a test shim.',
'PASS: single tracked Compose unchanged by setup, existing project/container/network/database identities, NAS UID/GID, secret preservation, private env file, resolved uploads bind path, no DB copy-up, stopped-container guard, no implicit data deletion, uploads excluded from Git/build. Compose parsed with real Docker; storage mutations isolated through a test shim.',
);
} finally {
if (checkedContainer) {