Compare commits

...
7 Commits
Author SHA1 Message Date
Lang a6f196f98a chg: pkg: new version release !skipChangelog 2026-10-24 19:56:47 +02:00
Lang dff39d670b chg: pkg: cleanup after release #16
Deploy to Production / deploy (push) Successful in 42s
2026-10-24 19:56:27 +02:00
Lang fe3338b96c chg: pkg: new version release !skipChangelog 2026-10-24 18:10:20 +02:00
Lang 721eada72f chg: pkg: remove all minio related stuff - & clean up and unificate the storage related code #16
Deploy to Production / deploy (push) Successful in 36s
2026-10-24 18:09:58 +02:00
Lang 7de1514cd9 chg: pkg: new version release !skipChangelog 2026-10-24 17:46:34 +02:00
Lang 3e90bbe5cf chg: pkg: migrate from shitty Minio to RustFS #16
Deploy to Production / deploy (push) Successful in 3m9s
2026-10-24 17:45:19 +02:00
Lang 1d8d0ac110 fix: pkg: upgrade PHP due to the security considerations #16 2026-10-24 16:23:40 +02:00
26 changed files with 1327 additions and 1102 deletions
+6 -6
View File
@@ -32,12 +32,12 @@ CAP_API_ENDPOINT=http://localhost:3000/changethis/
CAP_SECRET_KEY=changethis CAP_SECRET_KEY=changethis
###< trycap.dev/cap ### ###< trycap.dev/cap ###
###> minio/minio ### ###> object-storage ###
MINIO_ROOT_USER=changethis STORAGE_ACCESS_KEY=changethis
MINIO_ROOT_PASSWORD=changethis STORAGE_SECRET_KEY=changethis
MINIO_ENDPOINT=http://localhost:9010 STORAGE_ENDPOINT=http://localhost:9010
MINIO_PUBLIC_URL=https://your-minio-subdomain.example.com STORAGE_PUBLIC_URL=https://your-storage-subdomain.example.com
###< minio/minio ### ###< object-storage ###
###> symfony/mailer ### ###> symfony/mailer ###
MAILER_DSN=smtp://localhost:1025 MAILER_DSN=smtp://localhost:1025
+5 -5
View File
@@ -33,12 +33,12 @@ jobs:
cd "${{ vars.PROD_APP_DIR }}" cd "${{ vars.PROD_APP_DIR }}"
docker compose build docker compose build
- name: Start services - name: Deploy
env: env:
APP_VERSION: ${{ vars.APP_VERSION }} APP_VERSION: ${{ vars.APP_VERSION }}
run: | run: |
set -euo pipefail
cd "${{ vars.PROD_APP_DIR }}" cd "${{ vars.PROD_APP_DIR }}"
docker compose up -d minio # --remove-orphans removes retired service containers, not named volumes.
docker compose rm -sf minio_init docker compose up -d --wait --wait-timeout 180 --remove-orphans
docker compose run --rm minio_init docker compose rm -sf rustfs_init
docker compose up -d
+1 -1
View File
@@ -11,7 +11,7 @@ This document provides guidelines and context for AI coding agents working on th
- **Backend:** Symfony 7.2 (PHP 8.3) - **Backend:** Symfony 7.2 (PHP 8.3)
- **Frontend:** React 18, Vite 6 - **Frontend:** React 18, Vite 6
- **Database:** PostgreSQL 17 with materialized views - **Database:** PostgreSQL 17 with materialized views
- **Storage:** MinIO (S3-compatible) - **Storage:** RustFS (S3-compatible)
- **Real-time:** Mercure (Server-Sent Events) - **Real-time:** Mercure (Server-Sent Events)
- **Styling:** SCSS, MUI (Material-UI), Emotion - **Styling:** SCSS, MUI (Material-UI), Emotion
- **Fonts:** @fontsource packages (web), Carlito-Bold.ttf (server-side images) - **Fonts:** @fontsource packages (web), Carlito-Bold.ttf (server-side images)
+25
View File
@@ -1,6 +1,31 @@
# Changelog # Changelog
## v2026.4.0-2 (2026-10-24)
### Changes
* Cleanup after release #16. [Lang]
## v2026.4.0-1 (2026-10-24)
### Changes
* Remove all minio related stuff - & clean up and unificate the storage related code #16. [Lang]
## v2026.4.0-0 (2026-10-24)
### Changes
* Migrate from shitty Minio to RustFS #16. [Lang]
### Fix
* Upgrade PHP due to the security considerations #16. [Lang]
## v2026.3.1-2 (2026-09-21) ## v2026.3.1-2 (2026-09-21)
### Changes ### Changes
+4 -1
View File
@@ -18,7 +18,10 @@
header X-Forwarded-Host {host} header X-Forwarded-Host {host}
mercure { mercure {
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db} transport bolt {
path /data/mercure.db
}
protocol_version_compatibility 8
publisher_jwt {$MERCURE_JWT_SECRET} HS256 publisher_jwt {$MERCURE_JWT_SECRET} HS256
subscriber_jwt {$MERCURE_JWT_SECRET} HS256 subscriber_jwt {$MERCURE_JWT_SECRET} HS256
anonymous anonymous
+6 -10
View File
@@ -4,8 +4,8 @@
help: help:
@echo "Available commands:" @echo "Available commands:"
@echo " make start - Start services and initialize MinIO (uses existing images)" @echo " make start - Start services"
@echo " make start-build - Build, start services, and initialize MinIO" @echo " make start-build - Build and start services"
@echo " make stop - Stop running services" @echo " make stop - Stop running services"
@echo " make build - Build Docker images only" @echo " make build - Build Docker images only"
@echo " make down - Stop and remove containers/networks" @echo " make down - Stop and remove containers/networks"
@@ -19,18 +19,14 @@ help:
@echo " make test - Run PHPUnit tests" @echo " make test - Run PHPUnit tests"
start: start:
docker compose up -d minio docker compose up -d --remove-orphans app scheduler
docker compose rm -sf minio_init docker compose rm -sf rustfs_init
docker compose run --rm minio_init
docker compose up -d
start-build: start-build:
composer i composer i
bun run build bun run build
docker compose up -d --build minio docker compose up -d --build --remove-orphans app scheduler
docker compose rm -sf minio_init docker compose rm -sf rustfs_init
docker compose run --rm minio_init
docker compose up -d --build
stop: stop:
docker compose stop docker compose stop
+19 -23
View File
@@ -14,7 +14,7 @@ Created by [SplendidBear](https://www.splendidbear.org).
- **Guest & registered play** — jump in anonymously or create an account for stats and history - **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 - **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 - **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 - **Profile pictures** — uploaded to RustFS object storage, thumbnails generated on-the-fly by LiipImagine
- **Battle replay sharing** — share a direct link to any finished game - **Battle replay sharing** — share a direct link to any finished game
- **Docker-ready** — single `make start-build` brings up the full production-like stack - **Docker-ready** — single `make start-build` brings up the full production-like stack
@@ -28,7 +28,7 @@ Created by [SplendidBear](https://www.splendidbear.org).
| Frontend | React 19, Vite, MUI, SCSS | | Frontend | React 19, Vite, MUI, SCSS |
| Database | PostgreSQL 18 | | Database | PostgreSQL 18 |
| Real-time | Mercure (built into FrankenPHP / Caddy) | | Real-time | Mercure (built into FrankenPHP / Caddy) |
| File storage | MinIO (S3-compatible) | | File storage | RustFS (S3-compatible) |
| Image processing | LiipImagine + Flysystem | | Image processing | LiipImagine + Flysystem |
| Server | FrankenPHP (Caddy + PHP in one binary) | | Server | FrankenPHP (Caddy + PHP in one binary) |
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework | | Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
@@ -44,7 +44,7 @@ Created by [SplendidBear](https://www.splendidbear.org).
- PostgreSQL 18 - PostgreSQL 18
- Caddy with FrankenPHP and the Mercure module - Caddy with FrankenPHP and the Mercure module
- MailHog (or any SMTP server on port 1025) - MailHog (or any SMTP server on port 1025)
- MinIO (API on port 9010, console on port 9011) - RustFS (API on port 9010, console on port 9011)
### Docker ### Docker
- Docker Engine 24+ - Docker Engine 24+
@@ -75,9 +75,9 @@ Edit `.env` and fill in every value. Key ones:
|---|---| |---|---|
| `APP_SECRET` | Random 32-byte hex: `openssl rand -hex 32` | | `APP_SECRET` | Random 32-byte hex: `openssl rand -hex 32` |
| `POSTGRES_USER/PASSWORD/DB` | Your PostgreSQL credentials | | `POSTGRES_USER/PASSWORD/DB` | Your PostgreSQL credentials |
| `MINIO_ROOT_USER/PASSWORD` | MinIO admin credentials | | `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` | RustFS S3 credentials |
| `MINIO_ENDPOINT` | `http://localhost:9010` (bare-metal) | | `STORAGE_ENDPOINT` | RustFS S3 API URL (`http://localhost:9010` for bare-metal) |
| `MINIO_PUBLIC_URL` | Public URL browsers use to reach MinIO | | `STORAGE_PUBLIC_URL` | Public URL browsers use to reach RustFS (including media and LiipImagine thumbnails) |
| `CAP_API_ENDPOINT` | Full URL of your Cap instance including the site-key path (e.g. `https://cap.example.com/my-site-key/`) | | `CAP_API_ENDPOINT` | Full URL of your Cap instance including the site-key path (e.g. `https://cap.example.com/my-site-key/`) |
| `CAP_SECRET_KEY` | Secret key from your Cap site dashboard (stored in `PROD_ENV_FILE` Gitea secret on prod) | | `CAP_SECRET_KEY` | Secret key from your Cap site dashboard (stored in `PROD_ENV_FILE` Gitea secret on prod) |
| `MERCURE_JWT_SECRET` | Random secret (generated in step 3) | | `MERCURE_JWT_SECRET` | Random secret (generated in step 3) |
@@ -104,7 +104,7 @@ sudo systemctl reload caddy
make start-build 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`). This installs PHP dependencies, builds the frontend assets with Bun, builds the Docker images, initializes the RustFS bucket and public media/cache policy, and starts the services.
The app is available at `http://localhost:10080` (or the domain set in `APP_PUBLIC_HOSTNAME`). The app is available at `http://localhost:10080` (or the domain set in `APP_PUBLIC_HOSTNAME`).
@@ -119,21 +119,15 @@ bun run dev # Vite dev server with hot-reload
php bin/console doctrine:migrations:migrate --no-interaction php bin/console doctrine:migrations:migrate --no-interaction
``` ```
Start MinIO and MailHog, then open the URL configured in your Caddy vhost. Start RustFS and MailHog, then open the URL configured in your Caddy vhost.
### 5. MinIO bucket setup ### 5. Object storage setup
**Docker** — handled automatically on first start by the `minio_init` service. No action needed. The Docker stack initializes the `mineseeker` bucket and configures public reads for media and thumbnail objects automatically. For bare-metal installs, create that bucket in RustFS and allow public `GetObject` access for `media/*` and `cache/*`.
**Bare-metal** — run once after MinIO is up: ### Object storage backup and restore
```bash `bin/backup-rustfs.sh [backup-directory]` creates a compressed archive of the RustFS data volume (default: `backups/`). It briefly stops the app, scheduler, and RustFS for a consistent backup, then restarts the services that were running. To restore, run `bin/restore-rustfs.sh <backup-file>`; it verifies the archive and requires typing `yes` before replacing the current RustFS data.
mc alias set local http://localhost:9010 $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
```
--- ---
@@ -199,6 +193,8 @@ The `app` image is rebuilt with the new code; the database and storage container
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. 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.
Before deploying this RustFS-only configuration, update `PROD_ENV_FILE` to use `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY`, `STORAGE_ENDPOINT`, and `STORAGE_PUBLIC_URL` (the old `MINIO_*` names are no longer read).
#### `PROD_ENV_FILE` contents #### `PROD_ENV_FILE` contents
Paste the filled-in `.env` file as the secret value: Paste the filled-in `.env` file as the secret value:
@@ -215,10 +211,10 @@ POSTGRES_PASSWORD="<strong password>"
POSTGRES_DB=mineseeker POSTGRES_DB=mineseeker
POSTGRES_VERSION=18 POSTGRES_VERSION=18
MINIO_ROOT_USER=mineseeker STORAGE_ACCESS_KEY=mineseeker
MINIO_ROOT_PASSWORD="<strong password>" STORAGE_SECRET_KEY="<strong password>"
MINIO_ENDPOINT=http://minio:9000 STORAGE_ENDPOINT=http://rustfs:9000
MINIO_PUBLIC_URL=https://aws.mineseeker.hu STORAGE_PUBLIC_URL=https://aws.mineseeker.hu
MAILER_DSN=smtp://mail:25?verify_peer=0 MAILER_DSN=smtp://mail:25?verify_peer=0
MAIL_DOMAIN=mineseeker.hu MAIL_DOMAIN=mineseeker.hu
@@ -270,7 +266,7 @@ git tag v2026.01
git push origin 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`. This writes `.env`, builds the Docker images, runs database migrations, initializes RustFS, and starts the application services.
#### 4. Verify #### 4. Verify
-42
View File
@@ -1,42 +0,0 @@
#!/bin/sh
set -eu
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$PROJECT_DIR"
CONTAINER_ID=$(docker compose ps -q minio)
if [ -z "$CONTAINER_ID" ]; then
echo "Error: The MineSeeker MinIO service is not running."
exit 1
fi
VOLUME_NAME=$(docker inspect "$CONTAINER_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')
if [ -z "$VOLUME_NAME" ]; then
echo "Error: Could not determine the MinIO data volume."
exit 1
fi
BACKUP_DIR=${1:-"$PROJECT_DIR/backups"}
mkdir -p "$BACKUP_DIR"
BACKUP_DIR=$(CDPATH= cd -- "$BACKUP_DIR" && pwd)
TIMESTAMP=$(date +%Y-%m-%d.%H%M%S)
BACKUP_FILE="mineseeker-minio.${TIMESTAMP}.tar.gz"
BACKUP_PATH="$BACKUP_DIR/$BACKUP_FILE"
umask 077
restart_minio() {
docker compose start minio || echo "Warning: Failed to restart MinIO."
}
trap restart_minio EXIT
echo "Stopping MinIO for a consistent backup..."
docker compose stop minio
echo "Creating MinIO backup: $BACKUP_PATH"
docker run --rm \
--volume "$VOLUME_NAME:/data:ro" \
--volume "$BACKUP_DIR:/backup" \
busybox tar czf "/backup/$BACKUP_FILE" -C /data .
echo "Backup complete: $BACKUP_PATH"
+54
View File
@@ -0,0 +1,54 @@
#!/bin/sh
set -eu
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$PROJECT_DIR"
CONTAINER_ID=$(docker compose ps -q rustfs)
if [ -z "$CONTAINER_ID" ]; then
echo "Error: The MineSeeker RustFS service is not running."
exit 1
fi
VOLUME_NAME=$(docker inspect "$CONTAINER_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')
if [ -z "$VOLUME_NAME" ]; then
echo "Error: Could not determine the RustFS data volume."
exit 1
fi
BACKUP_DIR=${1:-"$PROJECT_DIR/backups"}
mkdir -p "$BACKUP_DIR"
BACKUP_DIR=$(CDPATH= cd -- "$BACKUP_DIR" && pwd)
TIMESTAMP=$(date +%Y-%m-%d.%H%M%S)
BACKUP_FILE="mineseeker-rustfs.${TIMESTAMP}.tar.gz"
BACKUP_PATH="$BACKUP_DIR/$BACKUP_FILE"
TEMP_PATH="$BACKUP_PATH.tmp"
umask 077
APP_WAS_RUNNING=0
SCHEDULER_WAS_RUNNING=0
RUSTFS_WAS_RUNNING=1
[ -z "$(docker compose ps -q app)" ] || APP_WAS_RUNNING=1
[ -z "$(docker compose ps -q scheduler)" ] || SCHEDULER_WAS_RUNNING=1
restart_services() {
result=$?
rm -f "$TEMP_PATH"
[ "$RUSTFS_WAS_RUNNING" -eq 0 ] || docker compose start rustfs || true
[ "$APP_WAS_RUNNING" -eq 0 ] || docker compose start app || true
[ "$SCHEDULER_WAS_RUNNING" -eq 0 ] || docker compose start scheduler || true
return "$result"
}
trap restart_services EXIT
echo "Stopping MineSeeker writers and RustFS for a consistent backup..."
docker compose stop app scheduler rustfs
echo "Creating RustFS backup: $BACKUP_PATH"
docker run --rm \
--volume "$VOLUME_NAME:/data:ro" \
--volume "$BACKUP_DIR:/backup" \
busybox tar czf "/backup/$BACKUP_FILE.tmp" -C /data .
mv "$TEMP_PATH" "$BACKUP_PATH"
echo "Backup complete: $BACKUP_PATH"
-61
View File
@@ -1,61 +0,0 @@
#!/bin/sh
set -eu
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$PROJECT_DIR"
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <backup-file>"
echo "Example: $0 backups/mineseeker-minio.2026-08-18.120000.tar.gz"
exit 1
fi
BACKUP_FILE=$1
if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
BACKUP_DIR=$(CDPATH= cd -- "$(dirname -- "$BACKUP_FILE")" && pwd)
BACKUP_NAME=$(basename "$BACKUP_FILE")
CONTAINER_ID=$(docker compose ps -q minio)
if [ -z "$CONTAINER_ID" ]; then
echo "Error: The MineSeeker MinIO service is not running."
exit 1
fi
VOLUME_NAME=$(docker inspect "$CONTAINER_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')
if [ -z "$VOLUME_NAME" ]; then
echo "Error: Could not determine the MinIO data volume."
exit 1
fi
echo "Verifying MinIO archive..."
docker run --rm \
--volume "$BACKUP_DIR:/backup:ro" \
busybox tar -tzf "/backup/$BACKUP_NAME" > /dev/null
echo "Archive OK."
printf 'WARNING: This erases all current MineSeeker MinIO data. Type yes to continue: '
read -r CONFIRM
if [ "$CONFIRM" != 'yes' ]; then
echo "Aborted."
exit 1
fi
restart_minio() {
docker compose start minio || echo "Warning: Failed to restart MinIO."
}
trap restart_minio EXIT
echo "Stopping MinIO before restore..."
docker compose stop minio
echo "Restoring MinIO backup: $BACKUP_FILE"
docker run --rm \
--volume "$VOLUME_NAME:/data" \
--volume "$BACKUP_DIR:/backup:ro" \
busybox sh -c 'find /data -mindepth 1 -maxdepth 1 -exec rm -rf {} + && tar xzf "/backup/$1" -C /data' sh "$BACKUP_NAME"
echo "Restore complete."
+24 -5
View File
@@ -28,6 +28,12 @@ if [ -z "$DB_USER" ]; then
exit 1 exit 1
fi fi
DB_NAME=$(docker inspect "$CONTAINER_ID" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^POSTGRES_DB=//p')
if [ -z "$DB_NAME" ]; then
echo "Error: Could not read POSTGRES_DB from the db service."
exit 1
fi
echo "Verifying PostgreSQL archive..." echo "Verifying PostgreSQL archive..."
docker exec -i "$CONTAINER_ID" pg_restore --list < "$BACKUP_FILE" > /dev/null docker exec -i "$CONTAINER_ID" pg_restore --list < "$BACKUP_FILE" > /dev/null
echo "Archive OK." echo "Archive OK."
@@ -53,13 +59,26 @@ trap restart_services EXIT
echo "Stopping MineSeeker services that use PostgreSQL..." echo "Stopping MineSeeker services that use PostgreSQL..."
docker compose stop app scheduler docker compose stop app scheduler
echo "Restoring PostgreSQL backup: $BACKUP_FILE" echo "Dropping configured database '$DB_NAME'..."
docker exec "$CONTAINER_ID" dropdb \
--username="$DB_USER" \
--if-exists \
--force \
"$DB_NAME"
echo "Creating '$DB_NAME' owned by '$DB_USER'..."
docker exec "$CONTAINER_ID" createdb \
--username="$DB_USER" \
--owner="$DB_USER" \
--template=template0 \
"$DB_NAME"
echo "Restoring PostgreSQL backup without source ownership/ACLs: $BACKUP_FILE"
docker exec -i "$CONTAINER_ID" pg_restore \ docker exec -i "$CONTAINER_ID" pg_restore \
--username="$DB_USER" \ --username="$DB_USER" \
--dbname=postgres \ --dbname="$DB_NAME" \
--clean \ --no-owner \
--create \ --no-acl \
--if-exists \
--exit-on-error < "$BACKUP_FILE" --exit-on-error < "$BACKUP_FILE"
echo "Restore complete." echo "Restore complete."
+77
View File
@@ -0,0 +1,77 @@
#!/bin/sh
set -eu
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$PROJECT_DIR"
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <backup-file>"
echo "Example: $0 backups/mineseeker-rustfs.2026-10-24.120000.tar.gz"
exit 1
fi
BACKUP_FILE=$1
if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
BACKUP_DIR=$(CDPATH= cd -- "$(dirname -- "$BACKUP_FILE")" && pwd)
BACKUP_NAME=$(basename "$BACKUP_FILE")
CONTAINER_ID=$(docker compose ps -q rustfs)
if [ -z "$CONTAINER_ID" ]; then
echo "Error: The MineSeeker RustFS service must be running before restore."
exit 1
fi
VOLUME_NAME=$(docker inspect "$CONTAINER_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')
if [ -z "$VOLUME_NAME" ]; then
echo "Error: Could not determine the RustFS data volume."
exit 1
fi
echo "Verifying RustFS archive..."
docker run --rm \
--volume "$BACKUP_DIR:/backup:ro" \
busybox tar -tzf "/backup/$BACKUP_NAME" > /dev/null
echo "Archive OK."
printf 'WARNING: This erases all current MineSeeker RustFS data. Type yes to continue: '
read -r CONFIRM
if [ "$CONFIRM" != 'yes' ]; then
echo "Aborted."
exit 1
fi
APP_WAS_RUNNING=0
SCHEDULER_WAS_RUNNING=0
RUSTFS_WAS_RUNNING=1
[ -z "$(docker compose ps -q app)" ] || APP_WAS_RUNNING=1
[ -z "$(docker compose ps -q scheduler)" ] || SCHEDULER_WAS_RUNNING=1
RESTORE_COMPLETE=0
restart_services() {
result=$?
if [ "$RESTORE_COMPLETE" -eq 1 ]; then
[ "$RUSTFS_WAS_RUNNING" -eq 0 ] || docker compose start rustfs || true
[ "$APP_WAS_RUNNING" -eq 0 ] || docker compose start app || true
[ "$SCHEDULER_WAS_RUNNING" -eq 0 ] || docker compose start scheduler || true
elif [ "$result" -ne 0 ]; then
echo "Restore did not complete; RustFS and application services remain stopped for safety." >&2
fi
return "$result"
}
trap restart_services EXIT
echo "Stopping MineSeeker writers and RustFS before restore..."
docker compose stop app scheduler rustfs
echo "Restoring RustFS backup: $BACKUP_FILE"
docker run --rm \
--volume "$VOLUME_NAME:/data" \
--volume "$BACKUP_DIR:/backup:ro" \
busybox sh -c 'find /data -mindepth 1 -maxdepth 1 -exec rm -rf {} + && tar xzf "/backup/$1" -C /data' sh "$BACKUP_NAME"
RESTORE_COMPLETE=1
echo "Restore complete."
+51 -34
View File
@@ -6,7 +6,7 @@ services:
restart: unless-stopped restart: unless-stopped
container_name: '${APP_NAME}-app' container_name: '${APP_NAME}-app'
ports: ports:
- "10080:80" - "${APP_PUBLISHED_PORT:-10080}:80"
environment: environment:
SERVER_NAME: ${SERVER_NAME:-:80} SERVER_NAME: ${SERVER_NAME:-:80}
APP_ENV: prod APP_ENV: prod
@@ -30,10 +30,10 @@ services:
CAP_SECRET_KEY: ${CAP_SECRET_KEY} CAP_SECRET_KEY: ${CAP_SECRET_KEY}
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost} WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost} WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
MINIO_ROOT_USER: ${MINIO_ROOT_USER} STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
MINIO_ENDPOINT: http://minio:9000 STORAGE_ENDPOINT: http://rustfs:9000
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9010} STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL:-http://localhost:9010}
# IMPORTANT: Set TRUSTED_PROXIES to your reverse proxy IP in production. # IMPORTANT: Set TRUSTED_PROXIES to your reverse proxy IP in production.
# For Docker on same host, use: 172.17.0.1 (default bridge) or 172.16.0.0/12 (overlay network) # For Docker on same host, use: 172.17.0.1 (default bridge) or 172.16.0.0/12 (overlay network)
# For Kubernetes or external proxy, use the proxy's IP address. # For Kubernetes or external proxy, use the proxy's IP address.
@@ -43,11 +43,21 @@ services:
- app_var:/app/var - app_var:/app/var
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/ -o /dev/null"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
mail: mail:
condition: service_started condition: service_started
rustfs:
condition: service_healthy
rustfs_init:
condition: service_completed_successfully
scheduler: scheduler:
build: build:
context: . context: .
@@ -75,10 +85,10 @@ services:
CAP_SECRET_KEY: ${CAP_SECRET_KEY} CAP_SECRET_KEY: ${CAP_SECRET_KEY}
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost} WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost} WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
MINIO_ROOT_USER: ${MINIO_ROOT_USER} STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
MINIO_ENDPOINT: http://minio:9000 STORAGE_ENDPOINT: http://rustfs:9000
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9010} STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL:-http://localhost:9010}
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1} TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1}
volumes: volumes:
- app_var:/app/var - app_var:/app/var
@@ -87,42 +97,49 @@ services:
condition: service_healthy condition: service_healthy
mail: mail:
condition: service_started condition: service_started
rustfs:
condition: service_healthy
rustfs_init:
condition: service_completed_successfully
healthcheck: healthcheck:
disable: true disable: true
minio: rustfs:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1 image: rustfs/rustfs:1.0.3
restart: unless-stopped restart: unless-stopped
container_name: '${APP_NAME}-minio' container_name: '${APP_NAME}-rustfs'
command: server /data --console-address ":9001"
ports:
- "9010:9000"
- "9011:9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER} RUSTFS_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} RUSTFS_SECRET_KEY: ${STORAGE_SECRET_KEY}
RUSTFS_ADDRESS: ':9000'
RUSTFS_CONSOLE_ADDRESS: ':9001'
RUSTFS_CONSOLE_ENABLE: 'true'
volumes: volumes:
- minio_data:/data - rustfs_data:/data
ports:
- "${RUSTFS_PUBLISHED_API_PORT:-9010}:9000"
- "${RUSTFS_PUBLISHED_CONSOLE_PORT:-9011}:9001"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 20
start_period: 10s start_period: 15s
minio_init: rustfs_init:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1 build:
profiles: ['bootstrap'] context: ./docker/storage-init
entrypoint: ["python"]
command: ["/usr/local/bin/storage-init.py"]
restart: "no" restart: "no"
container_name: '${APP_NAME}-minio-init' container_name: '${APP_NAME}-rustfs-init'
depends_on: depends_on:
minio: rustfs:
condition: service_healthy condition: service_healthy
environment: environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER} TARGET_ENDPOINT: http://rustfs:9000
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
volumes: STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
- ./docker/minio-init.sh:/minio-init.sh:ro STORAGE_BUCKET: mineseeker
entrypoint: ["/bin/sh", "/minio-init.sh"]
mail: mail:
image: boky/postfix:latest image: boky/postfix:latest
restart: unless-stopped restart: unless-stopped
@@ -157,11 +174,11 @@ services:
retries: 10 retries: 10
start_period: 10s start_period: 10s
ports: ports:
- "5452:5432" - "${POSTGRES_PUBLISHED_PORT:-5452}:5432"
volumes: volumes:
app_var: app_var:
postgres_data: postgres_data:
caddy_data: caddy_data:
caddy_config: caddy_config:
postfix_spool: postfix_spool:
minio_data: rustfs_data:
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "splendid-bear/mineseeker", "name": "splendid-bear/mineseeker",
"version": "2026.3.0", "version": "2026.4.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"author": "https://www.splendidbear.org", "author": "https://www.splendidbear.org",
"bugs": "https://source.splendidbear.org", "bugs": "https://source.splendidbear.org",
@@ -10,7 +10,7 @@
"prefer-stable": true, "prefer-stable": true,
"private": true, "private": true,
"require": { "require": {
"php": ">=8.5", "php": ">=8.5.12",
"ext-gd": "*", "ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-json": "*", "ext-json": "*",
@@ -33,8 +33,8 @@
"symfony/framework-bundle": "8.1.*", "symfony/framework-bundle": "8.1.*",
"symfony/http-client": "8.1.*", "symfony/http-client": "8.1.*",
"symfony/mailer": "8.1.*", "symfony/mailer": "8.1.*",
"symfony/mercure": "^0.8", "symfony/mercure": "^0.9",
"symfony/mercure-bundle": "^0.5", "symfony/mercure-bundle": "^0.6",
"symfony/messenger": "8.1.*", "symfony/messenger": "8.1.*",
"symfony/monolog-bundle": "^4.0", "symfony/monolog-bundle": "^4.0",
"symfony/scheduler": "8.1.*", "symfony/scheduler": "8.1.*",
Generated
+984 -837
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -4,12 +4,12 @@ liip_imagine:
default: default:
flysystem: flysystem:
filesystem_service: mineseeker.media.storage filesystem_service: mineseeker.media.storage
cache: minio_cache cache: object_storage_cache
resolvers: resolvers:
minio_cache: object_storage_cache:
flysystem: flysystem:
filesystem_service: mineseeker.cache.storage filesystem_service: mineseeker.cache.storage
root_url: '%env(MINIO_PUBLIC_URL)%/mineseeker/cache' root_url: '%env(STORAGE_PUBLIC_URL)%/mineseeker/cache'
cache_prefix: 'liip' cache_prefix: 'liip'
filter_sets: filter_sets:
avatar_thumb: avatar_thumb:
+1
View File
@@ -3,6 +3,7 @@ mercure:
default: default:
url: '%env(MERCURE_URL)%' url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%' public_url: '%env(MERCURE_PUBLIC_URL)%'
protocol_version: '0.x'
jwt: jwt:
secret: '%env(MERCURE_JWT_SECRET)%' secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*' publish: '*'
+4 -4
View File
@@ -29,14 +29,14 @@ services:
arguments: arguments:
$cacheDir: '%kernel.project_dir%/var/og-cache' $cacheDir: '%kernel.project_dir%/var/og-cache'
$fontPath: '%kernel.project_dir%/assets/fonts/Carlito-Bold.ttf' $fontPath: '%kernel.project_dir%/assets/fonts/Carlito-Bold.ttf'
$minioMediaStorage: '@mineseeker.media.storage' $mediaStorage: '@mineseeker.media.storage'
Aws\S3\S3Client: Aws\S3\S3Client:
arguments: arguments:
- version: 'latest' - version: 'latest'
region: 'us-east-1' region: 'us-east-1'
endpoint: '%env(MINIO_ENDPOINT)%' endpoint: '%env(STORAGE_ENDPOINT)%'
use_path_style_endpoint: true use_path_style_endpoint: true
credentials: credentials:
key: '%env(MINIO_ROOT_USER)%' key: '%env(STORAGE_ACCESS_KEY)%'
secret: '%env(MINIO_ROOT_PASSWORD)%' secret: '%env(STORAGE_SECRET_KEY)%'
-40
View File
@@ -1,40 +0,0 @@
#!/bin/sh
set -e
echo "[minio-init] Waiting for MinIO..."
until mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" > /dev/null 2>&1; do
sleep 2
done
echo "[minio-init] Configuring bucket..."
if mc ls local/mineseeker > /dev/null 2>&1; then
echo "[minio-init] Already initialized, skipping."
exit 0
fi
echo "[minio-init] Creating bucket 'mineseeker'..."
mc mb local/mineseeker
echo "[minio-init] Creating directories..."
echo '' | mc pipe local/mineseeker/media/.keep
echo '' | mc pipe local/mineseeker/cache/.keep
echo "[minio-init] Applying anonymous read policy for media/ and cache/..."
printf '%s' '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": ["*"] },
"Action": ["s3:GetObject"],
"Resource": [
"arn:aws:s3:::mineseeker/media/*",
"arn:aws:s3:::mineseeker/cache/*"
]
}
]
}' > /tmp/policy.json
mc anonymous set-json /tmp/policy.json local/mineseeker
echo "[minio-init] Done."
+7
View File
@@ -0,0 +1,7 @@
FROM python:3.13-slim
RUN pip install --no-cache-dir boto3==1.43.118
COPY init.py /usr/local/bin/storage-init.py
ENTRYPOINT ["python", "/usr/local/bin/storage-init.py"]
+45
View File
@@ -0,0 +1,45 @@
"""Create the application bucket and public object-read policy on RustFS."""
import json
import os
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
BUCKET = os.environ.get("STORAGE_BUCKET", "mineseeker")
s3 = boto3.client(
"s3",
endpoint_url=os.environ["TARGET_ENDPOINT"],
aws_access_key_id=os.environ["STORAGE_ACCESS_KEY"],
aws_secret_access_key=os.environ["STORAGE_SECRET_KEY"],
region_name="us-east-1",
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
try:
s3.head_bucket(Bucket=BUCKET)
except ClientError as error:
status = error.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
code = error.response.get("Error", {}).get("Code", "")
if status != 404 and code not in {"NoSuchBucket", "404", "NotFound"}:
raise
s3.create_bucket(Bucket=BUCKET)
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": [
f"arn:aws:s3:::{BUCKET}/media/*",
f"arn:aws:s3:::{BUCKET}/cache/*",
],
}
],
}
s3.put_bucket_policy(Bucket=BUCKET, Policy=json.dumps(policy))
print(f"RustFS bucket {BUCKET!r} is initialized", flush=True)
-19
View File
@@ -1,19 +0,0 @@
# MinIO Initialization
The `minio_init` Compose service creates the `mineseeker` bucket and applies
its public read policy. It is an idempotent bootstrap task: after the bucket
exists, it exits without changing it.
It belongs to the `bootstrap` Compose profile, so normal `docker compose up`
does not create a stopped `mineseeker-minio-init` container.
`make start`, `make start-build`, and the production deployment workflow run
the bootstrap task automatically with `docker compose run --rm minio_init`.
They also remove any stopped legacy initializer container first. The temporary
container is removed as soon as the task succeeds.
To run it manually when needed:
```bash
docker compose run --rm minio_init
```
+2 -2
View File
@@ -60,8 +60,8 @@ Quick command reference for testing:
### [Scheduled Tasks](./SCHEDULER.md) ### [Scheduled Tasks](./SCHEDULER.md)
Symfony Scheduler tasks and the Docker worker that runs them. Symfony Scheduler tasks and the Docker worker that runs them.
### [MinIO Initialization](./MINIO_INITIALIZATION.md) ### Object storage
Automatic, one-shot setup of the MinIO bucket and its access policy. RustFS is initialized automatically by the Docker stack. Configure its S3 credentials and public media URL in `.env`.
### [CI/CD Integration Guide](./CI_CD.md) ### [CI/CD Integration Guide](./CI_CD.md)
Comprehensive guide for continuous integration and deployment: Comprehensive guide for continuous integration and deployment:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mine-seeker", "name": "mine-seeker",
"version": "2026.3.0", "version": "2026.4.0",
"author": "https://www.splendidbear.org", "author": "https://www.splendidbear.org",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": "https://source.splendidbear.org", "bugs": "https://source.splendidbear.org",
+1 -1
View File
@@ -178,7 +178,7 @@ class ProfileController extends AbstractController
$cacheManager->remove($oldPath, 'avatar_thumb'); $cacheManager->remove($oldPath, 'avatar_thumb');
} }
/** Upload original to MinIO media/avatar/ */ /** Upload original avatar to object storage. */
$stream = fopen($file->getPathname(), 'rb'); $stream = fopen($file->getPathname(), 'rb');
try { try {
$mediaStorage->writeStream($newPath, $stream); $mediaStorage->writeStream($newPath, $stream);
+3 -3
View File
@@ -39,7 +39,7 @@ final class BattleCardGenerator
public function __construct( public function __construct(
private readonly string $cacheDir, private readonly string $cacheDir,
private readonly string $fontPath, private readonly string $fontPath,
private readonly FilesystemOperator $minioMediaStorage, private readonly FilesystemOperator $mediaStorage,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
) { ) {
} }
@@ -193,12 +193,12 @@ final class BattleCardGenerator
{ {
$avatarImg = null; $avatarImg = null;
/** Try to load avatar from MinIO if path exists*/ /** Try to load avatar from object storage if a path exists. */
if ($avatarPath) { if ($avatarPath) {
try { try {
/** Remove 'avatar/' prefix if it exists since storage already has media/ prefix*/ /** Remove 'avatar/' prefix if it exists since storage already has media/ prefix*/
$path = str_starts_with($avatarPath, 'avatar/') ? $avatarPath : 'avatar/' . $avatarPath; $path = str_starts_with($avatarPath, 'avatar/') ? $avatarPath : 'avatar/' . $avatarPath;
$avatarData = $this->minioMediaStorage->read($path); $avatarData = $this->mediaStorage->read($path);
$avatarImg = imagecreatefromstring($avatarData); $avatarImg = imagecreatefromstring($avatarData);
} catch (Exception $e) { } catch (Exception $e) {
/** Failed to load avatar, will use initials*/ /** Failed to load avatar, will use initials*/