docker compose brings up the app container. You bring your own PostgreSQL
16+ — the app talks to any reachable Postgres instance and the Compose file
does not bundle a database anymore.
All connection details — host, port, user, password, name, ssl,
pool tuning — live in config/database.yaml (a single YAML
file). The Compose file mounts the YAML read-only into the container
at /app/config/database.yaml; the app reads it on startup and
refuses to boot if it's missing or the password is empty.
The default form has the password in the YAML. Two indirection
options are also supported via ${VAR} / ${VAR:-default} references
(see "Supplying the password" below) for users who want to keep the
password out of the file.
- Docker Engine 20.10+ with the Compose v2 plugin (
docker compose, notdocker-compose) - A reachable PostgreSQL 16+ instance (local, sidecar, managed, etc.)
- Outbound HTTPS to
https://api.minimaxi.com(the MiniMax API) - Python 3.13 (only for the optional smoke test)
# 1. Clone
git clone https://github.com/<your-org>/minimax-tool.git
cd minimax-tool
# 2. Create the host master-key file. The container will see it as
# /app/.master_key, write a fresh Fernet key on first run, and
# bind-mount the change back to the host.
touch .master_key
chmod 600 .master_key
# 3. Create the database config from the template and fill in the
# real values. The default form has the password inline; the
# file is gitignored so it stays out of git.
cp config/database.yaml.example config/database.yaml
$EDITOR config/database.yaml # set host / user / password / name
# 4. Build and start
docker compose up -d --build
# 5. Verify
docker compose ps # app container healthy
curl http://localhost:9060/api/health
# → {"status":"ok","db":true,"version":"0.2.0"}The SPA is at http://localhost:9060. Compose binds it to 127.0.0.1
by default, so it is not reachable directly from another machine.
For a bundled database, use docker-compose.pg18.yml instead of the default
external-database stack:
cp config/database.pg18.yaml.example config/database.pg18.yaml
touch .master_key && chmod 600 .master_key
export POSTGRES_PASSWORD='replace-with-a-strong-password'
docker compose -f docker-compose.pg18.yml up -d --buildThe default named volumes are minimax_pg18_data and minimax_uploads.
Override their Docker names with PG_VOLUME_NAME / UPLOAD_VOLUME_NAME, or
set PG_DATA_PATH / UPLOAD_DATA_PATH to absolute host paths for bind mounts.
PostgreSQL 18 uses /var/lib/postgresql as its persistent mount point.
Set both variables before starting the stack. Browsers display their native login dialog; API clients send the same HTTP Basic credentials.
export APP_USERNAME=minimax-admin
export APP_PASSWORD="$(openssl rand -base64 32)" # save this in your password manager
docker compose up -d --build/api/health intentionally remains unauthenticated for container and load
balancer health checks. Use TLS at the reverse proxy: Basic credentials must
never travel over plaintext on an untrusted network.
For a same-host reverse proxy, keep the localhost binding. To bind deliberately
to another interface, set BIND_ADDRESS, and set ALLOWED_ORIGINS to a
comma-separated list of exact HTTPS origins when cross-origin API access is needed.
| Path (host) | Path (in container) | Purpose |
|---|---|---|
config/database.yaml |
/app/config/database.yaml (ro) |
DB connection (host, port, user, password, name, pool, ssl) |
.master_key |
/app/.master_key |
Fernet key for at-rest encryption |
uploads/ (volume) |
/app/uploads/ |
Generated images / voice / music / video |
| (your external Postgres) | — | Database files — managed by you, not by Compose |
config/database.yaml,.master_key, and theminimax_uploadsvolume are persistent acrossdocker compose down/upcycles. Your external Postgres is unaffected bydocker compose down.
Three options, in increasing complexity:
-
Inline in the YAML (default — simplest):
password: "your-strong-password"
The file is gitignored, so the password stays out of git. GitHub secret scanning will alert you if it ever lands in a commit. This is fine for a single-host personal tool.
-
Reference + env var (handy for CI / production):
password: "${DB_PASSWORD}"
Then
export DB_PASSWORD=...in the shell (or inject from your secret manager). The YAML is safe to commit; the password lives in the secret manager. -
Reference + Docker secret (most secure for swarm / k8s-style):
password: "${DB_PASSWORD_FILE:-/run/secrets/db_password}"
Add a
secrets:block todocker-compose.ymland mount the file at/run/secrets/db_password. The password is never in an environment variable or in any committed file.
# Tail logs
docker compose logs -f app
# Open a shell inside the app container
docker compose exec app sh
# Inspect the database config as the app sees it
docker compose exec app python -c "from app.config import get_settings, active_config_path; s = get_settings(); print('config:', active_config_path()); print('host:', s.db['host'], 'name:', s.db['name'])"
# Restart after changing the Dockerfile or requirements
docker compose build --no-cache app
docker compose up -d
# Restart after changing the frontend code
# (a rebuild is required — see "Frontend rebuilds" below)
docker compose build --no-cache app
docker compose up -d
# Stop everything (keeps the uploads volume + host .master_key + database.yaml)
docker compose down
# Nuke everything this Compose file owns (uploads + master key)
docker compose down -v
rm -f .master_keyThe Vite build output is baked into the Docker image at build time. docker compose restart does not pick up frontend code changes — you need to rebuild:
# After editing files under frontend/
docker compose build --no-cache app
docker compose up -dFor a fast iteration loop, run the Vite dev server on the host and point it at the containerized backend:
cd frontend
npm run dev # http://localhost:5173, proxying /api → :9060The vite.config.ts already sets up the proxy.
# 1. Database dump — read host/port/user/db straight from the YAML
# so the backup script and the app can never disagree.
DB_HOST=$(yq '.database.host' config/database.yaml)
DB_PORT=$(yq '.database.port' config/database.yaml)
DB_USER=$(yq '.database.user' config/database.yaml)
DB_NAME=$(yq '.database.name' config/database.yaml)
pg_dump -h "$DB_HOST" -p "${DB_PORT:-5432}" -U "$DB_USER" "$DB_NAME" > backup-$(date +%F).sql
# 2. Master key (already in your project root, but copy to a separate safe place)
cp .master_key .master_key.bak
# 3. Database YAML (mostly for reproducibility, not a secret once
# the password is exported as DB_PASSWORD).
cp config/database.yaml config/database.yaml.bak
# Store the files in DIFFERENT places.To restore on a fresh host:
# 1. Drop the master key in place
cp .master_key.bak .master_key
chmod 600 .master_key
# 2. Restore the DB
cat backup-2026-06-09.sql | psql -h "$DB_HOST" -p "${DB_PORT:-5432}" -U "$DB_USER" "$DB_NAME"
# 3. Restore the YAML
cp config/database.yaml.bak config/database.yaml
# 4. Start
docker compose up -dgit pull
docker compose build
docker compose up -dSchema migrations run automatically on app startup (idempotent). Seed configs are also applied automatically — but they do not overwrite rows you already customized (your API keys, model overrides, etc., are preserved).
If a new release changes the shape of a seed config (e.g. adds a field to the request template), run scripts/fix-seeds.sh once to bring existing rows up to date. The user's api_key_encrypted and any customizations are preserved.
- Set an explicit
MASTER_KEYenv var indocker-compose.ymlso the key is not generated into a host file. Generate with:python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" - Set a strong password in
config/database.yaml(the file is gitignored; rotate the underlying Postgres user password periodically and update the YAML to match) - Set
APP_USERNAMEand a randomAPP_PASSWORD - Put the app behind a reverse proxy (Caddy, nginx) with TLS
- Mount the
.master_key, theconfig/database.yaml, and the externally-hosted Postgres data dir onto a backup target - Set up log aggregation (the app logs to stdout, so any docker-log driver works)
- Keep
BIND_ADDRESS=127.0.0.1; only change it deliberately - Set
ALLOWED_ORIGINSonly when a separate frontend origin is required - Keep
ALLOW_PRIVATE_UPSTREAMS=falseand tuneMAX_DOWNLOAD_BYTES
| Symptom | Cause | Fix |
|---|---|---|
Container minimax-app-1 Restarting |
Permission denied on /app/.master_key or upstream error |
docker compose logs app to see the trace |
No database config found on startup |
config/database.yaml is missing or mounted incorrectly |
Confirm the file exists at the host path shown in the volumes section, then docker compose config to see the resolved mount |
Database password is empty on startup |
The YAML has password: "", or ${DB_PASSWORD} is unset |
Either paste the password in the YAML, or export DB_PASSWORD=... before docker compose up (if the YAML uses ${DB_PASSWORD}) |
Connection refused to Postgres |
External DB is not reachable from the app container | Check database.host in config/database.yaml; from inside the container, docker compose exec app sh -c 'nc -zv <host> <port>' |
Address already in use on port 9060 |
Another process on the host owns the port | `lsof -nP -i:9060 -t |
| Stale UI after a frontend change | Image not rebuilt | docker compose build --no-cache app && docker compose up -d |
| All stored API keys become unreadable | .master_key file was deleted or replaced |
Restore the master key from backup. Otherwise, re-paste each API key. |
FATAL: password authentication failed for postgres |
The password in config/database.yaml doesn't match the Postgres user |
Update config/database.yaml to match, or change the Postgres user's password to match |