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:

RequirementNotes
Docker + Docker ComposeAny 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 accountFree Sandbox tier is sufficient for local testing. See Plaid Setup.
SMTP credentialsRequired 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.

1

Create a Plaid account

Go to dashboard.plaid.com/signup and register. No credit card required.

2

Create an application

From the dashboard, create a new application. Under Keys, copy your Client ID and Sandbox Secret.

3

Configure ExMint

In your .env.dev file, set:

.env.dev
PLAID_ENV=sandbox
PLAID_CLIENT_ID=your_client_id_here
PLAID_SECRET=your_sandbox_secret_here
4

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.

1

Request Development access

In the Plaid Dashboard, go to Team Settings > Development Access and submit the short form. Approval is usually automatic.

2

Update your environment

.env
PLAID_ENV=development
PLAID_SECRET=your_development_secret_here   # different from Sandbox secret
NoteThe Development secret is different from the Sandbox secret. Copy it from Team Settings > Keys > Development in the Plaid Dashboard.

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).

ImportantProduction access requires a formal approval process with Plaid. Expect a review period of several business days. You will need to describe your use case and agree to Plaid's terms. For a personal or family self-hosted instance, the Development tier (100 items free) is usually sufficient.
1

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.

2

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
3

Update your production environment

.env.main
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.

VariableRequiredDescription
FLASK_ENVYesdev, stag, or prod. Controls debug mode, cookie security, and DB selection.
SECRET_KEYYesFlask session key. Generate: python -c "import secrets; print(secrets.token_hex(32))"
ENCRYPTION_KEYYesFernet key for Plaid token encryption. Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
DB_USERYesMySQL/MariaDB username
DB_PASSWORDYesMySQL/MariaDB password
DB_HOSTYesDatabase host. Use host.docker.internal when DB is on the host machine.
DB_PORTYesDatabase port, typically 3306.
DB_NAMEYesDatabase name.
PLAID_CLIENT_IDYesFrom Plaid Dashboard > Team Settings > Keys.
PLAID_SECRETYesEnvironment-specific Plaid secret (sandbox/development/production).
PLAID_ENVYessandbox, development, or production.
PLAID_WEBHOOK_URLNoPublic HTTPS URL for Plaid webhooks. Required for real-time transaction sync.
MAIL_SERVERYesSMTP server hostname.
MAIL_PORTYesSMTP port, typically 587.
MAIL_USERNAMEYesSMTP login username.
MAIL_PASSWORDYesSMTP login password or app-specific password.
APP_BASE_URLYesFull URL of the deployment, e.g. https://yourdomain.com. Used for password reset links.
ADMIN_EMAILYesEmail address that receives registration notifications.
BWS_ACCESS_TOKENNoOptional 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.

MySQL / MariaDB
-- 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:

bash
docker-compose --env-file .env.dev -p exmint-dev \
  run --rm flask-app flask db upgrade
TipAlways run 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

bash
# 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

bash
docker-compose --env-file .env.stag -p exmint-stag up -d --build

Production

bash
# 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
Production checklist Before going live: ensure 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:

  1. Ensure your ExMint deployment is accessible over HTTPS.
  2. Set PLAID_WEBHOOK_URL=https://yourdomain.com/plaid_webhook in your env file.
  3. Rebuild and restart the Docker container.
  4. New bank connections will automatically register the webhook with Plaid. Existing connections need to be re-linked to pick up the new webhook URL.
Sandbox webhooksYou can test webhooks in Sandbox using the Plaid Dashboard > Developers > Sandbox > "Fire a webhook" tool. Your server must be reachable from the internet for this to work — use a tunnel like ngrok for local testing.

Database Migrations

ExMint uses Alembic (via Flask-Migrate) for schema migrations. Migration files live in migrations/versions/.

bash
# 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
Always review auto-generated migrationsAlembic generates migrations from model diffs. Always review the generated file before committing — auto-detection can miss renames or produce unintended DROP COLUMN statements.

Secrets Management

ExMint resolves secrets through a two-tier lookup in secrets_manager.py:

  1. Environment variableos.getenv('KEY'). This is the recommended approach for self-hosters.
  2. Bitwarden Secrets Manager (BWS) — only used when BWS_ACCESS_TOKEN is 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:

bash
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.