diff --git a/.env.example b/.env.example index 0ea0ed9..500f166 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,14 @@ GAME_IMAGE=git.old-metin2.com/metin2/server:latest WEB_IMAGE=git.old-metin2.com/metin2/web:latest ################################################################################ -# MariaDB settings +# Database settings ################################################################################ -MARIADB_HOST=mariadb -MARIADB_USER=root -MARIADB_PASSWORD=metin2 -MARIADB_PORT=3306 -MARIADB_EXTERNAL_PORT=3306 -MARIADB_DB=metin2 +DATABASE_HOST=database +DATABASE_USER=metin2 +DATABASE_PASSWORD=metin2 +DATABASE_PORT=5432 +DATABASE_EXTERNAL_PORT=33065432 +DATABASE_DATABASE=metin2 ################################################################################ # Web app settings diff --git a/README.md b/README.md index ea4f8e5..da12519 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,7 @@ sure that you have a compatible Linux + Docker environment - for more information, check out the [compatibility matrix](https://git.old-metin2.com/metin2/deploy#compatibility-matrix). ### Architecture description -The MySQL database is currently pinned on version 5.5. We're using the -[biarms/mysql](https://github.com/biarms/mysql) project in order to provide ARM -compatibility for such an old version of MySQL. +The PostgreSQL database is currently pinned on version 18.. The game cores (`db`, `auth`, `game-*`) are ran by using pre-built images containing the server binaries and game files, provided in the @@ -93,7 +91,7 @@ need a database and website, but want to run the server in some other way (for example, in an IDE), you can just bring up only the services you need: ```shell -docker compose up -d mysql web +docker compose up -d database web ``` ## Compatibility matrix diff --git a/assets/db-init/create-databases.sql b/assets/db-init/create-databases.sql deleted file mode 100644 index c57f8d2..0000000 --- a/assets/db-init/create-databases.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE DATABASE metin2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/assets/postgres/Dockerfile b/assets/postgres/Dockerfile new file mode 100644 index 0000000..fd424fa --- /dev/null +++ b/assets/postgres/Dockerfile @@ -0,0 +1,23 @@ +FROM postgres:18 + +# Install build dependencies and redis-tools +RUN apt-get update && apt-get install -y \ + build-essential \ + git \ + postgresql-server-dev-18 \ + libhiredis-dev \ + ca-certificates \ + redis-tools \ + && rm -rf /var/lib/apt/lists/* + +# Clone and build redis_fdw +RUN git clone https://github.com/pg-redis-fdw/redis_fdw.git /tmp/redis_fdw && \ + cd /tmp/redis_fdw && \ + make USE_PGXS=1 && \ + make USE_PGXS=1 install && \ + rm -rf /tmp/redis_fdw + +# Clean up build dependencies to reduce image size (but keep redis-tools) +RUN apt-get update && \ + apt-get purge -y --auto-remove build-essential git postgresql-server-dev-18 && \ + rm -rf /var/lib/apt/lists/* diff --git a/assets/postgres/db-init/init-redis-fdw.sh b/assets/postgres/db-init/init-redis-fdw.sh new file mode 100755 index 0000000..8e726fa --- /dev/null +++ b/assets/postgres/db-init/init-redis-fdw.sh @@ -0,0 +1,212 @@ +#!/bin/bash +set -e + +# Function to log errors +log_error() { + echo "ERROR: $1" >&2 +} + +log_info() { + echo "INFO: $1" +} + +# Trap errors and log them +trap 'log_error "Script failed at line $LINENO"' ERR + +log_info "Starting Redis FDW initialization..." +log_info "Redis connection details:" +log_info " Host: ${REDIS_HOST:-redis}" +log_info " Port: 6379" +log_info " Password: $(if [ -n "$REDIS_PASSWORD" ]; then echo "[SET]"; else echo "[NOT SET]"; fi)" + +# Wait for Redis to be ready +log_info "Waiting for Redis to be ready..." +REDIS_READY=false +MAX_ATTEMPTS=30 +ATTEMPT=0 + +while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + if redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" ping 2>/dev/null | grep -q PONG; then + REDIS_READY=true + log_info "Redis is ready after $ATTEMPT attempts" + break + fi + + ATTEMPT=$((ATTEMPT + 1)) + log_info "Redis is unavailable - attempt $ATTEMPT/$MAX_ATTEMPTS" + sleep 2 +done + +if [ "$REDIS_READY" = false ]; then + log_error "Redis failed to become ready after $MAX_ATTEMPTS attempts" + log_error "Check if Redis container is running and accessible at ${REDIS_HOST:-redis}:6379" + exit 1 +fi + +# Get Redis info +log_info "Fetching Redis server information..." +REDIS_VERSION=$(redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" INFO server 2>/dev/null | grep "redis_version" | cut -d: -f2 | tr -d '\r' || echo "unknown") +REDIS_MODE=$(redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" INFO server 2>/dev/null | grep "redis_mode" | cut -d: -f2 | tr -d '\r' || echo "unknown") +REDIS_UPTIME=$(redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" INFO server 2>/dev/null | grep "uptime_in_seconds" | cut -d: -f2 | tr -d '\r' || echo "unknown") +TOTAL_KEYS=$(redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" DBSIZE 2>/dev/null | awk '{print $2}' || echo "0") + +log_info "Redis server info:" +log_info " Version: $REDIS_VERSION" +log_info " Mode: $REDIS_MODE" +log_info " Uptime: $REDIS_UPTIME seconds" +log_info " Total keys in DB 0: $TOTAL_KEYS" + +# Test Redis connectivity with a test key +log_info "Testing Redis write/read operations..." +if redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" SET fdw_test_key "fdw_init_$(date +%s)" EX 60 >/dev/null 2>&1; then + TEST_VALUE=$(redis-cli -h ${REDIS_HOST:-redis} -a "${REDIS_PASSWORD}" GET fdw_test_key 2>/dev/null) + log_info "Redis write/read test successful (value: $TEST_VALUE)" +else + log_error "Redis write/read test failed" + exit 1 +fi + +log_info "Setting up redis_fdw extension..." +log_info "PostgreSQL connection:" +log_info " User: $POSTGRES_USER" +log_info " Database: $POSTGRES_DB" + +# Run SQL setup as postgres user with error handling +if psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL; then + -- Create redis_fdw extension + DO \$\$ + BEGIN + CREATE EXTENSION IF NOT EXISTS redis_fdw; + RAISE NOTICE 'redis_fdw extension created/verified'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create redis_fdw extension: %', SQLERRM; + END; + \$\$; + + -- Create foreign server + DO \$\$ + BEGIN + DROP SERVER IF EXISTS redis_server CASCADE; + CREATE SERVER redis_server + FOREIGN DATA WRAPPER redis_fdw + OPTIONS ( + address '${REDIS_HOST:-redis}', + port '6379' + ); + RAISE NOTICE 'Foreign server redis_server created (host: ${REDIS_HOST:-redis}, port: 6379)'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create foreign server: %', SQLERRM; + END; + \$\$; + + -- Create user mapping + DO \$\$ + BEGIN + CREATE USER MAPPING FOR CURRENT_USER + SERVER redis_server + OPTIONS (password '${REDIS_PASSWORD}'); + RAISE NOTICE 'User mapping created for CURRENT_USER'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create user mapping: %', SQLERRM; + END; + \$\$; + + -- Create foreign table for Redis hashes + DO \$\$ + BEGIN + DROP FOREIGN TABLE IF EXISTS redis_hashes; + CREATE FOREIGN TABLE redis_hashes ( + key TEXT, + value TEXT + ) + SERVER redis_server + OPTIONS ( + database '0', + tabletype 'hash' + ); + RAISE NOTICE 'Foreign table redis_hashes created (database: 0, tabletype: hash)'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create redis_hashes: %', SQLERRM; + END; + \$\$; + + -- Create foreign table for Redis lists + DO \$\$ + BEGIN + DROP FOREIGN TABLE IF EXISTS redis_lists; + CREATE FOREIGN TABLE redis_lists ( + key TEXT, + value TEXT + ) + SERVER redis_server + OPTIONS ( + database '0', + tabletype 'list' + ); + RAISE NOTICE 'Foreign table redis_lists created (database: 0, tabletype: list)'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create redis_lists: %', SQLERRM; + END; + \$\$; + + -- Create foreign table for Redis sets + DO \$\$ + BEGIN + DROP FOREIGN TABLE IF EXISTS redis_sets; + CREATE FOREIGN TABLE redis_sets ( + key TEXT, + value TEXT + ) + SERVER redis_server + OPTIONS ( + database '0', + tabletype 'set' + ); + RAISE NOTICE 'Foreign table redis_sets created (database: 0, tabletype: set)'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create redis_sets: %', SQLERRM; + END; + \$\$; + + -- Create foreign table for Redis sorted sets + DO \$\$ + BEGIN + DROP FOREIGN TABLE IF EXISTS redis_zsets; + CREATE FOREIGN TABLE redis_zsets ( + key TEXT, + value TEXT + ) + SERVER redis_server + OPTIONS ( + database '0', + tabletype 'zset' + ); + RAISE NOTICE 'Foreign table redis_zsets created (database: 0, tabletype: zset)'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to create redis_zsets: %', SQLERRM; + END; + \$\$; + + -- Grant permissions + DO \$\$ + BEGIN + GRANT SELECT ON redis_hashes TO PUBLIC; + GRANT SELECT ON redis_lists TO PUBLIC; + GRANT SELECT ON redis_sets TO PUBLIC; + GRANT SELECT ON redis_zsets TO PUBLIC; + RAISE NOTICE 'Permissions granted on all Redis foreign tables'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to grant permissions: %', SQLERRM; + END; + \$\$; + + -- Final verification + SELECT 'Redis FDW setup completed successfully!' as status; +EOSQL + log_info "Redis FDW SQL setup completed successfully" +else + log_error "Redis FDW SQL setup failed with exit code $?" + exit 1 +fi + +log_info "Redis FDW initialization complete" diff --git a/docker-compose.yml b/docker-compose.yml index 5561ebc..f44dfc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,18 @@ x-environment: &common-environment - MARIADB_HOST: ${MARIADB_HOST} - MARIADB_USER: ${MARIADB_USER} - MARIADB_PASSWORD: ${MARIADB_PASSWORD} - MARIADB_PORT: ${MARIADB_PORT} - MARIADB_DB: ${MARIADB_DB} + DATABASE_HOST: ${DATABASE_HOST} + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + DATABASE_PORT: ${DATABASE_PORT} + DATABASE_DB: ${DATABASE_DB} + + REDIS_HOST: ${REDIS_HOST} + REDIS_PASSWORD: ${REDIS_PASSWORD} + REDIS_EXTERNAL_PORT: ${REDIS_EXTERNAL_PORT} TEST_SERVER: ${TEST_SERVER} + DATABASE_ADDR: ${DATABASE_ADDR} + DB_ADDR: ${DB_ADDR} DB_PORT: ${DB_PORT} @@ -15,32 +21,86 @@ x-environment: &common-environment WEB_APP_URL: ${WEB_APP_URL} WEB_APP_KEY: ${WEB_APP_KEY} + ADMIN_PAGE_IP: ${ADMIN_PAGE_IP} + ADMIN_PAGE_IP1: ${ADMIN_PAGE_IP1} + ADMIN_PAGE_IP2: ${ADMIN_PAGE_IP2} + ADMIN_PAGE_IP3: ${ADMIN_PAGE_IP3} + name: Metin2 services: - # MariaDB Database - mariadb: - image: mariadb:lts + # Database + database: + image: ${DB_IMAGE} restart: always environment: - # Password for root access - MARIADB_ROOT_PASSWORD: ${MARIADB_PASSWORD} + POSTGRES_USER: ${DATABASE_USER:-root} + POSTGRES_DB: ${DATABASE_DB:-metin2} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-rootpassword} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PASSWORD: ${REDIS_PASSWORD} ports: - - "${MARIADB_EXTERNAL_PORT}:${MARIADB_PORT}" + - "${DATABASE_EXTERNAL_PORT:-5432}:${DATABASE_PORT:-5432}" expose: - - ${MARIADB_PORT} + - "${DATABASE_PORT:-5432}" volumes: - - ./storage/database/:/var/lib/mysql/ - - ./assets/db-init/:/docker-entrypoint-initdb.d/:ro + - ./storage/database:/var/lib/postgresql + - ./assets/postgres/db-init:/docker-entrypoint-initdb.d:ro healthcheck: - test: - [ - "CMD", - "healthcheck.sh", - "--su-mysql", - "--connect", - "--innodb_initialized", - ] + test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-root}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - metin2 + depends_on: + redis: + condition: service_healthy + + pgbouncer: + image: docker.io/bitnamilegacy/pgbouncer:latest + restart: always + environment: + POSTGRESQL_HOST: database + POSTGRESQL_PORT: ${DATABASE_PORT:-5432} + POSTGRESQL_USERNAME: ${DATABASE_USER:-root} + POSTGRESQL_PASSWORD: ${DATABASE_PASSWORD:-rootpassword} + POSTGRESQL_DATABASE: ${DATABASE_DB:-metin2} + PGBOUNCER_DATABASE: ${DATABASE_DB:-metin2} + PGBOUNCER_PORT: 6432 + PGBOUNCER_POOL_MODE: transaction + PGBOUNCER_MAX_CLIENT_CONN: 500 + PGBOUNCER_DEFAULT_POOL_SIZE: 20 + PGBOUNCER_MIN_POOL_SIZE: 5 + PGBOUNCER_RESERVE_POOL_SIZE: 3 + PGBOUNCER_MAX_DB_CONNECTIONS: 50 + PGBOUNCER_QUERY_TIMEOUT: 30 + PGBOUNCER_IDLE_TRANSACTION_TIMEOUT: 60 + PGBOUNCER_SERVER_IDLE_TIMEOUT: 600 + PGBOUNCER_STATS_USERS: ${DATABASE_USER:-root} + PGBOUNCER_IGNORE_STARTUP_PARAMETERS: extra_float_digits + ports: + - "${PGBOUNCER_EXTERNAL_PORT:-6432}:6432" + expose: + - "6432" + depends_on: + database: + condition: service_healthy + networks: + - metin2 + + redis: + image: redis:7 + restart: always + ports: + - "${REDIS_EXTERNAL_PORT:-6379}:6379" + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] + environment: + - REDIS_PASSWORD=${REDIS_PASSWORD} + volumes: + - ./storage/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 @@ -58,7 +118,7 @@ services: volumes: - ./storage/log/db/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -69,7 +129,6 @@ services: restart: always environment: <<: *common-environment - MARIADB_DB: ${MARIADB_DB} GAME_HOSTNAME: auth GAME_CHANNEL: 1 GAME_AUTH_SERVER: master @@ -84,7 +143,7 @@ services: volumes: - ./storage/log/auth/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -111,7 +170,7 @@ services: - ./storage/log/ch1/first/:/app/log/ - ./storage/mark/:/app/mark/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -136,7 +195,7 @@ services: volumes: - ./storage/log/ch1/game1/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -161,7 +220,7 @@ services: volumes: - ./storage/log/ch1/game2/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -186,7 +245,7 @@ services: volumes: - ./storage/log/ch1/game3/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -211,7 +270,7 @@ services: volumes: - ./storage/log/ch1/game4/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -236,7 +295,7 @@ services: volumes: - ./storage/log/ch1/game5/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2 @@ -262,7 +321,7 @@ services: volumes: - ./storage/log/game99/:/app/log/ depends_on: - mariadb: + database: condition: service_healthy networks: - metin2