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.
23 KiB
Installation Guide
This guide covers three deployment methods:
- Development Setup — for local development
- Docker Production — single-command deployment with Docker Compose
- 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 thedaemonservice on its own machine and publishes50051there. - 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
- Open your panel URL in a browser
- Login with:
admin@gamepanel.local/admin123 - Immediately change the admin password via account settings
Create Your First Server
- Create an Organization — Click "New Organization" on the home page
- Add a Node — Go to Nodes, add your daemon node (FQDN + ports)
- Add Allocations — Assign IP:port pairs to the node
- Create a Server — Use the creation wizard: pick a game, node, and resources
- 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_URLis correct and PostgreSQL is running - Ensure migrations have been applied:
pnpm db:migrate - Check logs:
journalctl -u gamepanel-api -fordocker compose logs api
Daemon can't connect
- Verify
api_urlin daemon config points to the API - Check
node_tokenmatches 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
dockergroup:usermod -aG docker <user> - Or run the daemon with appropriate privileges
protoc not found (daemon build)
- Ubuntu:
sudo apt install protobuf-compiler - macOS:
brew install protobuf - Or download from github.com/protocolbuffers/protobuf/releases
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 |