Deployment Guide
Everything you need to self-host ExMint — from a free Plaid sandbox account to a hardened production setup. Covers Docker, environment configuration, database migrations, and optional Plaid Production access.
Prerequisites
You will need the following before deploying ExMint:
| Requirement | Notes |
|---|---|
| Docker + Docker Compose | Any recent stable version. Required for all deployment modes. |
| MySQL 8+ or MariaDB 10.6+ | SQLite works for local dev only. Not recommended for production. |
| Plaid account | Free Sandbox tier is sufficient for local testing. See Plaid Setup. |
| SMTP credentials | Required for password reset emails and admin notifications. Gmail, SendGrid, Mailgun, etc. |
| Domain + TLS (production) | Plaid requires HTTPS for Production webhooks. A reverse proxy (Nginx/Caddy) handles TLS. |
Plaid Setup
ExMint uses Plaid to connect bank accounts. Plaid offers three tiers — choose the one that matches your use case.
Sandbox (free, simulated data)
The Sandbox tier is free, requires no approval, and uses simulated financial data. It is the recommended starting point for all new deployments.
Create a Plaid account
Go to dashboard.plaid.com/signup and register. No credit card required.
Create an application
From the dashboard, create a new application. Under Keys, copy your Client ID and Sandbox Secret.
Configure ExMint
In your .env.dev file, set:
PLAID_ENV=sandbox PLAID_CLIENT_ID=your_client_id_here PLAID_SECRET=your_sandbox_secret_here
Use test credentials in Plaid Link
When the bank connection dialog opens, use these Plaid-provided test credentials for any institution:
- Username:
user_good - Password:
pass_good
For MFA-enabled test flows: code 1234. See the Plaid Sandbox docs for a full list.
Development (free, up to 100 real items)
The Development environment allows you to connect real bank accounts (up to 100 items) at no cost. It is suitable for personal use or testing with live data before going to Production.
Request Development access
In the Plaid Dashboard, go to Team Settings > Development Access and submit the short form. Approval is usually automatic.
Update your environment
PLAID_ENV=development PLAID_SECRET=your_development_secret_here # different from Sandbox secret
Production (paid, unlimited real items)
Production is required for deployments serving multiple users or connecting more than 100 real bank accounts. Plaid charges per connected item (bank account link).
Apply for Production access
In the Plaid Dashboard, go to Team Settings > Production Access. Fill in the application: describe your app, expected user volume, and the Plaid products you need (Transactions, Auth). Submit and wait for Plaid's approval email.
Complete Plaid's compliance checklist
Plaid requires you to configure:
- A privacy policy URL accessible from your deployment
- A Plaid-hosted Link customization with your logo and brand colors (optional but recommended)
- Webhook URL (HTTPS) for real-time transaction updates — see Webhooks
Update your production environment
PLAID_ENV=production PLAID_SECRET=your_production_secret_here PLAID_WEBHOOK_URL=https://yourdomain.com/plaid_webhook
Environment Variables
Copy .env.example to your target env file and fill in all values. ExMint uses different env files per environment: .env.dev, .env.stag, and .env.main.
| Variable | Required | Description |
|---|---|---|
FLASK_ENV | Yes | dev, stag, or prod. Controls debug mode, cookie security, and DB selection. |
SECRET_KEY | Yes | Flask session key. Generate: python -c "import secrets; print(secrets.token_hex(32))" |
ENCRYPTION_KEY | Yes | Fernet key for Plaid token encryption. Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" |
DB_USER | Yes | MySQL/MariaDB username |
DB_PASSWORD | Yes | MySQL/MariaDB password |
DB_HOST | Yes | Database host. Use host.docker.internal when DB is on the host machine. |
DB_PORT | Yes | Database port, typically 3306. |
DB_NAME | Yes | Database name. |
PLAID_CLIENT_ID | Yes | From Plaid Dashboard > Team Settings > Keys. |
PLAID_SECRET | Yes | Environment-specific Plaid secret (sandbox/development/production). |
PLAID_ENV | Yes | sandbox, development, or production. |
PLAID_WEBHOOK_URL | No | Public HTTPS URL for Plaid webhooks. Required for real-time transaction sync. |
MAIL_SERVER | Yes | SMTP server hostname. |
MAIL_PORT | Yes | SMTP port, typically 587. |
MAIL_USERNAME | Yes | SMTP login username. |
MAIL_PASSWORD | Yes | SMTP login password or app-specific password. |
APP_BASE_URL | Yes | Full URL of the deployment, e.g. https://yourdomain.com. Used for password reset links. |
ADMIN_EMAIL | Yes | Email address that receives registration notifications. |
BWS_ACCESS_TOKEN | No | Optional Bitwarden Secrets Manager token. When set, secrets are fetched from BWS instead of env vars. |
Database Setup
Create a MySQL/MariaDB database and user, then run the Alembic migrations to set up the schema.
-- Create database and user CREATE DATABASE exmint CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'exmint'@'%' IDENTIFIED BY 'your_strong_password'; GRANT ALL PRIVILEGES ON exmint.* TO 'exmint'@'%'; FLUSH PRIVILEGES;
Then run migrations via Docker:
docker-compose --env-file .env.dev -p exmint-dev \ run --rm flask-app flask db upgrade
flask db upgrade after pulling a new version of ExMint. See Database Migrations for the full workflow.Docker Deployment
ExMint uses a single docker-compose.yml for all environments, controlled by the FLASK_ENV variable and the env file you pass in.
Local / Development
# Clone and configure git clone https://github.com/manuelravila/ExMint.git cd ExMint cp .env.example .env.dev # Edit .env.dev with your values # Run migrations docker-compose --env-file .env.dev -p exmint-dev \ run --rm flask-app flask db upgrade # Start docker-compose --env-file .env.dev -p exmint-dev up -d --build
Open http://localhost:5000, register your first account, and connect a bank.
Staging
docker-compose --env-file .env.stag -p exmint-stag up -d --build
Production
# Apply any new migrations first docker-compose --env-file .env.main -p exmint-prod \ run --rm flask-app flask db upgrade # Build and start docker-compose --env-file .env.main -p exmint-prod up -d --build
FLASK_ENV=prod, TLS is terminated at your reverse proxy, APP_BASE_URL uses https://, and PLAID_ENV=production with your production secret. Registration is admin-controlled by default — the first registered user receives Admin role.Plaid Webhooks
Webhooks enable real-time transaction sync — new transactions appear automatically without a manual refresh. Without webhooks, users must sync manually.
To enable webhooks:
- Ensure your ExMint deployment is accessible over HTTPS.
- Set
PLAID_WEBHOOK_URL=https://yourdomain.com/plaid_webhookin your env file. - Rebuild and restart the Docker container.
- New bank connections will automatically register the webhook with Plaid. Existing connections need to be re-linked to pick up the new webhook URL.
Database Migrations
ExMint uses Alembic (via Flask-Migrate) for schema migrations. Migration files live in migrations/versions/.
# Apply all pending migrations flask db upgrade # Generate a new migration after a model change flask db migrate -m "describe your change" # Roll back one step flask db downgrade
DROP COLUMN statements.Secrets Management
ExMint resolves secrets through a two-tier lookup in secrets_manager.py:
- Environment variable —
os.getenv('KEY'). This is the recommended approach for self-hosters. - Bitwarden Secrets Manager (BWS) — only used when
BWS_ACCESS_TOKENis set in the environment. This is the maintainer's production deployment method and is entirely optional.
Self-hosters only need plain env vars. Simply omit BWS_ACCESS_TOKEN from your env file.
Troubleshooting
Login redirect loop
Usually caused by SESSION_COOKIE_SECURE=True when running without TLS. Ensure FLASK_ENV=dev in your local env file, which disables secure cookies automatically.
Database connection refused
When the database is on the host machine and the app is in Docker, use DB_HOST=host.docker.internal. On Linux, host.docker.internal may not be available — pass --add-host=host.docker.internal:host-gateway to Docker or use your host's LAN IP instead.
Plaid Link not opening
Ensure PLAID_CLIENT_ID and PLAID_SECRET are set correctly for the right environment (PLAID_ENV). Check the browser console for a 500 error on /api/create_link_token. If PLAID_WEBHOOK_URL is set but not a valid HTTPS URL, remove it until your webhook endpoint is ready.
Migrations fail with permission denied
Run migrations with explicit Python invocation inside Docker:
docker-compose --env-file .env.dev -p exmint-dev \ run --rm flask-app python -m flask db upgrade
Transactions not syncing automatically
Real-time sync requires Plaid webhooks. Ensure PLAID_WEBHOOK_URL is set, your server is reachable over HTTPS, and the container was rebuilt after adding the env var. You can always trigger a manual sync from the dashboard.