# 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](https://nodejs.org)) - **pnpm** 9.15+ (`corepack enable && corepack prepare pnpm@9.15.4 --activate`) - **Rust** 1.83+ ([rustup.rs](https://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 ```bash git clone https://github.com/your-org/source-gamepanel.git cd source-gamepanel pnpm install ``` ### 1.2 Environment Configuration ```bash cp .env.example .env ``` Edit `.env` and set at minimum: ```env # Generate secure secrets: # node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" JWT_SECRET= JWT_REFRESH_SECRET= # Database (defaults work with docker-compose.dev.yml) DATABASE_URL=postgresql://gamepanel:gamepanel@localhost:5432/gamepanel ``` ### 1.3 Start Infrastructure ```bash # Start PostgreSQL + Redis docker compose -f docker-compose.dev.yml up -d ``` ### 1.4 Database Setup ```bash # 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 ```bash # 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: ```bash # 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 ```bash 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 ```bash 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://: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 ```bash 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: ```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 ```bash 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: ```bash 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 ```bash 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 ```bash 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 ```bash sudo sed -i 's/# requirepass foobared/requirepass your-redis-password/' /etc/redis/redis.conf sudo systemctl restart redis-server ``` ### 3.4 Application Setup ```bash # 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 ```bash 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:** ```bash 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:** ```bash 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:** ```bash sudo systemctl daemon-reload sudo systemctl enable --now gamepanel-api sudo systemctl enable --now gamepanel-daemon ``` ### 3.7 Web Build + nginx ```bash # 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:** ```bash 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 ```bash sudo certbot --nginx -d panel.yourdomain.com ``` Certbot will automatically configure nginx for HTTPS and set up auto-renewal. ### 3.9 Firewall ```bash 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](https://gits.hibna.com.tr/hibna/Source-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: ```bash git tag v0.1.0 && git push origin v0.1.0 ``` ### 4.2 Prepare the host ```bash 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: ```bash # 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: "" 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 ` - 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](https://github.com/protocolbuffers/protobuf/releases) --- ## Updating ### Docker ```bash cd /opt/source-gamepanel git pull docker compose up -d --build ``` ### Manual ```bash 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 |