From 0f2ec7c1b366f8423ef6f06f4dbaf2a186b82055 Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 2 Apr 2026 21:47:39 +0400 Subject: [PATCH] feat(Core/Docker): production deploy configuration - docker-compose.prod.yml: MySQL tuning (innodb_buffer_pool=4G), resource limits, log rotation - scripts/prod-deploy.sh: production startup script mirroring start-server.sh sequence with pre-flight checks and sysctl tuning - conf/dist/env.production: production environment variable template Co-Authored-By: Claude Sonnet 4.6 --- conf/dist/env.production | 29 +++++ docker-compose.prod.yml | 45 ++++++++ scripts/prod-deploy.sh | 226 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 conf/dist/env.production create mode 100644 docker-compose.prod.yml create mode 100644 scripts/prod-deploy.sh diff --git a/conf/dist/env.production b/conf/dist/env.production new file mode 100644 index 0000000..7e8df6d --- /dev/null +++ b/conf/dist/env.production @@ -0,0 +1,29 @@ +# Production environment — copy to .env before deploying +# cp .env.production .env + +# Docker image tag (master = latest stable build) +DOCKER_IMAGE_TAG=master + +# Database root password — CHANGE THIS +DOCKER_DB_ROOT_PASSWORD=CHANGE_ME_STRONG_PASSWORD + +# External ports +DOCKER_WORLD_EXTERNAL_PORT=8085 +DOCKER_AUTH_EXTERNAL_PORT=3724 +DOCKER_SOAP_EXTERNAL_PORT=7878 +DOCKER_DB_EXTERNAL_PORT=3306 + +# Run server as this user (match host UID/GID to avoid volume permission issues) +# Run: id -u && id -g to get your values +DOCKER_USER=acore +DOCKER_USER_ID=1000 +DOCKER_GROUP_ID=1000 + +# Volume paths (use absolute paths in production) +DOCKER_VOL_ETC=./env/dist/etc +DOCKER_VOL_LOGS=./env/dist/logs +DOCKER_VOL_DATA=ac-client-data + +# Game rates (can also be set in worldserver.conf) +AC_RATE_XP_KILL=1 +ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN=0 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..df79697 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,45 @@ +services: + ac-database: + command: > + --innodb_buffer_pool_size=4G + --innodb_log_file_size=512M + --innodb_flush_log_at_trx_commit=2 + --innodb_flush_method=O_DIRECT + --max_connections=300 + --query_cache_size=0 + --query_cache_type=0 + --slow_query_log=1 + --long_query_time=2 + deploy: + resources: + limits: + cpus: '2' + memory: 5G + + ac-worldserver: + deploy: + resources: + limits: + cpus: '6' + memory: 6G + ulimits: + nofile: + soft: 65536 + hard: 65536 + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + + ac-authserver: + deploy: + resources: + limits: + cpus: '1' + memory: 512M + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" diff --git a/scripts/prod-deploy.sh b/scripts/prod-deploy.sh new file mode 100644 index 0000000..40a5ad4 --- /dev/null +++ b/scripts/prod-deploy.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}" +BUILD_FLAG="--build" + +# Production compose stack — layered on top of the standard override +COMPOSE="docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.prod.yml" + +# ── helpers ────────────────────────────────────────────────────────────────── + +log() { printf '[prod-deploy] %s\n' "$*"; } +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } + +print_service_logs() { + local service="$1" + printf '\n[%s logs]\n' "$service" >&2 + $COMPOSE logs --tail=120 "$service" >&2 || true + printf '\n' >&2 +} + +wait_for_healthy() { + local container_name="$1" + local timeout_seconds="${2:-180}" + local start_ts now status + + start_ts="$(date +%s)" + while :; do + status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)" + case "$status" in + healthy|running) return 0 ;; + unhealthy|exited|dead) return 1 ;; + esac + now="$(date +%s)" + if (( now - start_ts >= timeout_seconds )); then return 1; fi + sleep 2 + done +} + +run_one_shot_service() { + local service="$1" + local container_name="$2" + + if [[ -n "$BUILD_FLAG" ]]; then + if ! $COMPOSE up --build --no-deps "$service"; then + print_service_logs "$service" + printf '%s failed during startup\n' "$service" >&2 + exit 1 + fi + else + if ! $COMPOSE up --no-deps "$service"; then + print_service_logs "$service" + printf '%s failed during startup\n' "$service" >&2 + exit 1 + fi + fi + + local exit_code + exit_code="$(docker inspect -f '{{.State.ExitCode}}' "$container_name" 2>/dev/null || printf '1')" + if [[ "$exit_code" != "0" ]]; then + print_service_logs "$service" + printf '%s failed with exit code %s\n' "$service" "$exit_code" >&2 + exit 1 + fi +} + +require_docker() { + if ! command -v docker >/dev/null 2>&1; then + die "docker is not installed or not in PATH" + fi + if ! docker compose version >/dev/null 2>&1; then + die "docker compose plugin is not available. On Ubuntu: apt install docker-compose-plugin" + fi + if ! docker info >/dev/null 2>&1; then + die "docker daemon is not reachable. Run: sudo systemctl enable --now docker" + fi +} + +ensure_host_permissions() { + local target_uid="${DOCKER_USER_ID:-1000}" + local target_gid="${DOCKER_GROUP_ID:-1000}" + local dirs=( + "$ROOT_DIR/env/dist/etc" + "$ROOT_DIR/env/dist/logs" + "$ROOT_DIR/env/dist/temp" + ) + + mkdir -p "${dirs[@]}" + + if [[ "$(id -u)" -eq 0 ]]; then + chown -R "${target_uid}:${target_gid}" "${dirs[@]}" + return 0 + fi + + local dir + for dir in "${dirs[@]}"; do + if [[ ! -w "$dir" ]]; then + printf 'host directory is not writable: %s\n' "$dir" >&2 + printf 'Run: sudo chown -R %s:%s %q %q %q\n' \ + "$target_uid" "$target_gid" \ + "${dirs[0]}" "${dirs[1]}" "${dirs[2]}" >&2 + exit 1 + fi + done +} + +apply_sysctl() { + log "applying sysctl settings..." + sysctl -w net.core.somaxconn=4096 2>/dev/null || true + sysctl -w net.ipv4.tcp_max_syn_backlog=4096 2>/dev/null || true + sysctl -w fs.file-max=100000 2>/dev/null || true + # Prevent Docker conntrack table overflow (causes sudden disconnects) + sysctl -w net.netfilter.nf_conntrack_max=131072 2>/dev/null || true + sysctl -w net.ipv4.tcp_keepalive_time=120 2>/dev/null || true + sysctl -w net.ipv4.tcp_keepalive_intvl=30 2>/dev/null || true + sysctl -w net.ipv4.tcp_keepalive_probes=5 2>/dev/null || true +} + +# ── argument parsing ────────────────────────────────────────────────────────── + +usage() { + cat <&2; usage >&2; exit 1 ;; + esac + shift +done + +# ── pre-flight ──────────────────────────────────────────────────────────────── + +if [[ ! -f "$ENV_FILE" ]]; then + if [[ -f "$ROOT_DIR/.env.production" ]]; then + log ".env not found — copying from .env.production" + cp "$ROOT_DIR/.env.production" "$ENV_FILE" + die "Fill in $ENV_FILE (at minimum DOCKER_DB_ROOT_PASSWORD) and re-run." + else + die "$ENV_FILE not found. Create it based on .env.production." + fi +fi + +if grep -q "CHANGE_ME" "$ENV_FILE"; then + die "Change DOCKER_DB_ROOT_PASSWORD in $ENV_FILE before deploying to production." +fi + +# Source env so ensure_host_permissions picks up DOCKER_USER_ID / DOCKER_GROUP_ID +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +cd "$ROOT_DIR" + +require_docker + +if [[ $EUID -eq 0 ]]; then + apply_sysctl +else + log "not root — skipping sysctl (run with sudo to apply kernel settings)" +fi + +# ── startup sequence (mirrors start-server.sh) ─────────────────────────────── + +log "applying module configuration" +bash "$ROOT_DIR/setup-modules.sh" + +log "ensuring host directory permissions" +ensure_host_permissions + +log "starting database" +if [[ -n "$BUILD_FLAG" ]]; then + $COMPOSE up -d --build ac-database +else + $COMPOSE up -d ac-database +fi + +if ! wait_for_healthy "ac-database" 180; then + print_service_logs "ac-database" + die "ac-database did not become healthy" +fi + +log "running database import" +run_one_shot_service "ac-db-import" "ac-db-import" + +log "importing custom SQL" +bash "$ROOT_DIR/import-custom-sql.sh" + +log "applying post-database module setup" +bash "$ROOT_DIR/setup-modules.sh" + +log "initializing client data" +run_one_shot_service "ac-client-data-init" "ac-client-data-init" + +log "starting authserver and worldserver" +if [[ -n "$BUILD_FLAG" ]]; then + $COMPOSE up -d --build ac-authserver ac-worldserver +else + $COMPOSE up -d ac-authserver ac-worldserver +fi + +log "synchronizing realmlist" +bash "$ROOT_DIR/sync-realmlist.sh" + +log "current service status" +$COMPOSE ps + +printf '\n' +printf 'Realm: %s:%s\n' "${ACORE_REALMLIST_PUBLIC_ADDRESS:-127.0.0.1}" "${ACORE_REALMLIST_PORT:-${DOCKER_WORLD_EXTERNAL_PORT:-8085}}" +printf 'World: 127.0.0.1:%s\n' "${DOCKER_WORLD_EXTERNAL_PORT:-8085}" +printf 'Auth: 127.0.0.1:%s\n' "${DOCKER_AUTH_EXTERNAL_PORT:-3724}" +printf 'SOAP: 127.0.0.1:%s\n' "${DOCKER_SOAP_EXTERNAL_PORT:-7878}" +printf 'Account: ./create-account.sh admin your_password\n' +printf 'Logs: %s logs -f ac-worldserver ac-authserver\n' "$COMPOSE"