MineSeeker
A real-time 1v1 multiplayer minesweeper game played in the browser. Two players race on the same hidden minefield — uncover safe cells to score points, but hit a mine and you hand the advantage to your opponent. Games are live and synchronised instantly via a Mercure hub; no page reloads, no polling.
Created by SplendidBear.
Features
- Real-time 1v1 gameplay — moves broadcast instantly over Mercure (server-sent events)
- Guest & registered play — jump in anonymously or create an account for stats and history
- Full authentication stack — email/password, passkeys (WebAuthn), TOTP 2FA with backup codes
- Player profiles — win/loss/draw stats, per-month charts, recent battle history with shareable replay links
- Profile pictures — uploaded to MinIO object storage, thumbnails generated on-the-fly by LiipImagine
- Battle replay sharing — share a direct link to any finished game
- Docker-ready — single
make start-buildbrings up the full production-like stack
Tech stack
| Layer | Technology |
|---|---|
| Backend | PHP 8.5, Symfony 7.4, Doctrine ORM |
| Frontend | React 19, Vite, MUI, SCSS |
| Database | PostgreSQL 18 |
| Real-time | Mercure (built into FrankenPHP / Caddy) |
| File storage | MinIO (S3-compatible) |
| Image processing | LiipImagine + Flysystem |
| Server | FrankenPHP (Caddy + PHP in one binary) |
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
Requirements
Bare-metal development
- PHP >= 8.5 with extensions:
pdo_pgsql,gd,intl,zip,sodium - Composer 2
- Node.js 22 + Bun
- PostgreSQL 18
- Caddy with FrankenPHP and the Mercure module
- MailHog (or any SMTP server on port 1025)
- MinIO (listening on port 9000)
Docker
- Docker Engine 24+
- Docker Compose v2
- Bun (for building frontend assets before
make start-build) - Composer (for
make start-build)
Installation
1. Clone the repository
git clone https://github.com/splendidbear/mineseeker.git
cd mineseeker
2. Configure environment
cp .env.dist .env
Edit .env and fill in every value. Key ones:
| Variable | What to set |
|---|---|
APP_SECRET |
Random 32-byte hex: openssl rand -hex 32 |
POSTGRES_USER/PASSWORD/DB |
Your PostgreSQL credentials |
MINIO_ROOT_USER/PASSWORD |
MinIO admin credentials |
MINIO_ENDPOINT |
http://localhost:9000 (bare-metal) |
MINIO_PUBLIC_URL |
Public URL browsers use to reach MinIO |
RECAPTCHA_SITE_KEY/SECRET_KEY |
Google reCAPTCHA v3 keys for your domain |
MERCURE_JWT_SECRET |
Random secret (generated in step 3) |
MERCURE_JWT_TOKEN |
Signed publisher JWT (generated in step 3) |
MERCURE_SUBSCRIBER_JWT |
Signed subscriber JWT (generated in step 3) |
MAILER_DSN |
smtp://localhost:1025 for MailHog in dev |
3. Generate Mercure JWT tokens
composer install
make mercure-jwt
Copy the printed values into .env and into the publisher_jwt / subscriber_jwt lines of your Caddy Mercure block, then reload Caddy:
sudo systemctl reload caddy
4a. Run with Docker
make start-build
This installs PHP dependencies, builds the frontend assets with Bun, builds the Docker image, and starts all services (app, db, mail, minio, minio_init).
The app is available at http://localhost:10080 (or the domain set in APP_PUBLIC_HOSTNAME).
To apply any code changes later, run the same command again.
4b. Run bare-metal (development)
composer install
bun install
bun run dev # Vite dev server with hot-reload
php bin/console doctrine:migrations:migrate --no-interaction
Start MinIO and MailHog, then open the URL configured in your Caddy vhost.
5. MinIO bucket setup
Docker — handled automatically on first start by the minio_init service. No action needed.
Bare-metal — run once after MinIO is up:
mc alias set local http://localhost:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
mc mb local/mineseeker
echo '' | mc pipe local/mineseeker/media/.keep
echo '' | mc pipe local/mineseeker/cache/.keep
# Apply public-read policy for media/ and cache/ — see docker/minio-init.sh for the JSON
Development environment
When running the Docker stack locally you typically want to catch outgoing emails instead of relaying them through a real SMTP server.
The production compose.yaml uses Postfix for actual mail delivery and must not be edited for local overrides.
Use a compose.override.yaml file instead — Docker Compose merges it automatically on top of the base file whenever both are present.
Email: replace Postfix with MailHog
Create compose.override.yaml in the project root (it is git-ignored and never reaches production):
services:
app:
environment:
MAILER_DSN: smtp://mail:1025?verify_peer=0
TRUSTED_PROXIES: "0.0.0.0/0"
mail:
image: mailhog/mailhog:latest
ports:
- "1025:1025"
- "8025:8025"
This replaces the mail service image with MailHog and points the application's mailer at its SMTP port (1025).
No other files need to change.
After adding the file, restart the stack:
make start-build
All emails sent by the application are now captured by MailHog. Open the web UI at http://localhost:8025 to inspect them.
Production note —
compose.override.yamlis listed in.gitignore. Never commit it; the production server must only seecompose.yamlwith Postfix.
Deploying to production
Releases are automated via Gitea Actions. Pushing a tag that starts with v (e.g. v2026.01) triggers the workflow at .gitea/workflows/deploy.yml.
The job runs on a self-hosted runner installed on the production server — the server only needs an outbound connection to Gitea, no open SSH port required.
The app image is rebuilt with the new code; the database and storage containers are untouched so all data is preserved.
Gitea repository variables and secrets
Variable (plaintext, editable — Repository → Settings → Variables):
| Variable | Value |
|---|---|
PROD_APP_DIR |
Absolute path on the server (e.g. /var/www/mineseeker) |
Secret (encrypted, write-only — Repository → Settings → Secrets):
| Secret | Value |
|---|---|
PROD_ENV_FILE |
Full content of the production .env file (see below) |
The workflow writes PROD_ENV_FILE to .env on every deploy, so you never need to manage the file on the server manually. To update a credential, overwrite the secret in Gitea and push a new tag.
PROD_ENV_FILE contents
Paste the filled-in .env file as the secret value:
APP_NAME=mineseeker
APP_ENV=prod
APP_SECRET="<openssl rand -hex 32>"
DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@db:5432/POSTGRES_DB?serverVersion=18&charset=utf8
POSTGRES_USER=mineseeker
POSTGRES_PASSWORD="<strong password>"
POSTGRES_DB=mineseeker
POSTGRES_VERSION=18
MINIO_ROOT_USER=mineseeker
MINIO_ROOT_PASSWORD="<strong password>"
MINIO_ENDPOINT=http://minio:9000
MINIO_PUBLIC_URL=https://aws.mineseeker.hu
MAILER_DSN=smtp://mail:25?verify_peer=0
MAIL_DOMAIN=mineseeker.hu
RECAPTCHA_SITE_KEY="<your reCAPTCHA v3 site key>"
RECAPTCHA_SECRET_KEY="<your reCAPTCHA v3 secret key>"
MERCURE_URL=https://mineseeker.hu/.well-known/mercure
MERCURE_PUBLIC_URL=https://mineseeker.hu/.well-known/mercure
MERCURE_JWT_SECRET="<generated by make mercure-jwt>"
MERCURE_JWT_TOKEN="<generated by make mercure-jwt>"
MERCURE_SUBSCRIBER_JWT="<generated by make mercure-jwt>"
APP_PUBLIC_HOSTNAME=mineseeker.hu
WEBAUTHN_RP_ID=mineseeker.hu
WEBAUTHN_ORIGIN=https://mineseeker.hu
# OG Tags & Social Media Sharing (IMPORTANT for Docker deployments)
# TRUSTED_PROXIES: IP address (or range) of your reverse proxy (Caddy/Nginx)
# This ensures OG image tags use HTTPS URLs instead of HTTP
TRUSTED_PROXIES="172.18.0.0/16"
TRUSTED_HOSTS="mineseeker.hu,www.mineseeker.hu"
Production server: one-time setup
The server needs Docker, Git, and a self-hosted act_runner registered against the Gitea repository. Bun and Composer run inside the multi-stage Dockerfile, so they are not needed on the server.
1. Clone the repository
git clone https://gitea.mineseeker.hu/youruser/mineseeker.git /var/www/mineseeker
2. Generate Mercure JWT tokens (run once locally)
composer install # only needed for this step
make mercure-jwt
Copy the three printed values into the PROD_ENV_FILE secret.
3. First deploy
Trigger it by pushing the first tag:
git tag v2026.01
git push origin v2026.01
This writes .env, builds the Docker image, starts all services, runs migrations, and initialises the MinIO buckets automatically via minio_init.
4. Verify
docker compose ps # all services should be healthy/running
docker compose logs app # look for "Starting FrankenPHP"
Releasing
git tag v2026.01
git push origin v2026.01
Documentation
For detailed information about game mechanics, bonus systems, fonts, and other technical details, see the docs directory:
- Bonus Points System — Complete reference for all bonus point types, calculation rules, and implementation details
- Fonts — TrueType fonts used for server-side image generation
License
LGPL-3.0 — see LICENSE for details.
© 2026 SplendidBear