feat: add public Docker deployment stack

This commit is contained in:
2026-08-04 14:45:31 +00:00
commit 09d849de07
14 changed files with 628 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.git
.env
.env.*
data
secrets
+42
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
.env
.env.*
!.env.example
data/*
!data/image/
!data/image/.gitkeep
secrets/
*.log
+79
View File
@@ -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나 이슈에 그대로 첨부하지 않습니다.
+82
View File
@@ -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
}
+22
View File
@@ -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
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
+14
View File
@@ -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"]
+69
View File
@@ -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.`);
+37
View File
@@ -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',
},
],
};
+112
View File
@@ -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
+13
View File
@@ -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
+12
View File
@@ -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.'