Add public demo mode with auto-seeding, hourly reset, and Portainer deploy guide
- DEMO_MODE=true env flag: disables password changes and backup endpoints (403), exposes GET /demo/status for frontend detection - Auto-seed on first startup: creates demo user (demo@mymidas.app / demo123) with 6 months of transactions, investments, budgets, subscriptions, and tax payslips; takes a pg_dump snapshot immediately after for hourly restore - Hourly reset: resetter Alpine container with cron restores DB from snapshot and purges uploaded attachments every hour on the hour - Frontend: amber demo banner on all pages, login page shows credentials, password change disabled with notice, backups section replaced with notice - demo/ directory: self-contained docker-compose.yml (ports 4001/8091), .env.example, reset.sh, and step-by-step Portainer DEPLOY.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
afb5e99bb2
commit
9897d03d91
17 changed files with 975 additions and 2 deletions
18
demo/.env.example
Normal file
18
demo/.env.example
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# MyMidas Demo — environment variables
|
||||
# Copy to .env and fill in every value before deploying.
|
||||
|
||||
# ── Encryption ────────────────────────────────────────────────────────────────
|
||||
# 32-byte hex key for AES-256-GCM field encryption.
|
||||
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
# ── Database ──────────────────────────────────────────────────────────────────
|
||||
# Strong random password for the demo Postgres user.
|
||||
DB_PASSWORD=
|
||||
|
||||
# ── Redis ─────────────────────────────────────────────────────────────────────
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# ── Environment ───────────────────────────────────────────────────────────────
|
||||
# Keep as "production" (hides /docs, enforces security headers).
|
||||
ENVIRONMENT=production
|
||||
152
demo/DEPLOY.md
Normal file
152
demo/DEPLOY.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# MyMidas Demo — Deployment Guide (Portainer)
|
||||
|
||||
This guide deploys the public demo instance on a separate server using Portainer Stacks.
|
||||
|
||||
**What you get:** a fully seeded MyMidas instance at port 4001, with demo data and an hourly auto-reset. No manual steps after initial deploy.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Portainer installed on the demo server
|
||||
- SSH or console access to the demo server (for the initial clone and key generation)
|
||||
- Your reverse proxy pointing a public domain at port `4001` on this server
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Clone the repo
|
||||
|
||||
On the demo server, clone into your preferred location:
|
||||
|
||||
```bash
|
||||
git clone https://git.rdx4.com/megaproxy/MyMidas.git
|
||||
cd MyMidas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Generate JWT keys
|
||||
|
||||
The demo shares the `secrets/` directory with the main app structure. If you've already generated keys on this server you can skip this.
|
||||
|
||||
```bash
|
||||
mkdir -p secrets
|
||||
openssl genrsa -out secrets/jwt_private.pem 4096
|
||||
openssl rsa -in secrets/jwt_private.pem -pubout -out secrets/jwt_public.pem
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Create the demo .env file
|
||||
|
||||
```bash
|
||||
cd demo
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Open `.env` and fill in the three required values:
|
||||
|
||||
| Variable | How to generate |
|
||||
|---|---|
|
||||
| `ENCRYPTION_KEY` | `python3 -c "import secrets; print(secrets.token_hex(32))"` |
|
||||
| `DB_PASSWORD` | Any strong random string |
|
||||
| `REDIS_PASSWORD` | Any strong random string |
|
||||
|
||||
Leave `ENVIRONMENT=production`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Deploy the stack in Portainer
|
||||
|
||||
1. Open Portainer → **Stacks** → **Add stack**
|
||||
2. Name it `mymidas-demo`
|
||||
3. Select **Repository** as the build method
|
||||
4. Set the repository URL to your Gitea URL and branch `main`
|
||||
5. Set **Compose path** to `demo/docker-compose.yml`
|
||||
6. Under **Environment variables**, add the four variables from your `.env`:
|
||||
- `ENCRYPTION_KEY`
|
||||
- `DB_PASSWORD`
|
||||
- `REDIS_PASSWORD`
|
||||
- `ENVIRONMENT` = `production`
|
||||
7. Click **Deploy the stack**
|
||||
|
||||
> **Alternative (upload method):** If you prefer to upload the compose file directly, paste the contents of `demo/docker-compose.yml` into Portainer's web editor and add the environment variables manually.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Wait for first-time seeding
|
||||
|
||||
On first startup, the backend will:
|
||||
1. Run database migrations
|
||||
2. Detect that `DEMO_MODE=true` and no users exist
|
||||
3. Seed the full demo dataset (~180 transactions, investments, budgets, tax data)
|
||||
4. Save a compressed snapshot (`demo_snapshot.sql.gz`) for hourly resets
|
||||
|
||||
This takes about 30–60 seconds. Watch progress in Portainer → **Containers** → `mymidas-demo-backend-1` → **Logs**.
|
||||
|
||||
You'll see these log lines when ready:
|
||||
```
|
||||
demo_seed_complete
|
||||
demo_snapshot_created
|
||||
Uvicorn running on http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Configure your reverse proxy
|
||||
|
||||
Point your public domain at `http://<demo-server-ip>:4001`.
|
||||
|
||||
The frontend serves the React app and proxies all `/api/` calls to the backend internally — so you only need to expose port `4001`.
|
||||
|
||||
**nginx proxy manager example:**
|
||||
- Scheme: `http`
|
||||
- Forward hostname/IP: `<demo-server-ip>`
|
||||
- Forward port: `4001`
|
||||
- Enable websockets: off
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Verify
|
||||
|
||||
Open your domain in a browser. You should see the MyMidas login page with a yellow demo credentials banner:
|
||||
|
||||
```
|
||||
Email: demo@mymidas.app
|
||||
Password: demo123
|
||||
```
|
||||
|
||||
Log in and confirm the data is populated (accounts, transactions, investments, tax page).
|
||||
|
||||
---
|
||||
|
||||
## Hourly reset
|
||||
|
||||
The `resetter` container runs a cron job at the top of every hour that:
|
||||
1. Restores the database from the snapshot taken on first boot
|
||||
2. Deletes any files uploaded by demo users
|
||||
|
||||
No action needed — it runs automatically. You can check reset logs in Portainer → **Containers** → `mymidas-demo-resetter-1` → **Logs**.
|
||||
|
||||
---
|
||||
|
||||
## Updating the demo
|
||||
|
||||
When you push code changes to main:
|
||||
|
||||
1. In Portainer → **Stacks** → `mymidas-demo` → **Editor** → **Update the stack**
|
||||
2. This rebuilds the images and restarts all containers
|
||||
3. On restart, migrations run automatically; the seed check runs and skips if already seeded (snapshot is preserved on the `demo_snapshot` volume)
|
||||
|
||||
> If you want to **force a full re-seed** (e.g. after adding more demo data): in Portainer, delete the `demo_snapshot` volume, then redeploy. The backend will re-seed and take a new snapshot on next startup.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|---|---|
|
||||
| Blank page / 502 | Backend still starting — wait 60 s and refresh |
|
||||
| Login fails | Seeding still in progress — check backend logs |
|
||||
| Data not resetting | Check resetter logs; confirm `demo_snapshot` volume has the `.sql.gz` file |
|
||||
| "Snapshot not found" in resetter log | Backend may not have finished first-time seed — redeploy backend only |
|
||||
128
demo/docker-compose.yml
Normal file
128
demo/docker-compose.yml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8091:8000"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://demo_app:${DB_PASSWORD}@postgres:5432/demodb"
|
||||
REDIS_URL: "redis://:${REDIS_PASSWORD}@redis:6379/0"
|
||||
ENCRYPTION_KEY: "${ENCRYPTION_KEY}"
|
||||
BACKUP_PASSPHRASE: "not-used-in-demo"
|
||||
ENVIRONMENT: "${ENVIRONMENT:-production}"
|
||||
ALLOW_REGISTRATION: "false"
|
||||
BASE_CURRENCY: "GBP"
|
||||
DEMO_MODE: "true"
|
||||
DEMO_SNAPSHOT_PATH: "/app/demo_snapshot.sql.gz"
|
||||
volumes:
|
||||
- ../secrets:/run/secrets:ro
|
||||
- demo_snapshot:/app/demo_snapshot.sql.gz:rw
|
||||
- demo_uploads:/app/uploads
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- frontend_net
|
||||
- backend_net
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4001:3000"
|
||||
networks:
|
||||
- frontend_net
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /var/run
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: demodb
|
||||
POSTGRES_USER: demo_app
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
volumes:
|
||||
- demo_postgres:/var/lib/postgresql/data
|
||||
- ../postgres/init:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- backend_net
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U demo_app -d demodb"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass "${REDIS_PASSWORD}"
|
||||
networks:
|
||||
- backend_net
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
resetter:
|
||||
image: alpine:3.19
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://demo_app:${DB_PASSWORD}@postgres:5432/demodb"
|
||||
DEMO_SNAPSHOT_PATH: "/snapshot/demo_snapshot.sql.gz"
|
||||
BACKEND_URL: "http://backend:8000"
|
||||
volumes:
|
||||
- demo_snapshot:/snapshot:ro
|
||||
- demo_uploads:/uploads:rw
|
||||
- ./reset.sh:/reset.sh:ro
|
||||
networks:
|
||||
- backend_net
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
apk add --no-cache postgresql-client curl &&
|
||||
echo '0 * * * * sh /reset.sh >> /var/log/reset.log 2>&1' | crontab - &&
|
||||
crond -f -l 6
|
||||
"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
volumes:
|
||||
demo_postgres:
|
||||
demo_snapshot:
|
||||
demo_uploads:
|
||||
|
||||
networks:
|
||||
frontend_net:
|
||||
driver: bridge
|
||||
backend_net:
|
||||
driver: bridge
|
||||
internal: true
|
||||
25
demo/reset.sh
Normal file
25
demo/reset.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
# Hourly demo reset — restore DB from snapshot, purge uploads, bounce backend.
|
||||
set -e
|
||||
|
||||
SNAPSHOT="${DEMO_SNAPSHOT_PATH:-/snapshot/demo_snapshot.sql.gz}"
|
||||
UPLOADS_DIR="${UPLOADS_DIR:-/uploads}"
|
||||
DB_URL="${DATABASE_URL}"
|
||||
BACKEND="${BACKEND_URL:-http://backend:8000}"
|
||||
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Starting demo reset"
|
||||
|
||||
# 1. Restore database from snapshot
|
||||
if [ ! -f "$SNAPSHOT" ]; then
|
||||
echo "ERROR: Snapshot not found at $SNAPSHOT — skipping reset"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gunzip -c "$SNAPSHOT" | psql --single-transaction -v ON_ERROR_STOP=1 "$DB_URL"
|
||||
echo " DB restored from snapshot"
|
||||
|
||||
# 2. Purge uploaded files (attachments added by demo users)
|
||||
find "$UPLOADS_DIR" -type f -not -name ".gitkeep" -delete 2>/dev/null || true
|
||||
echo " Uploads purged"
|
||||
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Demo reset complete"
|
||||
Loading…
Add table
Add a link
Reference in a new issue