feat: add public Docker deployment stack
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
data
|
||||
secrets
|
||||
@@ -0,0 +1,42 @@
|
||||
# Public endpoint. Caddy obtains TLS certificates for DOMAIN automatically.
|
||||
DOMAIN=game.example.com
|
||||
PUBLIC_SCHEME=https
|
||||
ACME_EMAIL=admin@example.com
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
|
||||
# Core2026 source. A public HTTPS URL needs no extra credential.
|
||||
CORE_REPOSITORY_URL=https://github.com/your-org/core2026.git
|
||||
CORE_BOOTSTRAP_REF=main
|
||||
|
||||
# Development bind override (used only with compose.dev.yaml).
|
||||
# CORE_DEV_PATH=../core2026
|
||||
# DEV_UID=1000
|
||||
# DEV_GID=1000
|
||||
|
||||
# Persistent services. Use long random values; do not commit .env.
|
||||
POSTGRES_DB=sammo
|
||||
POSTGRES_USER=sammo
|
||||
POSTGRES_PASSWORD=replace-with-a-long-random-database-password
|
||||
REDIS_PASSWORD=replace-with-a-long-random-redis-password
|
||||
GAME_TOKEN_SECRET=replace-with-at-least-32-random-bytes
|
||||
GATEWAY_BOOTSTRAP_TOKEN=replace-with-a-separate-random-bootstrap-token
|
||||
|
||||
# The first startup creates this superuser only when the user table is empty.
|
||||
INITIAL_ADMIN_USERNAME=admin
|
||||
INITIAL_ADMIN_PASSWORD=replace-with-a-long-random-admin-password
|
||||
INITIAL_ADMIN_DISPLAY_NAME=Administrator
|
||||
|
||||
# Kakao Developers app values. Register https://DOMAIN/gateway/oauth/callback.
|
||||
KAKAO_REST_KEY=replace-with-kakao-rest-api-key
|
||||
KAKAO_ADMIN_KEY=
|
||||
|
||||
# Account policy and optional compatibility secret.
|
||||
GATEWAY_LOCAL_REGISTRATION_ENABLED=true
|
||||
GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED=true
|
||||
GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS=7
|
||||
GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT=
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,8 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
data/*
|
||||
!data/image/
|
||||
!data/image/.gitkeep
|
||||
secrets/
|
||||
*.log
|
||||
@@ -0,0 +1,79 @@
|
||||
# Sammo Core2026 Docker deployment
|
||||
|
||||
이 저장소는 새 호스트에서 `.env` 하나로 Core2026 Gateway, PostgreSQL, Redis,
|
||||
Caddy와 Git/PM2 release-controller를 기동하기 위한 공개 배포 골격입니다. 게임
|
||||
profile은 관리자 화면에서 각각 다른 branch 또는 commit을 선택해 DB 유지 배포,
|
||||
DB 초기화 배포와 rollback을 수행합니다.
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
Docker Engine과 Compose plugin이 설치된 Linux 호스트에서 다음을 실행합니다.
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
# .env의 domain, Core2026 URL, Kakao key와 모든 비밀값을 교체합니다.
|
||||
docker compose up -d --build --wait
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
DNS의 `DOMAIN` A/AAAA record가 호스트를 가리키고 80/443 TCP 및 443 UDP가
|
||||
열려 있으면 Caddy가 인증서를 자동 발급합니다. Kakao Developers에는
|
||||
`https://DOMAIN/gateway/oauth/callback`을 redirect URI로 등록합니다. 첫 기동은
|
||||
Core2026 clone, frozen-lockfile install, build와 Gateway migration 때문에 수 분이
|
||||
걸릴 수 있습니다.
|
||||
|
||||
`INITIAL_ADMIN_*`은 사용자 table이 비어 있을 때만 superuser를 한 번 생성합니다.
|
||||
그 뒤 `/gateway/admin/server-operations`에서 `che`, `kwe`, `pwe`, `twe`, `nya`,
|
||||
`pya`, `hwe`마다 source branch/commit을 독립 선택합니다. `DB 유지 배포`는 현
|
||||
시즌을 보존하고, `DB 초기화 배포`는 scenario를 다시 seed합니다. Gateway 자체의
|
||||
배포와 이전 commit rollback은 같은 화면의 별도 release 영역에서 처리됩니다.
|
||||
|
||||
## 데이터와 복구 경계
|
||||
|
||||
- PostgreSQL, Redis, Core clone/worktree, PM2 상태, Caddy 인증서와 user icon은
|
||||
named volume 또는 `data/image`에 보존됩니다.
|
||||
- `/image/*`는 앱 artifact가 아니며 `data/image/`의 별도 운영 자산을 제공합니다.
|
||||
- 일반 `docker compose down`은 volume을 보존합니다. `down --volumes`는 DB와
|
||||
release 상태를 삭제하는 파괴적 명령이므로 backup 없이 실행하지 않습니다.
|
||||
- 앱 rollback은 Prisma migration을 되돌리지 않습니다. 이전 앱과 새 schema의
|
||||
호환성을 배포 전에 확인합니다.
|
||||
- Core2026 저장소가 비공개라면 이 공개 저장소에 credential을 추가하지 말고,
|
||||
호스트의 read-only deploy key나 credential helper를 별도 override로 mount합니다.
|
||||
|
||||
상태 확인과 로그:
|
||||
|
||||
```sh
|
||||
docker compose ps
|
||||
docker compose logs --tail=200 runtime caddy
|
||||
docker compose exec runtime pnpm --filter @sammo-ts/release-controller status
|
||||
```
|
||||
|
||||
## 개발 bind 모드
|
||||
|
||||
로컬 Core2026 checkout을 container에 bind하고 DB/Redis/Caddy는 같은 구성으로
|
||||
사용할 수 있습니다. `.env`에 `CORE_DEV_PATH=/absolute/path/to/core2026`,
|
||||
host 사용자의 `DEV_UID`/`DEV_GID`, `PUBLIC_SCHEME=http`, `DOMAIN=localhost`를
|
||||
추가한 뒤 실행합니다. 기본 UID/GID는 `1000:1000`입니다.
|
||||
|
||||
```sh
|
||||
docker compose -f compose.yaml -f compose.dev.yaml up -d --build
|
||||
docker compose exec runtime pnpm --filter @sammo-ts/gateway-api dev
|
||||
docker compose exec runtime pnpm --filter @sammo-ts/gateway-frontend dev --host 0.0.0.0
|
||||
```
|
||||
|
||||
개발 override의 runtime은 의존성과 Prisma client를 준비한 뒤 대기합니다. 필요한
|
||||
watch process를 별도 shell에서 실행합니다. 운영 release-controller를 시험하려면
|
||||
override 없이 production mode를 사용해야 하며, bind checkout의 Git metadata에
|
||||
container worktree 경로를 등록하지 않도록 주의합니다.
|
||||
|
||||
## 설정 검증
|
||||
|
||||
`.env`를 채운 뒤 실제 값을 출력하지 않는 검사를 실행합니다.
|
||||
|
||||
```sh
|
||||
./scripts/check.sh
|
||||
```
|
||||
|
||||
검사는 production/development Compose model과 Caddyfile 구문을 확인합니다.
|
||||
`docker compose config` 전체 출력에는 펼쳐진 비밀값이 포함될 수 있으므로 CI
|
||||
artifact나 이슈에 그대로 첨부하지 않습니다.
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$SITE_ADDRESS} {
|
||||
encode zstd gzip
|
||||
|
||||
redir /gateway /gateway/ 308
|
||||
redir /che /che/ 308
|
||||
redir /kwe /kwe/ 308
|
||||
redir /pwe /pwe/ 308
|
||||
redir /twe /twe/ 308
|
||||
redir /nya /nya/ 308
|
||||
redir /pya /pya/ 308
|
||||
redir /hwe /hwe/ 308
|
||||
|
||||
handle_path /image/* {
|
||||
root * /srv/image
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /gateway/api/trpc* {
|
||||
reverse_proxy runtime:15001
|
||||
}
|
||||
handle /gateway/api/user-icons/* {
|
||||
uri strip_prefix /gateway/api
|
||||
reverse_proxy runtime:15001
|
||||
}
|
||||
handle /gateway/api/* {
|
||||
uri strip_prefix /gateway/api
|
||||
reverse_proxy runtime:15001
|
||||
}
|
||||
handle /gateway/* {
|
||||
reverse_proxy runtime:15000
|
||||
}
|
||||
|
||||
handle /che/api/* {
|
||||
reverse_proxy runtime:15003
|
||||
}
|
||||
handle /che/* {
|
||||
reverse_proxy runtime:15002
|
||||
}
|
||||
handle /kwe/api/* {
|
||||
reverse_proxy runtime:15005
|
||||
}
|
||||
handle /kwe/* {
|
||||
reverse_proxy runtime:15004
|
||||
}
|
||||
handle /pwe/api/* {
|
||||
reverse_proxy runtime:15007
|
||||
}
|
||||
handle /pwe/* {
|
||||
reverse_proxy runtime:15006
|
||||
}
|
||||
handle /twe/api/* {
|
||||
reverse_proxy runtime:15009
|
||||
}
|
||||
handle /twe/* {
|
||||
reverse_proxy runtime:15008
|
||||
}
|
||||
handle /nya/api/* {
|
||||
reverse_proxy runtime:15011
|
||||
}
|
||||
handle /nya/* {
|
||||
reverse_proxy runtime:15010
|
||||
}
|
||||
handle /pya/api/* {
|
||||
reverse_proxy runtime:15013
|
||||
}
|
||||
handle /pya/* {
|
||||
reverse_proxy runtime:15012
|
||||
}
|
||||
handle /hwe/api/* {
|
||||
reverse_proxy runtime:15015
|
||||
}
|
||||
handle /hwe/* {
|
||||
reverse_proxy runtime:15014
|
||||
}
|
||||
|
||||
respond /healthz 200
|
||||
redir / /gateway/ 302
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
runtime:
|
||||
user: ${DEV_UID:-1000}:${DEV_GID:-1000}
|
||||
environment:
|
||||
CORE_SOURCE_MODE: bind
|
||||
CORE_BIND_ROOT: /workspace/core2026
|
||||
RUNTIME_MODE: ${RUNTIME_MODE:-development}
|
||||
VITE_PREVIEW_ALLOWED_HOSTS: '*'
|
||||
volumes:
|
||||
- ${CORE_DEV_PATH:-../core2026}:/workspace/core2026
|
||||
- runtime-data:/srv/data
|
||||
- core-source:/srv/core
|
||||
command: [sh, -c, sleep infinity]
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, 'test -d /workspace/core2026/node_modules/.pnpm']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 5m
|
||||
caddy:
|
||||
environment:
|
||||
SITE_ADDRESS: :80
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
name: ${COMPOSE_PROJECT_NAME:-sammo-public}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18.4-bookworm
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-sammo}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-sammo}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
TZ: ${TZ:-Asia/Seoul}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 24
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:8.2.7-bookworm
|
||||
entrypoint: [/usr/local/bin/sammo-redis-entrypoint]
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
- ./runtime/redis-entrypoint.sh:/usr/local/bin/sammo-redis-entrypoint:ro
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, 'REDISCLI_AUTH="$${REDIS_PASSWORD}" redis-cli ping 2>/dev/null | grep -q PONG']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 24
|
||||
start_period: 5s
|
||||
restart: unless-stopped
|
||||
|
||||
runtime:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: runtime/Dockerfile
|
||||
environment:
|
||||
CORE_SOURCE_MODE: clone
|
||||
CORE_REPOSITORY_URL: ${CORE_REPOSITORY_URL:?set CORE_REPOSITORY_URL in .env}
|
||||
CORE_BOOTSTRAP_REF: ${CORE_BOOTSTRAP_REF:-main}
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: '5432'
|
||||
POSTGRES_DB: ${POSTGRES_DB:-sammo}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-sammo}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
GATEWAY_DB_SCHEMA: public
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: '6379'
|
||||
REDIS_DB: '0'
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}
|
||||
GAME_TOKEN_SECRET: ${GAME_TOKEN_SECRET:?set GAME_TOKEN_SECRET in .env}
|
||||
GATEWAY_BOOTSTRAP_TOKEN: ${GATEWAY_BOOTSTRAP_TOKEN:?set GATEWAY_BOOTSTRAP_TOKEN in .env}
|
||||
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:?set INITIAL_ADMIN_USERNAME in .env}
|
||||
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:?set INITIAL_ADMIN_PASSWORD in .env}
|
||||
INITIAL_ADMIN_DISPLAY_NAME: ${INITIAL_ADMIN_DISPLAY_NAME:-Administrator}
|
||||
BOOTSTRAP_PROFILES: ${BOOTSTRAP_PROFILES:-gateway,che,kwe,pwe,twe,nya,pya,hwe}
|
||||
KAKAO_REST_KEY: ${KAKAO_REST_KEY:?set KAKAO_REST_KEY in .env}
|
||||
KAKAO_ADMIN_KEY: ${KAKAO_ADMIN_KEY:-}
|
||||
KAKAO_REDIRECT_URI: ${PUBLIC_SCHEME:-https}://${DOMAIN:?set DOMAIN in .env}/gateway/oauth/callback
|
||||
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_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}
|
||||
GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT: ${GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT:-}
|
||||
GATEWAY_API_HOST: 0.0.0.0
|
||||
GATEWAY_API_PORT: '15001'
|
||||
GATEWAY_FRONTEND_PORT: '15000'
|
||||
GATEWAY_TRPC_PATH: /gateway/api/trpc
|
||||
GATEWAY_INTERNAL_API_URL: http://127.0.0.1:15001
|
||||
GATEWAY_REDIS_PREFIX: sammo:gateway
|
||||
GATEWAY_WORKSPACE_ROOT: /srv/core/repository
|
||||
GATEWAY_WORKTREE_ROOT: /srv/core/profile-worktrees
|
||||
RELEASE_CONTROLLER_WORKSPACE_ROOT: /srv/core/repository
|
||||
RELEASE_CONTROLLER_WORKTREE_ROOT: /srv/core/release-worktrees
|
||||
GATEWAY_BASE_PATH: /gateway
|
||||
VITE_APP_BASE_PATH: /gateway
|
||||
VITE_GATEWAY_API_URL: /gateway/api/trpc
|
||||
VITE_GAME_API_URL_TEMPLATE: /{profile}/api/trpc
|
||||
VITE_GAME_WEB_URL_TEMPLATE: /{profile}/
|
||||
VITE_GATEWAY_USER_ICON_BASE_URL: /gateway/api/user-icons
|
||||
VITE_PREVIEW_ALLOWED_HOSTS: ${DOMAIN:?set DOMAIN in .env}
|
||||
PM2_HOME: /srv/data/pm2
|
||||
TZ: ${TZ:-Asia/Seoul}
|
||||
volumes:
|
||||
- core-source:/srv/core
|
||||
- runtime-data:/srv/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: [CMD, curl, --fail, --silent, http://127.0.0.1:15001/healthz]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 5m
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: caddy:2.10.2-alpine
|
||||
environment:
|
||||
DOMAIN: ${DOMAIN:?set DOMAIN in .env}
|
||||
SITE_ADDRESS: ${DOMAIN:?set DOMAIN in .env}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?set ACME_EMAIL in .env}
|
||||
ports:
|
||||
- ${HTTP_PORT:-80}:80
|
||||
- ${HTTPS_PORT:-443}:443
|
||||
- ${HTTPS_PORT:-443}:443/udp
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./data/image:/srv/image:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
depends_on:
|
||||
runtime:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
core-source:
|
||||
runtime-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:24.18.0-bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl git openssh-client procps tini \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable \
|
||||
&& corepack prepare pnpm@11.17.0 --activate
|
||||
|
||||
COPY --chmod=755 runtime/entrypoint.sh /usr/local/bin/sammo-entrypoint
|
||||
COPY --chmod=755 runtime/redis-entrypoint.sh /opt/sammo/redis-entrypoint.sh
|
||||
COPY runtime/ecosystem.config.cjs runtime/bootstrap.mjs /opt/sammo/
|
||||
|
||||
WORKDIR /srv/core/repository
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/sammo-entrypoint"]
|
||||
@@ -0,0 +1,69 @@
|
||||
const endpoint = 'http://127.0.0.1:15001/gateway/api/trpc';
|
||||
|
||||
const post = async (procedure, input, sessionToken) => {
|
||||
const response = await fetch(`${endpoint}/${procedure}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(sessionToken ? { 'x-session-token': sessionToken } : {}),
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
const code = body?.error?.data?.code;
|
||||
const message = body?.error?.message ?? JSON.stringify(body);
|
||||
const error = new Error(`${procedure} failed (${response.status}, ${code ?? 'unknown'}): ${message}`);
|
||||
error.code = code;
|
||||
throw error;
|
||||
}
|
||||
return body?.result?.data;
|
||||
};
|
||||
|
||||
const token = process.env.GATEWAY_BOOTSTRAP_TOKEN;
|
||||
const username = process.env.INITIAL_ADMIN_USERNAME;
|
||||
const password = process.env.INITIAL_ADMIN_PASSWORD;
|
||||
if (!token || !username || !password) {
|
||||
console.log('Initial admin bootstrap is disabled because its environment values are incomplete.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let sessionToken;
|
||||
try {
|
||||
const result = await post('auth.bootstrapLocal', {
|
||||
token,
|
||||
username,
|
||||
password,
|
||||
displayName: process.env.INITIAL_ADMIN_DISPLAY_NAME || 'Administrator',
|
||||
});
|
||||
sessionToken = result?.sessionToken;
|
||||
if (!sessionToken) throw new Error('bootstrap response did not include a session token');
|
||||
console.log('Created the initial superuser without printing credentials.');
|
||||
} catch (error) {
|
||||
if (error?.code === 'CONFLICT') {
|
||||
console.log('Initial superuser already exists; bootstrap was left unchanged.');
|
||||
process.exit(0);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const profilePorts = new Map([
|
||||
['gateway', 15001],
|
||||
['che', 15003],
|
||||
['kwe', 15005],
|
||||
['pwe', 15007],
|
||||
['twe', 15009],
|
||||
['nya', 15011],
|
||||
['pya', 15013],
|
||||
['hwe', 15015],
|
||||
]);
|
||||
const profiles = (process.env.BOOTSTRAP_PROFILES || 'gateway,che,kwe,pwe,twe,nya,pya,hwe')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value && value !== 'gateway');
|
||||
for (const profile of profiles) {
|
||||
const apiPort = profilePorts.get(profile);
|
||||
if (!apiPort) throw new Error(`No reserved API port is defined for profile: ${profile}`);
|
||||
await post('admin.profiles.upsert', { profile, scenario: 'default', apiPort, status: 'STOPPED' }, sessionToken);
|
||||
}
|
||||
console.log(`Registered ${profiles.length} stopped profiles for branch-selectable Admin deployments.`);
|
||||
@@ -0,0 +1,37 @@
|
||||
const path = require('node:path');
|
||||
|
||||
const root = process.env.GATEWAY_WORKSPACE_ROOT || '/srv/core/repository';
|
||||
const common = { env: { ...process.env }, autorestart: true, kill_timeout: 15000 };
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
...common,
|
||||
name: 'sammo:gateway-api',
|
||||
cwd: path.join(root, 'app/gateway-api'),
|
||||
script: path.join(root, 'app/gateway-api/dist/index.js'),
|
||||
env: { ...process.env, GATEWAY_ROLE: 'api' },
|
||||
},
|
||||
{
|
||||
...common,
|
||||
name: 'sammo:gateway-frontend',
|
||||
cwd: path.join(root, 'app/gateway-frontend'),
|
||||
script: path.join(root, 'app/gateway-frontend/node_modules/vite/bin/vite.js'),
|
||||
args: 'preview --host 0.0.0.0 --port 15000',
|
||||
},
|
||||
{
|
||||
...common,
|
||||
name: 'sammo:gateway-orchestrator',
|
||||
cwd: path.join(root, 'app/gateway-api'),
|
||||
script: path.join(root, 'app/gateway-api/dist/index.js'),
|
||||
env: { ...process.env, GATEWAY_ROLE: 'orchestrator' },
|
||||
},
|
||||
{
|
||||
...common,
|
||||
name: 'sammo:release-controller',
|
||||
cwd: path.join(root, 'app/release-controller'),
|
||||
script: path.join(root, 'app/release-controller/dist/index.js'),
|
||||
args: 'daemon',
|
||||
},
|
||||
],
|
||||
};
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# The container is always non-interactive. This also makes pnpm replace a
|
||||
# modules directory created with a different store without waiting for a TTY.
|
||||
export CI="${CI:-true}"
|
||||
|
||||
require_env() {
|
||||
eval "value=\${$1:-}"
|
||||
if [ -z "$value" ]; then
|
||||
echo "$1 is required" >&2
|
||||
exit 64
|
||||
fi
|
||||
}
|
||||
|
||||
for key in POSTGRES_PASSWORD REDIS_PASSWORD GAME_TOKEN_SECRET KAKAO_REST_KEY KAKAO_REDIRECT_URI; do
|
||||
require_env "$key"
|
||||
done
|
||||
|
||||
case "${CORE_SOURCE_MODE:-clone}" in
|
||||
clone)
|
||||
require_env CORE_REPOSITORY_URL
|
||||
core_root=/srv/core/repository
|
||||
if [ ! -d "$core_root/.git" ]; then
|
||||
mkdir -p /srv/core
|
||||
git clone --no-single-branch "$CORE_REPOSITORY_URL" "$core_root"
|
||||
git -C "$core_root" checkout "${CORE_BOOTSTRAP_REF:-main}"
|
||||
fi
|
||||
;;
|
||||
bind)
|
||||
core_root=${CORE_BIND_ROOT:-/workspace/core2026}
|
||||
if [ ! -e "$core_root/.git" ] || [ ! -f "$core_root/pnpm-lock.yaml" ]; then
|
||||
echo "CORE_BIND_ROOT must be a Core2026 Git checkout" >&2
|
||||
exit 66
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo 'CORE_SOURCE_MODE must be clone or bind' >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
git config --global --add safe.directory "$core_root"
|
||||
export GATEWAY_WORKSPACE_ROOT="$core_root"
|
||||
export RELEASE_CONTROLLER_WORKSPACE_ROOT="$core_root"
|
||||
export DATABASE_URL="$(node -e 'const u=new URL("postgresql://localhost");u.username=process.env.POSTGRES_USER||"sammo";u.password=process.env.POSTGRES_PASSWORD;u.hostname=process.env.POSTGRES_HOST||"postgres";u.port=process.env.POSTGRES_PORT||"5432";u.pathname="/"+(process.env.POSTGRES_DB||"sammo");u.searchParams.set("schema","public");process.stdout.write(u.href)')"
|
||||
export GATEWAY_DATABASE_URL="$DATABASE_URL"
|
||||
export REDIS_URL="$(node -e 'const u=new URL("redis://localhost/0");u.password=process.env.REDIS_PASSWORD;u.hostname=process.env.REDIS_HOST||"redis";u.port=process.env.REDIS_PORT||"6379";process.stdout.write(u.href)')"
|
||||
|
||||
cd "$core_root"
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
if [ "${RUNTIME_MODE:-production}" = development ]; then
|
||||
pnpm --filter @sammo-ts/infra prisma:generate
|
||||
echo 'Development container is ready. Start the desired pnpm dev processes with docker compose exec runtime.'
|
||||
if [ "$#" -gt 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
pnpm --filter @sammo-ts/common build
|
||||
pnpm --filter @sammo-ts/infra prisma:generate
|
||||
pnpm --filter @sammo-ts/infra build
|
||||
pnpm --filter @sammo-ts/logic build
|
||||
pnpm --filter @sammo-ts/game-engine build
|
||||
pnpm --filter @sammo-ts/gateway-api build
|
||||
pnpm --filter @sammo-ts/gateway-frontend build
|
||||
pnpm --filter @sammo-ts/release-controller build
|
||||
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
|
||||
|
||||
GATEWAY_ROLE=api node app/gateway-api/dist/index.js &
|
||||
bootstrap_pid=$!
|
||||
cleanup_bootstrap() {
|
||||
kill "$bootstrap_pid" 2>/dev/null || true
|
||||
wait "$bootstrap_pid" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup_bootstrap EXIT INT TERM
|
||||
|
||||
attempt=0
|
||||
until curl --fail --silent http://127.0.0.1:15001/healthz >/dev/null; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge 120 ]; then
|
||||
echo 'Gateway API did not become ready for bootstrap' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
node /opt/sammo/bootstrap.mjs
|
||||
cleanup_bootstrap
|
||||
trap - EXIT INT TERM
|
||||
|
||||
pm2_bin="$core_root/app/gateway-api/node_modules/.bin/pm2"
|
||||
"$pm2_bin" start /opt/sammo/ecosystem.config.cjs
|
||||
pm2_pid=$(cat "$PM2_HOME/pm2.pid")
|
||||
tail -n 0 -F "$PM2_HOME/pm2.log" "$PM2_HOME"/logs/*.log &
|
||||
logs_pid=$!
|
||||
|
||||
shutdown_pm2() {
|
||||
kill "$logs_pid" 2>/dev/null || true
|
||||
"$pm2_bin" kill >/dev/null 2>&1 || true
|
||||
wait "$logs_pid" 2>/dev/null || true
|
||||
}
|
||||
trap 'shutdown_pm2; exit 0' INT TERM
|
||||
|
||||
while kill -0 "$pm2_pid" 2>/dev/null; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
shutdown_pm2
|
||||
echo 'PM2 daemon exited unexpectedly' >&2
|
||||
exit 1
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -z "${REDIS_PASSWORD:-}" ]; then
|
||||
echo 'REDIS_PASSWORD is required' >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
umask 077
|
||||
install -d -o redis -g redis -m 700 /run/sammo
|
||||
printf 'user default on >%s ~* &* +@all\n' "$REDIS_PASSWORD" >/run/sammo/users.acl
|
||||
chown redis:redis /run/sammo/users.acl
|
||||
exec /usr/local/bin/docker-entrypoint.sh redis-server --appendonly yes --aclfile /run/sammo/users.acl
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
stack_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$stack_dir"
|
||||
|
||||
docker compose --env-file .env config --quiet
|
||||
docker compose --env-file .env -f compose.yaml -f compose.dev.yaml config --quiet
|
||||
docker run --rm -v "$stack_dir/caddy/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||
-e SITE_ADDRESS=:80 -e ACME_EMAIL=validate@example.invalid caddy:2.10.2-alpine \
|
||||
caddy validate --config /etc/caddy/Caddyfile
|
||||
echo 'Compose and Caddy configuration are valid.'
|
||||
Reference in New Issue
Block a user