Complete installation guide
This guide takes you from an empty Linux server to a working HM Stats installation with:
- HM Stats Server
- PostgreSQL
- HM Stats Dashboard
- Caddy / automatic HTTPS
- OIDC administrator login
- a workspace
- a Minecraft server registration
- a season
- a restricted client API key
- a configured Minecraft 26.2 client
The production stack is defined by the docker-compose.yml and Caddyfile in this documentation repository. The Compose file builds the current server and dashboard repositories locally, so the installation does not depend on a pre-published application image.
Recommended: use a dedicated VPS or server with a normal Linux distribution such as Ubuntu or Debian. Do not expose PostgreSQL directly to the internet.
For all examples, replace:
stats.example.comwith your real HM Stats hostname.
1. Architecture
Internet
|
| HTTPS :443
v
Caddy
|--------------------> HM Stats Dashboard
|
+---- /api/* --------> HM Stats Server :8000
|
+---- /auth/* -------> HM Stats Server :8000
|
v
PostgreSQLThe Minecraft player installs the HM Stats mod on their own client:
Minecraft client
|
| HTTPS + client API key
v
HM Stats ServerThe Minecraft server itself does not need the HM Stats mod.
2. Requirements
Server requirements
You need:
- Linux
- Docker Engine
- Docker Compose v2
- Git
- OpenSSL
The supplied Compose stack contains Caddy, so you do not need a host-level web server.
Minecraft client requirements
The current client targets:
- Minecraft 26.2
- Java 25
- Fabric Loader 0.19.3 or newer
- Fabric API compatible with Minecraft 26.2
- HM Stats client JAR
3. Install Docker and Git
On Ubuntu/Debian, install Docker using Docker's official installation instructions, then verify:
docker --version
docker compose version
git --version
openssl versionThe exact Docker installation differs by Linux distribution. The important requirement is that docker compose works without using the old standalone docker-compose command.
4. Clone the three repositories
Create a deployment directory:
sudo mkdir -p /opt/hm-stats
sudo chown "$USER":"$USER" /opt/hm-stats
cd /opt/hm-statsClone:
git clone https://github.com/henrymmey/minecraft-stats-docs.git
git clone https://github.com/henrymmey/minecraft-stats-server.git
git clone https://github.com/henrymmey/minecraft-stats-dashboard.gitYour directory should now look like:
/opt/hm-stats/
├── minecraft-stats-docs/
├── minecraft-stats-server/
└── minecraft-stats-dashboard/The Compose file expects exactly these sibling directories.
5. Configure DNS
Choose the public hostname users will use for HM Stats.
Example:
stats.example.comAt your DNS provider create an A record:
A stats.example.com <YOUR_SERVER_IPV4>When you have working IPv6, also add:
AAAA stats.example.com <YOUR_SERVER_IPV6>Check the result:
dig +short stats.example.comIt must resolve to the server that will run Caddy.
Firewall
Allow inbound:
- TCP 80
- TCP 443
Do not allow PostgreSQL TCP 5432 from the public internet.
The supplied Compose stack does not publish PostgreSQL to the host.
6. Create the deployment files
Use the production Compose and Caddy files from the docs repository:
cd /opt/hm-stats
cp minecraft-stats-docs/docker-compose.yml .
cp minecraft-stats-docs/Caddyfile .
cp minecraft-stats-docs/.env.example .envYou now have:
/opt/hm-stats/
├── .env
├── docker-compose.yml
├── Caddyfile
├── minecraft-stats-docs/
├── minecraft-stats-server/
└── minecraft-stats-dashboard/Protect the environment file:
chmod 600 /opt/hm-stats/.env7. Configure .env
Edit:
/opt/hm-stats/.envMinimal production configuration:
APP_DOMAIN=stats.example.com
APP_URL=https://stats.example.com
APP_KEY=
POSTGRES_DB=hm_stats
POSTGRES_USER=hm_stats
POSTGRES_PASSWORD=CHANGE_ME
API_KEY_PEPPER=CHANGE_ME_TOO
OIDC_ISSUER=https://your-oidc-provider.example/...
OIDC_CLIENT_ID=hm-stats
OIDC_CLIENT_SECRET=CHANGE_ME
OIDC_REDIRECT_URI=https://stats.example.com/auth/callbackGenerate strong secrets
Generate separate random values:
openssl rand -base64 32
openssl rand -hex 32Use one value as POSTGRES_PASSWORD and another as API_KEY_PEPPER.
Do not reuse either value for an unrelated secret.
APP_KEY
The Laravel APP_KEY is required.
After the first image build you can generate it with:
docker compose run --rm --no-deps api php artisan key:generate --showCopy the returned value into:
APP_KEY=base64:...Do not regenerate it on every restart. Keep the same APP_KEY for the lifetime of the installation.
OIDC
The exact values depend on your OIDC provider. See OIDC setup.
8. Configure the public hostname in Caddy
Open:
/opt/hm-stats/CaddyfileIt should contain your hostname, for example:
stats.example.com {
handle /api/* {
reverse_proxy api:8000
}
handle /auth/* {
reverse_proxy api:8000
}
handle {
reverse_proxy dashboard:80
}
}The routing is important:
/api/*→ Laravel API/auth/*→ OIDC login/callback/logout- everything else → React dashboard
You do not need to configure TLS certificates manually. Caddy requests and renews them automatically.
9. Build and start HM Stats
From the deployment directory:
cd /opt/hm-stats
docker compose up -d --buildThe first build may take longer because both the server and dashboard images are built locally.
Check:
docker compose psYou should have four services:
api
dashboard
postgres
caddyServer startup
The HM Stats Server container automatically runs:
php artisan migrate --forcebefore starting Laravel on port 8000.
PostgreSQL stays on the internal Docker network.
10. Test the installation
First test through localhost:
curl -i http://127.0.0.1:8000/api/v1/health/live
curl -i http://127.0.0.1:8000/api/v1/health/readyThen test the public domain:
curl -i https://stats.example.com/api/v1/health/live
curl -i https://stats.example.com/api/v1/health/readyExpected successful response:
{"status":"ok"}health/ready also checks PostgreSQL. HTTP 503 means the application cannot currently confirm database readiness.
For problems:
docker compose logs -f api
docker compose logs -f postgres
docker compose logs -f caddy11. Configure OIDC administrator login
HM Stats administrators authenticate through OpenID Connect.
There is no local HM Stats dashboard password.
At your OIDC provider, create a confidential web application/client.
Use this exact redirect URI:
https://stats.example.com/auth/callbackHM Stats requests:
openid profile emailThe provider should return at least:
subnameorpreferred_username
Email and profile image claims are optional.
Put the real OIDC values into .env, then recreate the API service:
cd /opt/hm-stats
docker compose up -d --force-recreate apiRead OIDC setup for provider-specific details and troubleshooting.
12. Create the first workspace
The first workspace is created through an explicit one-time bootstrap process.
Generate a token:
cd /opt/hm-stats
docker compose exec api php artisan stats:bootstrap-token "My Clan" my-clan --ttl=60The command prints:
mst_bootstrap_<uuid>_<secret>The token is:
- displayed once
- single-use
- valid for the configured TTL
- unusable after the first workspace already exists
Now open:
https://stats.example.com/auth/loginLog in with your OIDC account.
Then open:
https://stats.example.com/setupPaste the token and click Initialize workspace.
The OIDC account consuming the token becomes the initial workspace owner.
13. Register your Minecraft server
Open the dashboard:
https://stats.example.com/Go to Servers.
Create an entry using the exact Minecraft hostname and port.
Example:
| Field | Example |
|---|---|
| Name | craftattack |
| Display name | CraftAttack |
| Hostname | play.example.net |
| Port | 25565 |
The server must be enabled.
Important
HM Stats matches the reported Minecraft hostname + port.
If players connect through:
play.example.net:25565register that exact endpoint.
A mismatch causes ingestion to fail.
14. Create the season
Go to Seasons.
Create a season such as:
Name: CraftAttack 14
Slug: craftattack-14
Activate immediately: yesValid slugs use lowercase letters/numbers and hyphens:
craftattack-14Only one season is active at a time.
The ingestion server uses the active season as authority.
15. Create a client API key
Go to API Keys.
Create:
Type: Client
For one player, the recommended permissions are:
Scope:
ingest:write
UUID restriction:
the player's Minecraft UUID
Server restriction:
the registered Minecraft server
Season restriction:
the current seasonAfter creation, HM Stats displays the full secret once.
The token looks like:
mst_client_<uuid>_<secret>Save it immediately.
Do not put a client token into a public repository, website, issue or shared screenshot.
Read API Keys for the full key model.
16. Install the Minecraft client
The HM Stats mod is installed per player.
The server operator does not install it into the dedicated Minecraft server.
For the current release the player needs:
- Minecraft 26.2
- Java 25
- Fabric Loader 0.19.3 or newer
- Fabric API compatible with Minecraft 26.2
- HM Stats client JAR
The current client CI produces an artifact named:
hm-stats-clientThere is currently no automatic Modrinth or CurseForge publication configured in the repository.
For the exact filesystem paths and config format, see Client installation and configuration.
17. Configure the player
The client creates:
.minecraft/config/hm-stats.jsonA typical configuration is:
{
"enabled": true,
"api": {
"url": "https://stats.example.com",
"key": "mst_client_<uuid>_<secret>"
},
"upload": {
"intervalSeconds": 60,
"batchSize": 100,
"maxQueueSize": 5000
},
"servers": {
"allow": [
"play.example.net:25565"
]
},
"privacy": {
"sendStatistics": true,
"sendAdvancements": true,
"sendEvents": true
}
}Important:
api.url = https://stats.example.comnot:
https://stats.example.com/api/v1/ingest/batchThe client adds /api/v1/ingest/batch itself.
Server allow-list
An empty servers.allow list means all servers.
For a clan client you can explicitly add:
play.example.net:25565The comparison is case-insensitive and includes the port.
Queue
The local upload queue is stored under:
.minecraft/config/hm-stats-queue/The queue lets the client keep data during temporary network failures.
18. Verify one player end-to-end
Use one test player before distributing the setup to the whole clan.
Server side
Confirm:
- health endpoints are 200
- workspace exists
- server exists and is enabled
- season is active
- client key exists
- key restrictions match the player's UUID/server/season
Client side
Confirm:
- Minecraft is 26.2
- Fabric Loader is 0.19.3+
- HM Stats is in the correct
modsfolder hm-stats.jsonexistsenabled=true- API URL is correct
- client key is complete
Join the registered Minecraft server.
Dashboard side
Open Players.
The player should appear after the first successful ingest.
Open API Keys and check Last used.
If the timestamp changes, the key was accepted by the server.
For client-side failures, use Client troubleshooting.
For server-side failures, use Troubleshooting.
19. Add the remaining clan members
For each player:
- get their Minecraft UUID
- create a separate Client API key
- restrict it to their UUID
- restrict it to the correct server
- restrict it to the current season
- send the token and client configuration privately
- have the player install the mod
- verify Last used
Do not use one unrestricted client token for the whole clan.
A separate key per player gives you precise revocation and containment.
20. Website/API integration
Use a separate Website or Integration key for trusted server-side applications.
Typical read scopes:
players:read
stats:read
leaderboards:read
presence:readNever expose such a key in frontend JavaScript.
API documentation:
21. Backups
Back up PostgreSQL regularly.
Example:
cd /opt/hm-stats
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" > hm-stats-backup.sqlStore the backup outside the server too.
A backup should be tested by restoring it into a separate PostgreSQL environment.
Do not delete the Docker volume as a way of "starting fresh" unless you intentionally want to destroy the database.
Read Maintenance & backups.
22. Updating HM Stats
Back up first.
Pull the latest repositories:
cd /opt/hm-stats/minecraft-stats-docs
git pull
cd /opt/hm-stats/minecraft-stats-server
git pull
cd /opt/hm-stats/minecraft-stats-dashboard
git pullRebuild and restart:
cd /opt/hm-stats
docker compose up -d --buildThe API container runs database migrations during startup.
Then verify:
curl -i https://stats.example.com/api/v1/health/readyKeep your .env, PostgreSQL volume and Caddy certificate data.
23. Key rotation
When a key is exposed:
- open API Keys
- choose Rotate
- copy the new secret immediately
- replace the player's
api.key - restart Minecraft
- verify Last used
The old key is revoked by rotation.
Use Revoke when you want the key to stop working without creating a replacement.
24. Domain and HTTPS troubleshooting
DNS does not point to the server
dig +short stats.example.comCompare the result with your public server IP.
HTTPS certificate is not issued
Check:
- DNS
- inbound TCP 80
- inbound TCP 443
- whether another web server already owns those ports
Inspect:
docker compose logs -f caddyDashboard loads but API fails
Make sure Caddy routes:
/api/* -> api:8000
/auth/* -> api:8000and that the dashboard is served on the same hostname as the API.
25. Server troubleshooting
Check service state:
cd /opt/hm-stats
docker compose psThen:
docker compose logs -f api
docker compose logs -f postgresCommon causes:
- invalid
.env - missing
APP_KEY - invalid database credentials
- OIDC configuration mismatch
- PostgreSQL not healthy
- DNS/TLS issue
Use the request ID from API errors to correlate a failed API call with server logs.
26. Security checklist
Before opening the installation to the full clan:
- [ ]
APP_DEBUG=false - [ ] HTTPS works
- [ ] DNS is correct
- [ ] PostgreSQL is not public
- [ ]
APP_KEYis persistent - [ ]
API_KEY_PEPPERis persistent - [ ] OIDC client secret is private
- [ ] OIDC callback URL is exact
- [ ] first workspace owner is known
- [ ] server is registered and enabled
- [ ] season is active
- [ ] each player has a separate restricted client key
- [ ] website/integration keys are not in frontend code
- [ ] database backups exist
- [ ] a backup restore has been tested
27. Continue with the operator docs
After the base installation, read: