Multi-tenant Docker Compose deployment for FreightOps. Supports multiple tenants with isolated configurations (database, webhook, dashboard) while sharing nginx reverse proxy, certbot, pgadmin, and watchtower.
- Shared services: nginx-proxy (reverse proxy only), certbot, pgadmin, watchtower
- Per-tenant services: freightops-api, freightops-daemon, freightops-dashboard (one container each per tenant)
┌─────────────────┐
│ nginx-proxy │
│ (port 80/443) │
└────────┬────────┘
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ covan │ │ tenant2 │ │ tenant3 │
│ dashboard │ │ dashboard │ │ dashboard │
│ api │ │ api │ │ api │
│ daemon │ │ daemon │ │ daemon │
└─────────────┘ └─────────────┘ └─────────────┘
# Create certbot directories (if not present)
mkdir -p config/certbot-conf config/certbot-www
# Generate nginx config from tenants
./scripts/generate-nginx.sh
# Start shared infrastructure
./scripts/manage-tenants.sh shared-start
# Start tenant(s)
./scripts/manage-tenants.sh start covanFor production at /home/docker:
export COMPOSE_DIR=/home/docker
cd /home/docker
./scripts/manage-tenants.sh shared-start
./scripts/manage-tenants.sh start covan| Command | Description |
|---|---|
./scripts/manage-tenants.sh shared-start |
Start nginx-proxy, certbot, pgadmin, watchtower |
./scripts/manage-tenants.sh shared-stop |
Stop shared infrastructure |
./scripts/manage-tenants.sh start [tenant-id] |
Start tenant(s). Omit id to start all |
./scripts/manage-tenants.sh stop [tenant-id] |
Stop tenant(s) |
./scripts/manage-tenants.sh up [tenant-id] |
Pull images and start tenant(s) |
./scripts/manage-tenants.sh logs <tenant-id> |
Follow tenant logs |
./scripts/manage-tenants.sh add <tenant-id> |
Add a new tenant |
./scripts/manage-tenants.sh create-db <tenant-id> |
Create PostgreSQL databases and users for a tenant |
./scripts/manage-tenants.sh migrate <tenant-id> |
Run EF migrations and Marten seed (uses freightops-migrations image) |
./scripts/manage-tenants.sh list |
List all tenants |
# 1. Scaffold tenant (creates tenants/<id>/.env)
./scripts/manage-tenants.sh add tenant2
# 2. Edit tenants/tenant2/.env with real credentials:
# - POSTGRES_PASSWORD (maintenance user - copy from covan for shared server)
# - DEV_DB_PASSWORD, AUTH_DB_PASSWORD (tenant DB users)
# - Webhook endpoint in OutboxNotifications section
# 3. Create databases and users on PostgreSQL
./scripts/manage-tenants.sh create-db tenant2
# 4. Run EF migrations and Marten seed (uses freightops-migrations image)
# Build/push the image from FreightOps repo first (see FreightOps/.github/workflows/docker-build-migrations.yml)
./scripts/manage-tenants.sh migrate tenant2
# 5. Regenerate nginx config (add-tenant.sh does this automatically)
./scripts/generate-nginx.sh
# 6. Add domain to SSL cert (run from infra root)
docker compose -f docker-compose.shared.yml --env-file .env.shared run --rm certbot \
certonly --webroot --webroot-path /var/www/certbot/ \
--non-interactive --agree-tos --expand \
-d covan.freightopsconnect.com -d tenant2.freightopsconnect.com
# 7. Reload nginx
docker compose -f docker-compose.shared.yml --env-file .env.shared exec nginx-proxy nginx -s reload
# 8. Start the new tenant
./scripts/manage-tenants.sh start tenant2Certificates are renewed via run-certbot.sh, which reads domains from tenant .env files.
Create /etc/systemd/system/certbot-renew.service:
[Unit]
Description=Renew LetsEncrypt Certificates via Docker Compose
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/home/docker/run-certbot.sh
WorkingDirectory=/home/docker
User=root
Environment=COMPOSE_DIR=/home/dockerCreate /etc/systemd/system/certbot-renew.timer:
[Unit]
Description=Run Certbot renewal twice daily (randomized)
[Timer]
OnCalendar=*-*-* 00,12:00:00
Persistent=true
RandomizedDelaySec=1h
AccuracySec=5m
OnBootSec=10m
[Install]
WantedBy=timers.targetEnable: sudo systemctl enable --now certbot-renew.timer
./run-certbot.shfreightops-infrastructure/
├── docker-compose.shared.yml # nginx-proxy, certbot, pgadmin, watchtower
├── docker-compose.tenant.yml # api, daemon, dashboard (per-tenant)
├── .env.shared # PGADMIN, DOCKER_REGISTRY
├── tenants/
│ ├── covan/
│ │ └── .env # Tenant-specific config
│ ├── tenant2/
│ │ └── .env
│ └── ...
├── config/
│ ├── nginx/
│ │ ├── default.conf # Generated - HTTP, ACME challenge
│ │ └── ssl.conf # Generated - HTTPS, per-tenant routing
│ ├── certbot-conf/ # Let's Encrypt certificates
│ └── certbot-www/ # ACME challenge webroot
├── scripts/
│ ├── manage-tenants.sh # Main orchestration CLI
│ ├── add-tenant.sh # New tenant scaffolding
│ ├── create-tenant-db.sh # Create PostgreSQL DBs and users per tenant
│ └── generate-nginx.sh # Build nginx config from tenants
└── run-certbot.sh # Certificate renewal
If migrating from the original single-tenant setup:
- Stop the old stack:
docker compose down - Create
tenants/covan/.env(already done - migrated from root.env) - Run
./scripts/generate-nginx.sh - Start shared:
./scripts/manage-tenants.sh shared-start - Start covan:
./scripts/manage-tenants.sh start covan - Verify https://covan.freightopsconnect.com
Production runs migrations from the manicapps904/freightops-migrations image (no FreightOps source required). Build and push from the FreightOps repo:
# From FreightOps repo root
docker build -f Dockerfile.migrations -t manicapps904/freightops-migrations:latest .
docker push manicapps904/freightops-migrations:latestOr trigger the GitHub workflow docker-build-migrations.yml (workflow_dispatch).
- Network: Tenant containers join the
freightops-proxynetwork created by the shared stack. Start shared infrastructure before tenants. - Paths: For production at
/home/docker, setCOMPOSE_DIR=/home/dockeror run from that directory. - Dashboard: Each tenant gets its own dashboard container. The
freightops-nginximage usesentrypoint.shto injectVITE_API_HOSTandVITE_BASE_URLat runtime.