Files
hibna c1adb94abb Format the repository with Prettier
CI has been running `prettier --check` against a tree that was never
formatted, so the check reported 63 files and failed every run. Nothing
here is a behaviour change: `pnpm lint` and the four typecheck builds
pass exactly as before.

conduit-bringup-artifacts is added to .prettierignore instead. Those
files are captured bring-up reports, not maintained sources; reflowing
them would only churn a record of what happened.
2026-08-02 21:08:12 +03:00

23 KiB

Installation Guide

This guide covers three deployment methods:

  1. Development Setup — for local development
  2. Docker Production — single-command deployment with Docker Compose
  3. Manual Production — step-by-step on Ubuntu 22.04+

Prerequisites

All Methods

  • Git
  • A PostgreSQL 16+ database (or use the included Docker Compose)

Development

  • Node.js 20+ (nodejs.org)
  • pnpm 9.15+ (corepack enable && corepack prepare pnpm@9.15.4 --activate)
  • Rust 1.83+ (rustup.rs)
  • protoc (Protocol Buffers compiler) — required for the daemon's gRPC build
  • Docker — for running PostgreSQL and Redis locally

Docker Production

  • Docker 24+ with Docker Compose v2
  • At least 2 GB RAM and 10 GB disk for the panel itself
  • Additional resources for game servers on daemon nodes

1. Development Setup

1.1 Clone and Install

git clone https://github.com/your-org/source-gamepanel.git
cd source-gamepanel
pnpm install

1.2 Environment Configuration

cp .env.example .env

Edit .env and set at minimum:

# Generate secure secrets:
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=<your-64-byte-hex>
JWT_REFRESH_SECRET=<another-64-byte-hex>

# Database (defaults work with docker-compose.dev.yml)
DATABASE_URL=postgresql://gamepanel:gamepanel@localhost:5432/gamepanel

1.3 Start Infrastructure

# Start PostgreSQL + Redis
docker compose -f docker-compose.dev.yml up -d

1.4 Database Setup

# Sync the schema from packages/database/src/schema, then apply the
# hand-written data migrations in packages/database/drizzle/*.sql
pnpm db:migrate

# Seed admin user and default games
pnpm db:seed

All three steps are idempotent, so re-running them after a git pull is the normal way to pick up schema and default-game changes.

After seeding, you'll have:

  • Admin account: admin@gamepanel.local / admin123
  • Games: Minecraft Java & Bedrock, CS2, Terraria, Rust, Satisfactory, FiveM, ARK: Survival Evolved

1.5 Start Development Servers

# Start API (port 3000) + Web (port 5173) via Turborepo
pnpm dev

The web dev server proxies /api and /socket.io requests to the API automatically.

Open http://localhost:5173 in your browser.

1.6 Daemon (Optional)

The Rust daemon manages Docker containers on game server nodes. For development you can run it locally:

# Ensure protoc is installed
protoc --version  # Should show libprotoc 3.x or higher

# If not installed:
# Ubuntu: sudo apt install protobuf-compiler
# macOS: brew install protobuf
# Windows: choco install protoc (or download from GitHub releases)

cd apps/daemon
cargo run

The daemon reads its config from /etc/gamepanel/config.yml or the path in DAEMON_CONFIG env var. For development, it falls back to defaults (API at localhost:3000, dev token).

1.7 Useful Commands

pnpm build            # Build all packages
pnpm lint             # ESLint across all packages
pnpm format           # Prettier format
pnpm format:check     # Check formatting without modifying
pnpm db:studio        # Open Drizzle Studio (visual DB browser)

# Daemon
cd apps/daemon
cargo test            # Run unit tests (3 tests: Minecraft parser, CS2 parser)
cargo clippy          # Rust linter
cargo build --release # Production build

2. Docker Production Deployment

The whole panel comes up with two commands. TLS and domain handling are deliberately not included — the panel serves plain HTTP and you put your own reverse proxy in front of it (see 2.6).

2.1 Install

git clone https://github.com/your-org/source-gamepanel.git
cd source-gamepanel

./scripts/install.sh
docker compose up -d --build

scripts/install.sh generates .env with fresh secrets, writes a daemon-config.yml with a matching node token, and creates the host data directories. It never overwrites files that already exist, so it is safe to re-run.

Then open http://<server-ip>:80 and sign in with admin@gamepanel.local / admin123 — change the password immediately.

2.2 What gets started

Service Port Description
postgres internal PostgreSQL database
redis internal Rate limiting & cache
migrate Applies the schema + seed, then exits
api internal Fastify REST API
web WEB_PORT (80) nginx + React SPA, proxies /api and /socket.io
daemon DAEMON_GRPC_PORT (50051) Rust gRPC daemon

Only web and daemon publish ports. Postgres, Redis and the API stay on the internal Compose network.

The migrate service runs on every docker compose up; all three of its steps (drizzle-kit push, the data migrations, the seed) are idempotent.

2.3 Register the node

In the panel, create a node with:

Field Value
FQDN host.docker.internal (or the host's IP/hostname)
gRPC port the DAEMON_GRPC_PORT from .env
Daemon token the DAEMON_TOKEN from .env

2.4 Where game server files live

DAEMON_DATA_PATH in .env (default /var/lib/gamepanel/servers) is a host directory. The daemon runs in a container but creates game containers through the host's Docker socket, so their bind mounts are resolved by the host, not by the daemon container.

That is why the same path is passed twice — once as the daemon's own bind mount and once as DAEMON_HOST_DATA_PATH. If you change DAEMON_DATA_PATH, both follow automatically. Do not replace the bind mount with a named volume: the daemon and the game servers would then read and write two different directories, and files edited in the panel would never reach the game.

2.5 Verify

docker compose ps
docker compose logs -f api
docker compose logs -f daemon

curl -s http://localhost/api/health
# {"status":"ok","timestamp":"..."}

2.6 TLS, domain and reverse proxy

The panel intentionally ships without certificate handling. Terminate TLS in whatever proxy you already run and forward to WEB_PORT. WebSocket upgrades must be forwarded too, otherwise the live console will not connect.

Set CORS_ORIGIN in .env to the exact origin users open in the browser, then docker compose up -d to apply it.

Caddy (Caddyfile):

panel.example.com {
    reverse_proxy 127.0.0.1:80
}

nginx:

server {
    listen 443 ssl;
    server_name panel.example.com;

    ssl_certificate     /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:80;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

If the proxy runs on the same host, bind the panel to loopback only by setting WEB_PORT=127.0.0.1:8080 in .env.

2.7 Updating

git pull
docker compose up -d --build

The migrate service applies schema and seed changes on every start, so no extra step is needed.

2.8 Upgrading from a pre-install.sh deployment

Older docker-compose.yml versions stored the daemon's server directory in a named volume (daemon_data). That never matched what the game containers actually used: their bind mounts were resolved by the host, so the real game files ended up in /var/lib/gamepanel/servers on the host while the panel read and wrote the named volume. Editing a config in the panel appeared to work and then had no effect, and files could look like they reset themselves.

The compose file now bind-mounts the host directory directly, so after upgrading, the panel sees the same files the game servers do. Nothing needs to be moved — the game files were already on the host.

If you had put files into the old named volume through the panel and want them back, copy them out before removing it:

docker run --rm -v gamepanel_daemon_data:/from -v /var/lib/gamepanel/servers:/to   alpine sh -c 'cp -an /from/. /to/'
docker volume rm gamepanel_daemon_data gamepanel_daemon_backups

Also note that postgres, redis and api no longer publish host ports; only web and daemon do. If you were proxying straight to API_PORT, point your proxy at WEB_PORT instead — nginx forwards /api and /socket.io.


3. Manual Production Setup (Ubuntu 22.04+)

3.1 System Dependencies

sudo apt update && sudo apt upgrade -y

# Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# pnpm
corepack enable
corepack prepare pnpm@9.15.4 --activate

# PostgreSQL 16
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql-16

# Redis
sudo apt install -y redis-server

# Docker (for game containers)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

# Rust (for daemon)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

# protoc (for gRPC)
sudo apt install -y protobuf-compiler

# nginx (reverse proxy)
sudo apt install -y nginx certbot python3-certbot-nginx

3.2 Database Setup

sudo -u postgres psql << 'EOF'
CREATE USER gamepanel WITH PASSWORD 'your-strong-password';
CREATE DATABASE gamepanel OWNER gamepanel;
GRANT ALL PRIVILEGES ON DATABASE gamepanel TO gamepanel;
EOF

3.3 Redis Configuration

sudo sed -i 's/# requirepass foobared/requirepass your-redis-password/' /etc/redis/redis.conf
sudo systemctl restart redis-server

3.4 Application Setup

# Clone
cd /opt
sudo git clone https://github.com/your-org/source-gamepanel.git
sudo chown -R $USER:$USER source-gamepanel
cd source-gamepanel

# Install
pnpm install

# Environment
cp .env.example .env
nano .env  # Set all production values

# Build
pnpm build

# Database
pnpm db:migrate
pnpm db:seed

# Build daemon
cd apps/daemon
cargo build --release
sudo cp target/release/gamepanel-daemon /usr/local/bin/

3.5 Daemon Configuration

sudo mkdir -p /etc/gamepanel /var/lib/gamepanel/{servers,backups}

sudo tee /etc/gamepanel/config.yml << 'EOF'
api_url: "http://127.0.0.1:3000"
node_token: "generate-a-secure-token-here"
grpc_port: 50051
data_path: "/var/lib/gamepanel/servers"
backup_path: "/var/lib/gamepanel/backups"
docker:
  socket: "/var/run/docker.sock"
  network: "gamepanel_nw"
  network_subnet: "172.18.0.0/16"
EOF

3.6 Systemd Services

API Service:

sudo tee /etc/systemd/system/gamepanel-api.service << 'EOF'
[Unit]
Description=GamePanel API
After=network.target postgresql.service redis-server.service
Requires=postgresql.service

[Service]
Type=simple
User=gamepanel
WorkingDirectory=/opt/source-gamepanel
ExecStart=/usr/bin/node apps/api/dist/index.js
Restart=always
RestartSec=5
EnvironmentFile=/opt/source-gamepanel/.env
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
EOF

Daemon Service:

sudo tee /etc/systemd/system/gamepanel-daemon.service << 'EOF'
[Unit]
Description=GamePanel Daemon
After=network.target docker.service
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/local/bin/gamepanel-daemon
Restart=always
RestartSec=5
Environment=DAEMON_CONFIG=/etc/gamepanel/config.yml
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target
EOF

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now gamepanel-api
sudo systemctl enable --now gamepanel-daemon

3.7 Web Build + nginx

# Build the SPA
cd /opt/source-gamepanel/apps/web
pnpm build  # outputs to dist/

# Copy to nginx
sudo mkdir -p /var/www/gamepanel
sudo cp -r dist/* /var/www/gamepanel/

nginx site config:

sudo tee /etc/nginx/sites-available/gamepanel << 'EOF'
server {
    listen 80;
    server_name panel.yourdomain.com;
    root /var/www/gamepanel;
    index index.html;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;

    # API proxy
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Socket.IO
    location /socket.io/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    # Static assets
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }
}
EOF

sudo ln -sf /etc/nginx/sites-available/gamepanel /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

3.8 TLS with Let's Encrypt

sudo certbot --nginx -d panel.yourdomain.com

Certbot will automatically configure nginx for HTTPS and set up auto-renewal.

3.9 Firewall

sudo ufw allow 22/tcp      # SSH
sudo ufw allow 80/tcp      # HTTP
sudo ufw allow 443/tcp     # HTTPS
sudo ufw allow 50051/tcp   # gRPC (daemon)
# Open game server port ranges as needed:
sudo ufw allow 25565/tcp   # Minecraft
sudo ufw allow 27015/tcp   # CS2
sudo ufw enable

4. Control Panel Deployment (pre-built images)

Sections 2 and 3 build from a checkout on the server. A control panel does not have one: it writes a compose file and an .env into its own project directory and runs docker compose up. Anything with a build: stanza fails there — the build context simply is not on disk.

docker-compose.panel.yml exists for that case. Every service references a published image, so the stack installs on a server that has never seen this repository. It was written against WebPanel but nothing in it is panel-specific.

4.1 Publish the images

.github/workflows/ci.yml pushes four images to this Gitea instance's own container registry on every v* tag:

Image Contents
gamepanel-api Fastify API
gamepanel-migrate The API Dockerfile's migrate stage, run once before the API starts
gamepanel-web SPA + nginx, built with VITE_API_URL=/api
gamepanel-daemon Rust daemon

Add a REGISTRY_TOKEN repository secret with package write scope, then:

git tag v0.1.0 && git push origin v0.1.0

4.2 Prepare the host

sudo mkdir -p /etc/gamepanel /var/lib/gamepanel/servers /var/lib/gamepanel/backups
sudo cp daemon-config.yml /etc/gamepanel/daemon-config.yml
sudo sed -i 's/CHANGE_ME_GENERATE_A_SECURE_TOKEN/'"$(openssl rand -hex 32)"'/'   /etc/gamepanel/daemon-config.yml

Note the token you generated — the panel needs the same value when you register the node. If the panel has a file manager, both steps can be done from it.

4.3 Install

Paste docker-compose.panel.yml into the panel's custom-compose screen and set:

Variable Example Notes
REGISTRY gits.hibna.com.tr/hibna Namespace holding the four images
TAG v0.1.0 The tag you pushed
HOST_PORT 8096 Not 80 if the panel's own web server owns it
DB_PASSWORD, REDIS_PASSWORD openssl rand -hex 24
JWT_SECRET, JWT_REFRESH_SECRET openssl rand -hex 64
CORS_ORIGIN https://panel.example.com Must match the address the browser uses

The published port is called HOST_PORT because panels commonly reverse-proxy "the" port of an installation and need to know which one that is when a stack publishes more than one.

If the registry is private, the host needs docker login once — panels pull anonymously otherwise. On Gitea the package can also be made public while the repository stays private.

4.4 Notes

  • The daemon holds the Docker socket. That is root-equivalent access to every container on the machine, the panel's own containers included. Running the daemon on a separate node — which the multi-node architecture is built for — keeps the game hosts and the control plane apart.
  • Nothing publishes gRPC on a single host. The API reaches the daemon over the compose network as daemon:50051. A remote node runs the daemon service on its own machine and publishes 50051 there.
  • Game server ports are opened by the daemon on the host; a panel with a default-deny firewall needs an explicit rule for the range you hand out.

Post-Installation

First Login

  1. Open your panel URL in a browser
  2. Login with: admin@gamepanel.local / admin123
  3. Immediately change the admin password via account settings

Create Your First Server

  1. Create an Organization — Click "New Organization" on the home page
  2. Add a Node — Go to Nodes, add your daemon node (FQDN + ports)
  3. Add Allocations — Assign IP:port pairs to the node
  4. Create a Server — Use the creation wizard: pick a game, node, and resources
  5. Start the Server — Use the power controls on the console page

Adding a Remote Daemon Node

On the remote machine:

# Install Docker
curl -fsSL https://get.docker.com | sh

# Install the daemon binary
scp user@panel-server:/usr/local/bin/gamepanel-daemon /usr/local/bin/

# Configure
mkdir -p /etc/gamepanel /var/lib/gamepanel/{servers,backups}

cat > /etc/gamepanel/config.yml << EOF
api_url: "https://panel.yourdomain.com"
node_token: "<token-from-panel>"
grpc_port: 50051
EOF

# Create systemd service (same as above)
# Start it
systemctl enable --now gamepanel-daemon

Then add the node in the panel with the remote machine's FQDN.


Troubleshooting

API won't start

  • Check DATABASE_URL is correct and PostgreSQL is running
  • Ensure migrations have been applied: pnpm db:migrate
  • Check logs: journalctl -u gamepanel-api -f or docker compose logs api

Daemon can't connect

  • Verify api_url in daemon config points to the API
  • Check node_token matches what's stored in the panel's nodes table
  • Ensure the daemon's gRPC port (50051) is open

Web shows blank page

  • Build the SPA: pnpm --filter @source/web build
  • Check nginx config: sudo nginx -t
  • Verify API proxy is working: curl http://localhost:3000/api/health

Docker permission denied

  • Ensure the daemon user is in the docker group: usermod -aG docker <user>
  • Or run the daemon with appropriate privileges

protoc not found (daemon build)


Updating

Docker

cd /opt/source-gamepanel
git pull
docker compose up -d --build

Manual

cd /opt/source-gamepanel
git pull
pnpm install
pnpm build
pnpm db:migrate

# Rebuild daemon
cd apps/daemon && cargo build --release
sudo cp target/release/gamepanel-daemon /usr/local/bin/

# Rebuild web
cd ../web && pnpm build
sudo cp -r dist/* /var/www/gamepanel/

# Restart services
sudo systemctl restart gamepanel-api gamepanel-daemon
sudo systemctl reload nginx

Environment Variables Reference

Variable Default Description
DATABASE_URL PostgreSQL connection string
DB_USER gamepanel PostgreSQL username (Docker)
DB_PASSWORD gamepanel PostgreSQL password (Docker)
DB_NAME gamepanel Database name (Docker)
DB_PORT 5432 PostgreSQL exposed port
REDIS_URL Redis connection string
REDIS_PASSWORD gamepanel Redis password
PORT 3000 API listen port
HOST 0.0.0.0 API listen host
NODE_ENV development Environment mode
JWT_SECRET Required. Access token signing key
JWT_REFRESH_SECRET Required. Refresh token signing key
CORS_ORIGIN http://localhost:5173 Allowed CORS origin
RATE_LIMIT_MAX 100 Max requests per window
RATE_LIMIT_WINDOW_MS 60000 Rate limit window (ms)
WEB_PORT 80 Web nginx exposed port
API_PORT 3000 API exposed port (Docker)
DAEMON_CONFIG /etc/gamepanel/config.yml Daemon config file path
DAEMON_GRPC_PORT 50051 Daemon gRPC exposed port