Why Monitor Your Game Server with Docker Compose?
Running a Rust or Arma 3 dedicated server without monitoring is like flying blind. Players disconnect, the process silently crashes, memory leaks pile up, and you find out only when someone pings you on Discord at 2 AM. In this tutorial we wire up a full monitoring stack using Docker Compose: Prometheus scrapes metrics, Grafana visualizes them, and a custom Python exporter translates game server query responses into metrics Prometheus can consume. Everything runs in containers alongside your game server process.
Architecture Overview
The stack has four services: the game server itself (we use a community Rust dedicated server image as the primary example, with Arma 3 notes where behavior differs), a Python-based exporter that queries the server via the Steam Query Protocol (A2S), Prometheus for time-series storage, and Grafana for dashboards. All services share a single Docker network so the exporter can reach the game server by container name.
The Python A2S Exporter
The exporter uses the python-a2s library to query any Source/Steam-compatible server and the official prometheus_client library to expose metrics on port 8000. It polls every 15 seconds and exposes player count, max players, server ping, map name as a label, and a binary up/down gauge. Save this as exporter/exporter.py.
#!/usr/bin/env python3
# exporter/exporter.py
# Requires: pip install python-a2s prometheus_client
import time
import os
import a2s
from prometheus_client import start_http_server, Gauge, Info
GAME_HOST = os.getenv("GAME_HOST", "gameserver")
GAME_PORT = int(os.getenv("GAME_QUERY_PORT", "27015"))
POLL_INTERVAL = int(os.getenv("POLL_INTERVAL", "15"))
# Prometheus metrics
server_up = Gauge("gameserver_up", "1 if server is reachable, 0 otherwise")
player_count = Gauge("gameserver_players_current", "Current player count")
player_max = Gauge("gameserver_players_max", "Maximum player slots")
server_ping = Gauge("gameserver_ping_seconds", "Round-trip query latency in seconds")
server_info = Info("gameserver", "Static server metadata")
def collect_metrics():
address = (GAME_HOST, GAME_PORT)
try:
start = time.monotonic()
info = a2s.info(address, timeout=5)
latency = time.monotonic() - start
server_up.set(1)
player_count.set(info.player_count)
player_max.set(info.max_players)
server_ping.set(round(latency, 4))
server_info.info({
"map": info.map_name,
"game": info.game,
"server_name": info.server_name,
"version": info.version,
})
print(f"[OK] {info.server_name} | map={info.map_name} "
f"players={info.player_count}/{info.max_players} "
f"ping={latency:.3f}s")
except Exception as exc:
server_up.set(0)
player_count.set(0)
server_ping.set(0)
print(f"[ERROR] Could not reach {GAME_HOST}:{GAME_PORT} โ {exc}")
if __name__ == "__main__":
start_http_server(8000)
print(f"Exporter listening on :8000, polling {GAME_HOST}:{GAME_PORT} "
f"every {POLL_INTERVAL}s")
while True:
collect_metrics()
time.sleep(POLL_INTERVAL)
Dockerfile for the Exporter
Create exporter/Dockerfile with a minimal Python 3.12 slim image. We pin library versions for reproducibility.
FROM python:3.12-slim
WORKDIR /app
COPY exporter.py .
RUN pip install --no-cache-dir python-a2s==1.3.0 prometheus_client==0.20.0
CMD ["python", "-u", "exporter.py"]
Docker Compose Stack
The docker-compose.yml below wires everything together. The Rust server uses the didstopia/rust-server community image. For Arma 3 substitute your preferred image and change GAME_QUERY_PORT to 2303 (Arma 3 uses a separate query port that is game port + 1 by default). Prometheus and Grafana data are persisted in named volumes so you survive container restarts.
version: "3.9"
services:
gameserver:
image: didstopia/rust-server:latest
container_name: rust_gameserver
ports:
- "28015:28015/udp"
- "28015:28015/tcp"
- "28016:28016/tcp"
environment:
RUST_SERVER_STARTUP_ARGUMENTS: "-batchmode -load +server.port 28015 +server.queryport 27015 +server.maxplayers 50"
RUST_SERVER_IDENTITY: "devops_days_server"
volumes:
- rust_data:/steamcmd/rust
networks:
- monitoring
exporter:
build: ./exporter
container_name: gameserver_exporter
environment:
GAME_HOST: gameserver
GAME_QUERY_PORT: "27015"
POLL_INTERVAL: "15"
ports:
- "8000:8000"
depends_on:
- gameserver
networks:
- monitoring
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.51.2
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=30d"
ports:
- "9090:9090"
networks:
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:10.4.2
container_name: grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: "changeme_in_prod"
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- monitoring
restart: unless-stopped
volumes:
rust_data:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: bridge
Prometheus Configuration
Create prometheus/prometheus.yml. The scrape interval matches the exporter poll interval so you never miss a data point.
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: gameserver
static_configs:
- targets: ["exporter:8000"]
labels:
game: rust
environment: production
Grafana Auto-Provisioning
Create grafana/provisioning/datasources/prometheus.yml so Grafana connects to Prometheus automatically on first boot without manual UI clicks.
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
Useful Grafana Queries
Once the stack is running open Grafana at http://localhost:3000, log in with admin / changeme_in_prod, and create a dashboard. Recommended PromQL panels: current players gameserver_players_current, player fill percentage gameserver_players_current / gameserver_players_max * 100, server availability over 1 hour avg_over_time(gameserver_up[1h]) * 100, and ping trend rate(gameserver_ping_seconds[5m]). Add an alert rule on gameserver_up == 0 for 2 minutes to get notified before players start complaining.
Arma 3 Specific Notes
Arma 3 also supports the Steam A2S query protocol so the same exporter works without code changes. Set GAME_QUERY_PORT to your Arma query port (default game port 2302, query port 2303). Arma 3 dedicated servers require a valid Steam account and the server files installed via SteamCMD; use the brettanomyces/arma3server or hermsi1337/docker-arma3server community images and map the correct UDP ports. The monitoring stack is identical.
Running the Stack
Clone or create the directory structure, then run docker compose up -d --build. Watch the exporter logs with docker compose logs -f exporter. The Rust server takes several minutes to download on first run. Once it is up you will see OK lines in the exporter log and metrics flowing into Prometheus within 15 seconds. The full project structure is: docker-compose.yml, exporter/Dockerfile, exporter/exporter.py, prometheus/prometheus.yml, grafana/provisioning/datasources/prometheus.yml.
