image service
This commit is contained in:
@@ -65,6 +65,13 @@ GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED=true
|
|||||||
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS=7
|
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS=7
|
||||||
GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT=
|
GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT=
|
||||||
|
|
||||||
|
# Shared image service. The two secret files must contain the values configured
|
||||||
|
# for the core2026 caller on the image server and must not be committed.
|
||||||
|
IMAGE_SERVICE_URL=https://sam-image.hided.net
|
||||||
|
IMAGE_PUBLIC_URL=https://sam-image.hided.net
|
||||||
|
IMAGE_UPLOAD_CORE2026_SECRET_FILE=./secrets/image_upload_core2026_secret
|
||||||
|
IMAGE_SYNC_CORE2026_SECRET_FILE=./secrets/image_sync_core2026_secret
|
||||||
|
|
||||||
# Initial stopped profile inventory. Each deployment can choose its own branch in the Admin GUI.
|
# Initial stopped profile inventory. Each deployment can choose its own branch in the Admin GUI.
|
||||||
BOOTSTRAP_PROFILES=gateway,che,kwe,pwe,twe,nya,pya,hwe
|
BOOTSTRAP_PROFILES=gateway,che,kwe,pwe,twe,nya,pya,hwe
|
||||||
TZ=Asia/Seoul
|
TZ=Asia/Seoul
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ password 24자, game/bootstrap token 32자, 최초 관리자 password 16자입
|
|||||||
시즌을 보존하고, `DB 초기화 배포`는 scenario를 다시 seed합니다. Gateway 자체의
|
시즌을 보존하고, `DB 초기화 배포`는 scenario를 다시 seed합니다. Gateway 자체의
|
||||||
배포와 이전 commit rollback은 같은 화면의 별도 release 영역에서 처리됩니다.
|
배포와 이전 commit rollback은 같은 화면의 별도 release 영역에서 처리됩니다.
|
||||||
|
|
||||||
|
이미지 서비스 연동에는 서버 간 접근 URL `IMAGE_SERVICE_URL`, 브라우저 공개 URL
|
||||||
|
`IMAGE_PUBLIC_URL`과 서로 다른 두 secret 파일이 필요합니다. 업로드 secret은
|
||||||
|
Gateway 전용 아이콘과 game-api 편집기 첨부에만 사용하고, sync secret은 Gitea
|
||||||
|
webhook 누락 시 `docker compose exec runtime pnpm sync:image`로 현재 이미지
|
||||||
|
branch의 fast-forward를 요청할 때만 사용합니다. 두 파일은 runtime에 read-only로
|
||||||
|
mount되며 원문은 환경 변수나 브라우저 bundle에 들어가지 않습니다.
|
||||||
|
|
||||||
## 데이터와 복구 경계
|
## 데이터와 복구 경계
|
||||||
|
|
||||||
- PostgreSQL, Redis, Core clone/worktree, PM2 상태, Caddy 인증서와 user icon은
|
- PostgreSQL, Redis, Core clone/worktree, PM2 상태, Caddy 인증서와 user icon은
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
restart: "no"
|
||||||
|
redis:
|
||||||
|
restart: "no"
|
||||||
|
runtime:
|
||||||
|
environment:
|
||||||
|
CORE_SOURCE_MODE: bind
|
||||||
|
CORE_BIND_ROOT: /workspace/core2026
|
||||||
|
volumes:
|
||||||
|
- /home/letrhee/sam_rebuild/core2026:/workspace/core2026
|
||||||
|
- /home/letrhee/.cache/ms-playwright:/root/.cache/ms-playwright:ro
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- image-internal
|
||||||
|
restart: "no"
|
||||||
|
caddy:
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
image-internal:
|
||||||
|
external: true
|
||||||
|
name: sam-image_image-internal
|
||||||
@@ -73,6 +73,14 @@ services:
|
|||||||
GATEWAY_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${DOMAIN:?set DOMAIN in .env}/gateway/
|
GATEWAY_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${DOMAIN:?set DOMAIN in .env}/gateway/
|
||||||
GATEWAY_USER_ICON_DIR: /srv/data/user-icons
|
GATEWAY_USER_ICON_DIR: /srv/data/user-icons
|
||||||
GATEWAY_USER_ICON_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${DOMAIN:?set DOMAIN in .env}/gateway/api/user-icons
|
GATEWAY_USER_ICON_PUBLIC_URL: ${PUBLIC_SCHEME:-https}://${DOMAIN:?set DOMAIN in .env}/gateway/api/user-icons
|
||||||
|
GATEWAY_IMAGE_UPLOAD_URL: ${IMAGE_SERVICE_URL:-https://sam-image.hided.net}
|
||||||
|
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: /run/secrets/image_upload_core2026_secret
|
||||||
|
GATEWAY_SHARED_ICON_PUBLIC_URL: ${IMAGE_PUBLIC_URL:-https://sam-image.hided.net}/icons
|
||||||
|
GAME_IMAGE_UPLOAD_URL: ${IMAGE_SERVICE_URL:-https://sam-image.hided.net}
|
||||||
|
GAME_IMAGE_UPLOAD_SECRET_FILE: /run/secrets/image_upload_core2026_secret
|
||||||
|
GAME_CONTENT_IMAGE_PUBLIC_URL: ${IMAGE_PUBLIC_URL:-https://sam-image.hided.net}/uploads/core2026
|
||||||
|
IMAGE_SYNC_URL: ${IMAGE_SERVICE_URL:-https://sam-image.hided.net}
|
||||||
|
IMAGE_SYNC_SECRET_FILE: /run/secrets/image_sync_core2026_secret
|
||||||
GATEWAY_LOCAL_REGISTRATION_ENABLED: ${GATEWAY_LOCAL_REGISTRATION_ENABLED:-true}
|
GATEWAY_LOCAL_REGISTRATION_ENABLED: ${GATEWAY_LOCAL_REGISTRATION_ENABLED:-true}
|
||||||
GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED: ${GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED:-true}
|
GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED: ${GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED:-true}
|
||||||
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS: ${GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS:-7}
|
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS: ${GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS:-7}
|
||||||
@@ -99,6 +107,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- core-source:/srv/core
|
- core-source:/srv/core
|
||||||
- runtime-data:/srv/data
|
- runtime-data:/srv/data
|
||||||
|
- ${IMAGE_UPLOAD_CORE2026_SECRET_FILE:?set IMAGE_UPLOAD_CORE2026_SECRET_FILE in .env}:/run/secrets/image_upload_core2026_secret:ro
|
||||||
|
- ${IMAGE_SYNC_CORE2026_SECRET_FILE:?set IMAGE_SYNC_CORE2026_SECRET_FILE in .env}:/run/secrets/image_sync_core2026_secret:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -11,6 +11,34 @@ export const validateComposeModel = (model, mode) => {
|
|||||||
const runtime = model?.services?.runtime;
|
const runtime = model?.services?.runtime;
|
||||||
if (!runtime) return ['runtime service is missing'];
|
if (!runtime) return ['runtime service is missing'];
|
||||||
|
|
||||||
|
const requiredImageEnvironment = {
|
||||||
|
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
||||||
|
GAME_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
||||||
|
IMAGE_SYNC_SECRET_FILE: '/run/secrets/image_sync_core2026_secret',
|
||||||
|
};
|
||||||
|
for (const [key, expected] of Object.entries(requiredImageEnvironment)) {
|
||||||
|
if (runtime.environment?.[key] !== expected) errors.push(`${key} must be ${expected}`);
|
||||||
|
}
|
||||||
|
for (const key of [
|
||||||
|
'GATEWAY_IMAGE_UPLOAD_URL',
|
||||||
|
'GATEWAY_SHARED_ICON_PUBLIC_URL',
|
||||||
|
'GAME_IMAGE_UPLOAD_URL',
|
||||||
|
'GAME_CONTENT_IMAGE_PUBLIC_URL',
|
||||||
|
'IMAGE_SYNC_URL',
|
||||||
|
]) {
|
||||||
|
if (!runtime.environment?.[key]) errors.push(`${key} must be configured`);
|
||||||
|
}
|
||||||
|
const mounts = Array.isArray(runtime.volumes) ? runtime.volumes : [];
|
||||||
|
for (const target of [
|
||||||
|
'/run/secrets/image_upload_core2026_secret',
|
||||||
|
'/run/secrets/image_sync_core2026_secret',
|
||||||
|
]) {
|
||||||
|
const mount = mounts.find((candidate) => candidate?.target === target);
|
||||||
|
if (!mount || mount.type !== 'bind' || mount.read_only !== true) {
|
||||||
|
errors.push(`${target} must be a read-only bind mount`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!positive(runtime.mem_limit)) errors.push('runtime memory limit must be positive');
|
if (!positive(runtime.mem_limit)) errors.push('runtime memory limit must be positive');
|
||||||
if (!positive(runtime.memswap_limit)) errors.push('runtime memory+swap limit must be positive');
|
if (!positive(runtime.memswap_limit)) errors.push('runtime memory+swap limit must be positive');
|
||||||
if (Number(runtime.memswap_limit) !== Number(runtime.mem_limit)) {
|
if (Number(runtime.memswap_limit) !== Number(runtime.mem_limit)) {
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ if [ ! -f "$env_file" ]; then
|
|||||||
exit 66
|
exit 66
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
for key in IMAGE_UPLOAD_CORE2026_SECRET_FILE IMAGE_SYNC_CORE2026_SECRET_FILE; do
|
||||||
|
value=$(sed -n "s/^${key}=//p" "$env_file" | tail -n 1)
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
echo "$key must name a readable, non-empty secret file." >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
case "$value" in
|
||||||
|
/*) secret_path=$value ;;
|
||||||
|
*) secret_path=$stack_dir/$value ;;
|
||||||
|
esac
|
||||||
|
if [ ! -r "$secret_path" ] || [ ! -s "$secret_path" ]; then
|
||||||
|
echo "$key must name a readable, non-empty secret file." >&2
|
||||||
|
exit 66
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
docker run --rm --network=none --memory=128m --memory-swap=128m --cpus=1 --pids-limit=64 --env-file "$env_file" \
|
docker run --rm --network=none --memory=128m --memory-swap=128m --cpus=1 --pids-limit=64 --env-file "$env_file" \
|
||||||
-v "$stack_dir/runtime/validate-env.mjs:/opt/sammo/validate-env.mjs:ro" \
|
-v "$stack_dir/runtime/validate-env.mjs:/opt/sammo/validate-env.mjs:ro" \
|
||||||
node:24.18.0-bookworm-slim node /opt/sammo/validate-env.mjs
|
node:24.18.0-bookworm-slim node /opt/sammo/validate-env.mjs
|
||||||
|
|||||||
@@ -8,7 +8,29 @@ const safeRuntime = {
|
|||||||
CORE_SOURCE_MODE: 'clone',
|
CORE_SOURCE_MODE: 'clone',
|
||||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||||
RAYON_NUM_THREADS: '2',
|
RAYON_NUM_THREADS: '2',
|
||||||
|
GATEWAY_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net',
|
||||||
|
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
||||||
|
GATEWAY_SHARED_ICON_PUBLIC_URL: 'https://sam-image.hided.net/icons',
|
||||||
|
GAME_IMAGE_UPLOAD_URL: 'https://sam-image.hided.net',
|
||||||
|
GAME_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
||||||
|
GAME_CONTENT_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net/uploads/core2026',
|
||||||
|
IMAGE_SYNC_URL: 'https://sam-image.hided.net',
|
||||||
|
IMAGE_SYNC_SECRET_FILE: '/run/secrets/image_sync_core2026_secret',
|
||||||
},
|
},
|
||||||
|
volumes: [
|
||||||
|
{
|
||||||
|
type: 'bind',
|
||||||
|
source: '/secrets/image_upload_core2026_secret',
|
||||||
|
target: '/run/secrets/image_upload_core2026_secret',
|
||||||
|
read_only: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bind',
|
||||||
|
source: '/secrets/image_sync_core2026_secret',
|
||||||
|
target: '/run/secrets/image_sync_core2026_secret',
|
||||||
|
read_only: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
restart: 'unless-stopped',
|
restart: 'unless-stopped',
|
||||||
mem_limit: String(4 * 1024 * 1024 * 1024),
|
mem_limit: String(4 * 1024 * 1024 * 1024),
|
||||||
memswap_limit: String(4 * 1024 * 1024 * 1024),
|
memswap_limit: String(4 * 1024 * 1024 * 1024),
|
||||||
@@ -26,10 +48,9 @@ test('accepts a literal, non-restarting development model', () => {
|
|||||||
...safeRuntime,
|
...safeRuntime,
|
||||||
restart: 'no',
|
restart: 'no',
|
||||||
environment: {
|
environment: {
|
||||||
|
...safeRuntime.environment,
|
||||||
CORE_SOURCE_MODE: 'bind',
|
CORE_SOURCE_MODE: 'bind',
|
||||||
RUNTIME_MODE: 'development',
|
RUNTIME_MODE: 'development',
|
||||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
|
||||||
RAYON_NUM_THREADS: '2',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert.deepEqual(validateComposeModel({ services }, 'development'), []);
|
assert.deepEqual(validateComposeModel({ services }, 'development'), []);
|
||||||
|
|||||||
Reference in New Issue
Block a user