image service
This commit is contained in:
@@ -65,6 +65,13 @@ GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED=true
|
||||
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS=7
|
||||
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.
|
||||
BOOTSTRAP_PROFILES=gateway,che,kwe,pwe,twe,nya,pya,hwe
|
||||
TZ=Asia/Seoul
|
||||
|
||||
@@ -51,6 +51,13 @@ password 24자, game/bootstrap token 32자, 최초 관리자 password 16자입
|
||||
시즌을 보존하고, `DB 초기화 배포`는 scenario를 다시 seed합니다. Gateway 자체의
|
||||
배포와 이전 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은
|
||||
|
||||
@@ -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_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_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_ADMIN_LOCAL_ACCOUNT_ENABLED: ${GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED:-true}
|
||||
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS: ${GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS:-7}
|
||||
@@ -99,6 +107,8 @@ services:
|
||||
volumes:
|
||||
- core-source:/srv/core
|
||||
- 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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -11,6 +11,34 @@ export const validateComposeModel = (model, mode) => {
|
||||
const runtime = model?.services?.runtime;
|
||||
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.memswap_limit)) errors.push('runtime memory+swap limit must be positive');
|
||||
if (Number(runtime.memswap_limit) !== Number(runtime.mem_limit)) {
|
||||
|
||||
@@ -10,6 +10,22 @@ if [ ! -f "$env_file" ]; then
|
||||
exit 66
|
||||
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" \
|
||||
-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
|
||||
|
||||
@@ -8,7 +8,29 @@ const safeRuntime = {
|
||||
CORE_SOURCE_MODE: 'clone',
|
||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||
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',
|
||||
mem_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,
|
||||
restart: 'no',
|
||||
environment: {
|
||||
...safeRuntime.environment,
|
||||
CORE_SOURCE_MODE: 'bind',
|
||||
RUNTIME_MODE: 'development',
|
||||
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||
RAYON_NUM_THREADS: '2',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(validateComposeModel({ services }, 'development'), []);
|
||||
|
||||
Reference in New Issue
Block a user