Private
Public Access
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff39d670b | ||
|
|
fe3338b96c | ||
|
|
721eada72f | ||
|
|
7de1514cd9 | ||
|
|
3e90bbe5cf | ||
|
|
1d8d0ac110 |
@@ -32,12 +32,12 @@ CAP_API_ENDPOINT=http://localhost:3000/changethis/
|
||||
CAP_SECRET_KEY=changethis
|
||||
###< trycap.dev/cap ###
|
||||
|
||||
###> minio/minio ###
|
||||
MINIO_ROOT_USER=changethis
|
||||
MINIO_ROOT_PASSWORD=changethis
|
||||
MINIO_ENDPOINT=http://localhost:9010
|
||||
MINIO_PUBLIC_URL=https://your-minio-subdomain.example.com
|
||||
###< minio/minio ###
|
||||
###> object-storage ###
|
||||
STORAGE_ACCESS_KEY=changethis
|
||||
STORAGE_SECRET_KEY=changethis
|
||||
STORAGE_ENDPOINT=http://localhost:9010
|
||||
STORAGE_PUBLIC_URL=https://your-storage-subdomain.example.com
|
||||
###< object-storage ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=smtp://localhost:1025
|
||||
|
||||
@@ -33,12 +33,12 @@ jobs:
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose build
|
||||
|
||||
- name: Start services
|
||||
- name: Deploy
|
||||
env:
|
||||
APP_VERSION: ${{ vars.APP_VERSION }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose up -d minio
|
||||
docker compose rm -sf minio_init
|
||||
docker compose run --rm minio_init
|
||||
docker compose up -d
|
||||
# --remove-orphans removes retired service containers, not named volumes.
|
||||
docker compose up -d --wait --wait-timeout 180 --remove-orphans
|
||||
docker compose rm -sf rustfs_init
|
||||
|
||||
@@ -11,7 +11,7 @@ This document provides guidelines and context for AI coding agents working on th
|
||||
- **Backend:** Symfony 7.2 (PHP 8.3)
|
||||
- **Frontend:** React 18, Vite 6
|
||||
- **Database:** PostgreSQL 17 with materialized views
|
||||
- **Storage:** MinIO (S3-compatible)
|
||||
- **Storage:** RustFS (S3-compatible)
|
||||
- **Real-time:** Mercure (Server-Sent Events)
|
||||
- **Styling:** SCSS, MUI (Material-UI), Emotion
|
||||
- **Fonts:** @fontsource packages (web), Carlito-Bold.ttf (server-side images)
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
header X-Forwarded-Host {host}
|
||||
|
||||
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
|
||||
subscriber_jwt {$MERCURE_JWT_SECRET} HS256
|
||||
anonymous
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make start - Start services and initialize MinIO (uses existing images)"
|
||||
@echo " make start-build - Build, start services, and initialize MinIO"
|
||||
@echo " make start - Start services"
|
||||
@echo " make start-build - Build and start services"
|
||||
@echo " make stop - Stop running services"
|
||||
@echo " make build - Build Docker images only"
|
||||
@echo " make down - Stop and remove containers/networks"
|
||||
@@ -19,18 +19,14 @@ help:
|
||||
@echo " make test - Run PHPUnit tests"
|
||||
|
||||
start:
|
||||
docker compose up -d minio
|
||||
docker compose rm -sf minio_init
|
||||
docker compose run --rm minio_init
|
||||
docker compose up -d
|
||||
docker compose up -d --remove-orphans app scheduler
|
||||
docker compose rm -sf rustfs_init
|
||||
|
||||
start-build:
|
||||
composer i
|
||||
bun run build
|
||||
docker compose up -d --build minio
|
||||
docker compose rm -sf minio_init
|
||||
docker compose run --rm minio_init
|
||||
docker compose up -d --build
|
||||
docker compose up -d --build --remove-orphans app scheduler
|
||||
docker compose rm -sf rustfs_init
|
||||
|
||||
stop:
|
||||
docker compose stop
|
||||
|
||||
@@ -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
|
||||
- **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
|
||||
- **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
|
||||
- **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 |
|
||||
| Database | PostgreSQL 18 |
|
||||
| Real-time | Mercure (built into FrankenPHP / Caddy) |
|
||||
| File storage | MinIO (S3-compatible) |
|
||||
| File storage | RustFS (S3-compatible) |
|
||||
| Image processing | LiipImagine + Flysystem |
|
||||
| Server | FrankenPHP (Caddy + PHP in one binary) |
|
||||
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
|
||||
@@ -44,7 +44,7 @@ Created by [SplendidBear](https://www.splendidbear.org).
|
||||
- PostgreSQL 18
|
||||
- Caddy with FrankenPHP and the Mercure module
|
||||
- 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 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` |
|
||||
| `POSTGRES_USER/PASSWORD/DB` | Your PostgreSQL credentials |
|
||||
| `MINIO_ROOT_USER/PASSWORD` | MinIO admin credentials |
|
||||
| `MINIO_ENDPOINT` | `http://localhost:9010` (bare-metal) |
|
||||
| `MINIO_PUBLIC_URL` | Public URL browsers use to reach MinIO |
|
||||
| `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` | RustFS S3 credentials |
|
||||
| `STORAGE_ENDPOINT` | RustFS S3 API URL (`http://localhost:9010` for bare-metal) |
|
||||
| `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_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) |
|
||||
@@ -104,7 +104,7 @@ sudo systemctl reload caddy
|
||||
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`).
|
||||
|
||||
@@ -119,21 +119,15 @@ 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.
|
||||
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
|
||||
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
|
||||
```
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
Paste the filled-in `.env` file as the secret value:
|
||||
@@ -215,10 +211,10 @@ 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
|
||||
STORAGE_ACCESS_KEY=mineseeker
|
||||
STORAGE_SECRET_KEY="<strong password>"
|
||||
STORAGE_ENDPOINT=http://rustfs:9000
|
||||
STORAGE_PUBLIC_URL=https://aws.mineseeker.hu
|
||||
|
||||
MAILER_DSN=smtp://mail:25?verify_peer=0
|
||||
MAIL_DOMAIN=mineseeker.hu
|
||||
@@ -270,7 +266,7 @@ 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`.
|
||||
This writes `.env`, builds the Docker images, runs database migrations, initializes RustFS, and starts the application services.
|
||||
|
||||
#### 4. Verify
|
||||
|
||||
|
||||
@@ -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"
|
||||
Executable
+54
@@ -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"
|
||||
@@ -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
@@ -28,6 +28,12 @@ if [ -z "$DB_USER" ]; then
|
||||
exit 1
|
||||
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..."
|
||||
docker exec -i "$CONTAINER_ID" pg_restore --list < "$BACKUP_FILE" > /dev/null
|
||||
echo "Archive OK."
|
||||
@@ -53,13 +59,26 @@ trap restart_services EXIT
|
||||
echo "Stopping MineSeeker services that use PostgreSQL..."
|
||||
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 \
|
||||
--username="$DB_USER" \
|
||||
--dbname=postgres \
|
||||
--clean \
|
||||
--create \
|
||||
--if-exists \
|
||||
--dbname="$DB_NAME" \
|
||||
--no-owner \
|
||||
--no-acl \
|
||||
--exit-on-error < "$BACKUP_FILE"
|
||||
|
||||
echo "Restore complete."
|
||||
|
||||
Executable
+77
@@ -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
@@ -6,7 +6,7 @@ services:
|
||||
restart: unless-stopped
|
||||
container_name: '${APP_NAME}-app'
|
||||
ports:
|
||||
- "10080:80"
|
||||
- "${APP_PUBLISHED_PORT:-10080}:80"
|
||||
environment:
|
||||
SERVER_NAME: ${SERVER_NAME:-:80}
|
||||
APP_ENV: prod
|
||||
@@ -30,10 +30,10 @@ services:
|
||||
CAP_SECRET_KEY: ${CAP_SECRET_KEY}
|
||||
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
|
||||
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9010}
|
||||
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||
STORAGE_ENDPOINT: http://rustfs:9000
|
||||
STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL:-http://localhost:9010}
|
||||
# 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 Kubernetes or external proxy, use the proxy's IP address.
|
||||
@@ -43,11 +43,21 @@ services:
|
||||
- app_var:/app/var
|
||||
- caddy_data:/data
|
||||
- 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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
mail:
|
||||
condition: service_started
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
rustfs_init:
|
||||
condition: service_completed_successfully
|
||||
scheduler:
|
||||
build:
|
||||
context: .
|
||||
@@ -75,10 +85,10 @@ services:
|
||||
CAP_SECRET_KEY: ${CAP_SECRET_KEY}
|
||||
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
|
||||
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9010}
|
||||
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||
STORAGE_ENDPOINT: http://rustfs:9000
|
||||
STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL:-http://localhost:9010}
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1}
|
||||
volumes:
|
||||
- app_var:/app/var
|
||||
@@ -87,42 +97,49 @@ services:
|
||||
condition: service_healthy
|
||||
mail:
|
||||
condition: service_started
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
rustfs_init:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.3
|
||||
restart: unless-stopped
|
||||
container_name: '${APP_NAME}-minio'
|
||||
command: server /data --console-address ":9001"
|
||||
ports:
|
||||
- "9010:9000"
|
||||
- "9011:9001"
|
||||
container_name: '${APP_NAME}-rustfs'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
RUSTFS_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||
RUSTFS_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||
RUSTFS_ADDRESS: ':9000'
|
||||
RUSTFS_CONSOLE_ADDRESS: ':9001'
|
||||
RUSTFS_CONSOLE_ENABLE: 'true'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
- rustfs_data:/data
|
||||
ports:
|
||||
- "${RUSTFS_PUBLISHED_API_PORT:-9010}:9000"
|
||||
- "${RUSTFS_PUBLISHED_CONSOLE_PORT:-9011}:9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
minio_init:
|
||||
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1
|
||||
profiles: ['bootstrap']
|
||||
retries: 20
|
||||
start_period: 15s
|
||||
rustfs_init:
|
||||
build:
|
||||
context: ./docker/storage-init
|
||||
entrypoint: ["python"]
|
||||
command: ["/usr/local/bin/storage-init.py"]
|
||||
restart: "no"
|
||||
container_name: '${APP_NAME}-minio-init'
|
||||
container_name: '${APP_NAME}-rustfs-init'
|
||||
depends_on:
|
||||
minio:
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- ./docker/minio-init.sh:/minio-init.sh:ro
|
||||
entrypoint: ["/bin/sh", "/minio-init.sh"]
|
||||
TARGET_ENDPOINT: http://rustfs:9000
|
||||
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||
STORAGE_BUCKET: mineseeker
|
||||
mail:
|
||||
image: boky/postfix:latest
|
||||
restart: unless-stopped
|
||||
@@ -157,11 +174,11 @@ services:
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
ports:
|
||||
- "5452:5432"
|
||||
- "${POSTGRES_PUBLISHED_PORT:-5452}:5432"
|
||||
volumes:
|
||||
app_var:
|
||||
postgres_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
postfix_spool:
|
||||
minio_data:
|
||||
rustfs_data:
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "splendid-bear/mineseeker",
|
||||
"version": "2026.3.0",
|
||||
"version": "2026.4.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "https://www.splendidbear.org",
|
||||
"bugs": "https://source.splendidbear.org",
|
||||
@@ -10,7 +10,7 @@
|
||||
"prefer-stable": true,
|
||||
"private": true,
|
||||
"require": {
|
||||
"php": ">=8.5",
|
||||
"php": ">=8.5.12",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
@@ -33,8 +33,8 @@
|
||||
"symfony/framework-bundle": "8.1.*",
|
||||
"symfony/http-client": "8.1.*",
|
||||
"symfony/mailer": "8.1.*",
|
||||
"symfony/mercure": "^0.8",
|
||||
"symfony/mercure-bundle": "^0.5",
|
||||
"symfony/mercure": "^0.9",
|
||||
"symfony/mercure-bundle": "^0.6",
|
||||
"symfony/messenger": "8.1.*",
|
||||
"symfony/monolog-bundle": "^4.0",
|
||||
"symfony/scheduler": "8.1.*",
|
||||
|
||||
Generated
+984
-837
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,12 @@ liip_imagine:
|
||||
default:
|
||||
flysystem:
|
||||
filesystem_service: mineseeker.media.storage
|
||||
cache: minio_cache
|
||||
cache: object_storage_cache
|
||||
resolvers:
|
||||
minio_cache:
|
||||
object_storage_cache:
|
||||
flysystem:
|
||||
filesystem_service: mineseeker.cache.storage
|
||||
root_url: '%env(MINIO_PUBLIC_URL)%/mineseeker/cache'
|
||||
root_url: '%env(STORAGE_PUBLIC_URL)%/mineseeker/cache'
|
||||
cache_prefix: 'liip'
|
||||
filter_sets:
|
||||
avatar_thumb:
|
||||
|
||||
@@ -3,6 +3,7 @@ mercure:
|
||||
default:
|
||||
url: '%env(MERCURE_URL)%'
|
||||
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||
protocol_version: '0.x'
|
||||
jwt:
|
||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||
publish: '*'
|
||||
|
||||
@@ -29,14 +29,14 @@ services:
|
||||
arguments:
|
||||
$cacheDir: '%kernel.project_dir%/var/og-cache'
|
||||
$fontPath: '%kernel.project_dir%/assets/fonts/Carlito-Bold.ttf'
|
||||
$minioMediaStorage: '@mineseeker.media.storage'
|
||||
$mediaStorage: '@mineseeker.media.storage'
|
||||
|
||||
Aws\S3\S3Client:
|
||||
arguments:
|
||||
- version: 'latest'
|
||||
region: 'us-east-1'
|
||||
endpoint: '%env(MINIO_ENDPOINT)%'
|
||||
endpoint: '%env(STORAGE_ENDPOINT)%'
|
||||
use_path_style_endpoint: true
|
||||
credentials:
|
||||
key: '%env(MINIO_ROOT_USER)%'
|
||||
secret: '%env(MINIO_ROOT_PASSWORD)%'
|
||||
key: '%env(STORAGE_ACCESS_KEY)%'
|
||||
secret: '%env(STORAGE_SECRET_KEY)%'
|
||||
|
||||
@@ -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."
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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
@@ -60,8 +60,8 @@ Quick command reference for testing:
|
||||
### [Scheduled Tasks](./SCHEDULER.md)
|
||||
Symfony Scheduler tasks and the Docker worker that runs them.
|
||||
|
||||
### [MinIO Initialization](./MINIO_INITIALIZATION.md)
|
||||
Automatic, one-shot setup of the MinIO bucket and its access policy.
|
||||
### Object storage
|
||||
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)
|
||||
Comprehensive guide for continuous integration and deployment:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mine-seeker",
|
||||
"version": "2026.3.0",
|
||||
"version": "2026.4.0",
|
||||
"author": "https://www.splendidbear.org",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": "https://source.splendidbear.org",
|
||||
|
||||
@@ -178,7 +178,7 @@ class ProfileController extends AbstractController
|
||||
$cacheManager->remove($oldPath, 'avatar_thumb');
|
||||
}
|
||||
|
||||
/** Upload original to MinIO media/avatar/ */
|
||||
/** Upload original avatar to object storage. */
|
||||
$stream = fopen($file->getPathname(), 'rb');
|
||||
try {
|
||||
$mediaStorage->writeStream($newPath, $stream);
|
||||
|
||||
@@ -39,7 +39,7 @@ final class BattleCardGenerator
|
||||
public function __construct(
|
||||
private readonly string $cacheDir,
|
||||
private readonly string $fontPath,
|
||||
private readonly FilesystemOperator $minioMediaStorage,
|
||||
private readonly FilesystemOperator $mediaStorage,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
@@ -193,12 +193,12 @@ final class BattleCardGenerator
|
||||
{
|
||||
$avatarImg = null;
|
||||
|
||||
/** Try to load avatar from MinIO if path exists*/
|
||||
/** Try to load avatar from object storage if a path exists. */
|
||||
if ($avatarPath) {
|
||||
try {
|
||||
/** Remove 'avatar/' prefix if it exists since storage already has media/ prefix*/
|
||||
$path = str_starts_with($avatarPath, 'avatar/') ? $avatarPath : 'avatar/' . $avatarPath;
|
||||
$avatarData = $this->minioMediaStorage->read($path);
|
||||
$avatarData = $this->mediaStorage->read($path);
|
||||
$avatarImg = imagecreatefromstring($avatarData);
|
||||
} catch (Exception $e) {
|
||||
/** Failed to load avatar, will use initials*/
|
||||
|
||||
Reference in New Issue
Block a user