Managing personal and professional relationships without a system means forgetting follow-ups, losing track of birthdays, and letting important connections go cold. Monica is an open-source Personal CRM designed to fix this: it stores contacts, records interactions, tracks relationship context, and sends reminders — all running privately on your own infrastructure. Unlike cloud-based CRMs that monetize your contact data, Monica keeps every conversation note, life event, and reminder on hardware you control. This guide walks you through a production-grade deployment using Docker Compose, Caddy for automatic HTTPS and HTTP/2, and MySQL as the persistent backend — the combination that gives you the cleanest upgrade path and easiest operational footprint on a standard Ubuntu VPS.
Architecture and flow overview
Monica runs as a single Laravel PHP application container backed by a MySQL database. Caddy acts as the TLS-terminating reverse proxy, handling certificate issuance via ACME and forwarding HTTPS traffic to the Monica container on port 80. A dedicated Docker network isolates Monica from MySQL while keeping them co-located on the same host. When a user navigates to the configured domain, Caddy presents a valid Let's Encrypt certificate, proxies the request to the PHP-FPM-powered Monica app, which in turn reads and writes contact data to MySQL using an encrypted socket connection inside the Docker network.
The design is intentionally minimal: no Redis required (Monica uses file-based queue drivers by default), no separate job worker unless you enable cron-based reminders, and no object storage unless you attach document uploads. This makes it operationally straightforward — you get one app container, one database container, one reverse proxy, and a cron container for scheduled reminder emails.
Prerequisites
- Ubuntu 22.04 or 24.04 VPS with at least 1 CPU and 1 GB RAM (2 GB recommended for comfortable operation)
- Docker Engine 24+ and Docker Compose v2 installed (
docker compose versionto verify) - A domain name with a DNS A record pointing to the server (e.g.,
crm.example.com) - Ports 80 and 443 open in UFW and your cloud provider's security group
- An SMTP relay or email service (Mailgun, SendGrid, or local Postfix) for reminder delivery
Step-by-step deployment
1. Create the project directory and environment file
Create a dedicated directory and populate the required environment variables. Monica relies on an APP_KEY for encryption; generate it before first boot.
mkdir -p /opt/monica && cd /opt/monica
# Generate a 32-byte base64 app key
APP_KEY="base64:$(openssl rand -base64 32)"
echo "APP_KEY=${APP_KEY}" # Save this value
# Create the .env file
cat > .env << 'EOF'
APP_KEY=REPLACE_WITH_GENERATED_KEY
APP_ENV=production
APP_URL=https://crm.example.com
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=monica
DB_USERNAME=monica
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
[email protected]
MAIL_PASSWORD=CHANGE_THIS_MAIL_PASSWORD
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME=Monica
EOFReplace all placeholder values — REPLACE_WITH_GENERATED_KEY, CHANGE_THIS_STRONG_PASSWORD, crm.example.com, and mail credentials — before proceeding.
2. Write the Docker Compose file
Create the compose file that defines the Monica app container, MySQL database, and a scheduled task runner.
version: "3.9"
services:
app:
image: monica:4
restart: unless-stopped
env_file: .env
volumes:
- monica_storage:/var/www/html/storage
depends_on:
mysql:
condition: service_healthy
networks:
- monica_net
expose:
- "80"
cron:
image: monica:4
restart: unless-stopped
env_file: .env
command: cron
volumes:
- monica_storage:/var/www/html/storage
depends_on:
- app
networks:
- monica_net
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: CHANGE_ROOT_PASSWORD
MYSQL_DATABASE: monica
MYSQL_USER: monica
MYSQL_PASSWORD: CHANGE_THIS_STRONG_PASSWORD
volumes:
- mysql_data:/var/lib/mysql
networks:
- monica_net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pCHANGE_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
volumes:
monica_storage:
mysql_data:
networks:
monica_net:
driver: bridgeReplace both MySQL password references (CHANGE_ROOT_PASSWORD and CHANGE_THIS_STRONG_PASSWORD) with the values you set in .env. The cron service runs the Laravel scheduler loop that dispatches reminder emails at the configured intervals.
3. Configure Caddy as the reverse proxy
Create a Caddyfile in /etc/caddy/Caddyfile (or wherever your global Caddy config lives) for the Monica domain. Caddy will automatically obtain and renew the TLS certificate.
crm.example.com {
reverse_proxy 127.0.0.1:8080
encode gzip
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}If Caddy is running as a bare binary on the host (not inside Docker), add a ports mapping to the app service in the Compose file:
ports:
- "127.0.0.1:8080:80"This exposes Monica only on the loopback interface so it is not directly reachable from the public internet — Caddy is the only entry point.
4. Start the stack and run database migrations
Bring up all services, wait for MySQL to pass its health check, then run Monica's initial database migrations and seed command.
cd /opt/monica
docker compose up -d
# Wait for MySQL to become healthy
docker compose ps
# Run migrations (first-time setup)
docker compose exec app php artisan setup:production
# Check for errors
docker compose logs app --tail 40The setup:production command runs database migrations and seeds initial data. If prompted to confirm, press y. On subsequent upgrades, run php artisan migrate --force instead.
Configuration and secrets handling
All sensitive values live in the .env file. Apply restrictive permissions immediately after creation:
chmod 600 /opt/monica/.env
chown root:root /opt/monica/.envNever commit .env to version control. If you use a Git-based deployment workflow, add .env to .gitignore and supply secrets through a secrets manager (HashiCorp Vault, Infisical, or Docker Swarm secrets). For the MySQL root password, store it only in the Compose environment block or a Docker secret — do not echo it into log files or shell history.
For reminder emails, use an application-specific SMTP credential scoped to the sending domain. Rotating the mail password requires only updating .env and restarting the app and cron containers — no database migration needed.
Verification
Run these checks after the initial deployment to confirm all components are healthy before handing the URL to users:
# All containers running
docker compose ps
# MySQL is reachable from the app container
docker compose exec app php artisan tinker --execute="DB::connection()->getPdo() and print 'DB OK';"
# HTTPS certificate issued and valid
curl -I https://crm.example.com
# Cron worker is dispatching jobs (check scheduler output)
docker compose logs cron --tail 20A successful deployment shows all three services (app, cron, mysql) with status Up (healthy), the curl response returning HTTP/2 200, and cron logs showing Running scheduled command: Illuminate\Console\Scheduling\CallbackEvent at one-minute intervals.
Common issues and fixes
APP_KEY not set or malformed: Monica will throw a No application encryption key has been specified error. Regenerate with openssl rand -base64 32, prefix with base64:, and restart the app container.
MySQL connection refused on first boot: The app container starts before MySQL is fully initialized. The healthcheck + depends_on: condition: service_healthy pattern in the Compose file prevents this, but if you see connection errors, run docker compose restart app after MySQL shows healthy.
Caddy cannot obtain certificate (ACME challenge fails): Verify that port 80 is open to the internet (UFW: ufw allow 80/tcp) and that no other process is bound to port 80 on the host. Caddy uses HTTP-01 challenge by default and requires public port 80 reachability.
Reminder emails not sending: Check docker compose logs cron for SMTP authentication errors. Confirm your SMTP credentials in .env and test with docker compose exec app php artisan tinker --execute="Mail::raw('Test', fn(\$m) => \$m->to('[email protected]')->subject('Monica test'));".
Storage volume permissions: Monica writes uploaded files and cache to /var/www/html/storage. If uploads fail, check that the volume is mounted correctly and the container user has write access: docker compose exec app ls -la /var/www/html/storage.
FAQ
Can I use PostgreSQL instead of MySQL?
Monica's official Docker image is tested with MySQL 8.0 and MariaDB. PostgreSQL support was added in later versions but is considered experimental. For a production deployment requiring long-term stability, stick with MySQL 8.0 as documented here.
How do I upgrade Monica to a new version?
Pull the new image tag, recreate the app container, and run migrations: docker compose pull app && docker compose up -d app && docker compose exec app php artisan migrate --force. Always back up the MySQL data volume before upgrading.
How do I back up my contact data?
Monica's contact data lives entirely in MySQL. Create a backup with: docker compose exec mysql mysqldump -umonica -pCHANGE_THIS_STRONG_PASSWORD monica > /opt/monica/backup_$(date +%Y%m%d).sql. Automate this with a daily cron job and ship the dump to S3 or object storage using rclone.
Is Monica suitable for team use?
Monica supports multiple user accounts and vaults, making it usable by small teams. Each user manages their own contact network, and vaults allow selective sharing. For organizations needing shared contact databases with RBAC, evaluate Monica's vault sharing model carefully before rolling out to large teams.
How do I enable two-factor authentication?
Monica supports TOTP-based 2FA natively. Users can enable it from Settings > Security after logging in. No server-side configuration is needed — the TOTP secret is stored per user in the MySQL database and bound to their account.
What happens if I lose the APP_KEY?
The APP_KEY is used to encrypt sensitive fields in the database. Losing it means encrypted data (API tokens, certain notification settings) becomes unreadable. Store the APP_KEY in a secrets manager or password vault immediately after generation and never regenerate it for an existing deployment without a full data migration plan.
Can I run Monica behind Cloudflare proxy?
Yes, with caveats. Set Caddy's TLS to handle origin certificates and configure Cloudflare in Full (Strict) SSL mode. Do not set APP_URL to an HTTP address when Cloudflare is in proxy mode, as CSRF verification will fail. Also ensure Cloudflare's IP ranges are trusted in Caddy's config to pass real client IPs correctly.
Internal links
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Authentik with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Rallly with Docker Compose + Caddy + PostgreSQL + SMTP on Ubuntu
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.