feat(image-lab): Dockerfile + compose entry + scripts 6위치 + nginx 차단
Task 5 of Video Studio backend plan. Wires image-lab Python code (T1-T4)
into NAS Docker infrastructure on port 18802.
- image-lab/Dockerfile (python:3.12-slim + uvicorn)
- image-lab/requirements.txt (fastapi, redis, httpx)
- image-lab/env.example (INTERNAL_API_KEY, IMAGE_DATA_DIR, REDIS_URL, CORS)
- docker-compose.yml: image-lab service block (port 18802, redis depends_on,
healthcheck, volume ${RUNTIME_PATH}/image-data:/app/data) + frontend
depends_on entry
- scripts/deploy-nas.sh: SERVICES += image-lab
- scripts/deploy.sh: BUILD_TARGETS/CONTAINER_NAMES/HEALTH_ENDPOINTS += image-lab,
DATA_DIRS += image
- nginx/default.conf: /api/internal/image/ 3-layer block (IP allowlist +
deny all + X-Internal-Key forward) mirroring /api/internal/video/
Plan-B-Video lesson: 6-location registration enforced per
feedback_nas_deploy_paths.md rule 3 to avoid 'transferring dockerfile: 2B'
deploy failure.
Tests: image-lab pytest 11 passed (no regression).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,27 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
|
image-lab:
|
||||||
|
build: ./image-lab
|
||||||
|
container_name: image-lab
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "18802:8000"
|
||||||
|
environment:
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
|
||||||
|
- IMAGE_DATA_DIR=/app/data
|
||||||
|
- CORS_ALLOW_ORIGINS=http://localhost:3007,http://localhost:8080
|
||||||
|
volumes:
|
||||||
|
- ${RUNTIME_PATH}/image-data:/app/data
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||||
|
interval: 60s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
insta-lab:
|
insta-lab:
|
||||||
build:
|
build:
|
||||||
context: ./insta-lab
|
context: ./insta-lab
|
||||||
@@ -289,6 +310,7 @@ services:
|
|||||||
- packs-lab
|
- packs-lab
|
||||||
- travel-proxy
|
- travel-proxy
|
||||||
- video-lab
|
- video-lab
|
||||||
|
- image-lab
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
7
image-lab/Dockerfile
Normal file
7
image-lab/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY app ./app
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
||||||
4
image-lab/env.example
Normal file
4
image-lab/env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
INTERNAL_API_KEY=replace-me
|
||||||
|
IMAGE_DATA_DIR=/app/data
|
||||||
|
CORS_ALLOW_ORIGINS=http://localhost:3007,http://localhost:8080
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
5
image-lab/requirements.txt
Normal file
5
image-lab/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.115.0
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
pydantic==2.9.2
|
||||||
|
redis==5.0.8
|
||||||
|
httpx==0.27.2
|
||||||
@@ -276,6 +276,26 @@ server {
|
|||||||
proxy_pass http://$video_internal_backend$request_uri;
|
proxy_pass http://$video_internal_backend$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Video Studio — Windows image-render → NAS image-lab internal webhook
|
||||||
|
# Layer 1·2: nginx IP 화이트리스트 (LAN + Tailscale)
|
||||||
|
# Layer 3: X-Internal-Key (FastAPI dependency)
|
||||||
|
location /api/internal/image/ {
|
||||||
|
allow 192.168.45.0/24; # LAN 화이트리스트
|
||||||
|
allow 100.64.0.0/10; # Tailscale CGNAT
|
||||||
|
allow 127.0.0.1; # NAS 내부
|
||||||
|
deny all;
|
||||||
|
|
||||||
|
resolver 127.0.0.11 valid=10s;
|
||||||
|
set $image_internal_backend image-lab:8000;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Internal-Key $http_x_internal_key;
|
||||||
|
proxy_pass http://$image_internal_backend$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
# portfolio API (Stock) — trailing slash 유무 모두 매칭
|
# portfolio API (Stock) — trailing slash 유무 모두 매칭
|
||||||
location /api/portfolio {
|
location /api/portfolio {
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── 서비스 목록 (한 곳에서만 관리) ──
|
# ── 서비스 목록 (한 곳에서만 관리) ──
|
||||||
SERVICES="lotto travel-proxy deployer stock music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab nginx scripts"
|
SERVICES="lotto travel-proxy deployer stock music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab image-lab nginx scripts"
|
||||||
|
|
||||||
# 1. 자동 감지: Docker 컨테이너 내부인가?
|
# 1. 자동 감지: Docker 컨테이너 내부인가?
|
||||||
if [ -d "/repo" ] && [ -d "/runtime" ]; then
|
if [ -d "/repo" ] && [ -d "/runtime" ]; then
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ flock -n 200 || { echo "Deploy already running, skipping"; exit 0; }
|
|||||||
|
|
||||||
# ── 서비스 목록 (한 곳에서만 관리) ──
|
# ── 서비스 목록 (한 곳에서만 관리) ──
|
||||||
# docker compose 서비스명 (deployer 제외 — 자기 자신을 재빌드하면 스크립트 중단)
|
# docker compose 서비스명 (deployer 제외 — 자기 자신을 재빌드하면 스크립트 중단)
|
||||||
BUILD_TARGETS="lotto travel-proxy stock music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab frontend"
|
BUILD_TARGETS="lotto travel-proxy stock music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab image-lab frontend"
|
||||||
# 컨테이너 이름 (고아 정리용 — blog-lab은 폐기 대상으로 정리 리스트에 유지)
|
# 컨테이너 이름 (고아 정리용 — blog-lab은 폐기 대상으로 정리 리스트에 유지)
|
||||||
CONTAINER_NAMES="lotto stock music-lab insta-lab blog-lab realestate-lab agent-office personal packs-lab travel-proxy video-lab frontend"
|
CONTAINER_NAMES="lotto stock music-lab insta-lab blog-lab realestate-lab agent-office personal packs-lab travel-proxy video-lab image-lab frontend"
|
||||||
# Infra 서비스 (image-based, 영속 데이터 보존을 위해 stop/rm 없이 up만)
|
# Infra 서비스 (image-based, 영속 데이터 보존을 위해 stop/rm 없이 up만)
|
||||||
INFRA_SERVICES="redis"
|
INFRA_SERVICES="redis"
|
||||||
# 헬스체크 대상
|
# 헬스체크 대상
|
||||||
HEALTH_ENDPOINTS="lotto stock travel-proxy music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab redis"
|
HEALTH_ENDPOINTS="lotto stock travel-proxy music-lab insta-lab realestate-lab agent-office personal packs-lab video-lab image-lab redis"
|
||||||
# data 디렉토리 (packs-lab은 별도 media/packs 사용)
|
# data 디렉토리 (packs-lab은 별도 media/packs 사용)
|
||||||
DATA_DIRS="music stock insta realestate agent-office personal video"
|
DATA_DIRS="music stock insta realestate agent-office personal video image"
|
||||||
|
|
||||||
# 1. 자동 감지: Docker 컨테이너 내부인가?
|
# 1. 자동 감지: Docker 컨테이너 내부인가?
|
||||||
if [ -d "/repo" ] && [ -d "/runtime" ]; then
|
if [ -d "/repo" ] && [ -d "/runtime" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user