Self-hosting
Run EkhaneMark on your own server with Docker Compose.
EkhaneMark runs on one small server (2 vCPU and 4 GB RAM is plenty for an agency). It uses about 1 GB of memory: the dashboard, the API server, PostgreSQL and Caddy for HTTPS. Self-hosted, there are no plan limits.
Install
- Point a domain (for example
ekhanemark.youragency.com) at the server with a DNS A record. - Install Docker and clone the repository.
- In
deploy/, copy.env.exampleto.envand fill it in (below). - Run
docker compose up -d --build. Caddy gets the HTTPS certificate; database migrations run before the app starts. - Open the domain, create your account and workspace.
Update with git pull && docker compose up -d --build.
Server already running nginx
If other sites on the server already use ports 80 and 443, leave COMPOSE_PROFILES empty in .env so Caddy doesn't start. EkhaneMark then listens on 127.0.0.1 only: the dashboard on 3100 (APP_PORT) and the API server on 3102(API_PORT). Add a site to your existing proxy:
server {
server_name ekhanemark.youragency.com;
client_max_body_size 50m; # screenshots, voice notes, images, files
# The API server: what the widget and coding agents' uploads talk to.
location ~ ^/api/(w/|uploads/|live$) {
proxy_pass http://127.0.0.1:3102;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Check it with nginx -t, reload, then get a certificate with certbot --nginx -d ekhanemark.youragency.com. Other sites keep running throughout. Without that block the dashboard passes those requests on to the API server itself, a little slower.
Settings
| Variable | What it does |
|---|---|
DOMAIN, BETTER_AUTH_URL | Your domain, and the same with https://. |
BETTER_AUTH_SECRET | A long random string. Signs sessions, links and file URLs. Changing it signs everyone out and breaks existing invite links. |
ALLOWED_EMAIL_DOMAINS | Only these email domains can sign up (e.g. your agency's). |
SMTP_URL, MAIL_FROM | Email: smtps://user:[email protected]:465. Without it, emails are written to the server log. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Optional “Continue with Google”. Redirect URI: your domain + /api/auth/callback/google. |
STORAGE_DRIVER | local (files in a Docker volume) or s3 (Cloudflare R2, Garage, any S3 API) with the S3_* variables. |
BILLING_ENABLED | Leave false for internal use: no limits, no billing pages. true runs it as a service with plans. |
SUPERADMIN_EMAILS | Who can open the admin panel to turn on paid plans after a payment. The panel only exists in a container started with ADMIN_MODE=1; the compose file runs one as admin on port 3101, and the proxy exposes it on /admin to an allowlist of addresses. |
CRON_SECRET | Protects /api/cron/cleanup (feedback past the Free plan's history, old meeting audio) and /api/cron/transcribe (meeting clips still waiting). |
Meetings
Meetings are off until MEETINGS_ENABLED=true; until then the dashboard shows them as coming soon. They need a LiveKit server, which the compose file runs when COMPOSE_PROFILES includes meetings. Set LIVEKIT_URL (wss:// your domain /livekit), LIVEKIT_API_KEY and a long random LIVEKIT_API_SECRET. Call audio and video go straight to the server on TCP 7881 and UDP 7882, so let those in. Caddy already proxies /livekit/; with nginx, add:
location /livekit/ {
proxy_pass http://127.0.0.1:7880/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 1h;
}Transcripts need TRANSCRIBE_API_KEY: an OpenAI key, or any OpenAI-compatible speech-to-text service through TRANSCRIBE_URL and TRANSCRIBE_MODEL. Without it, meetings work and their clips wait until a key is set. The cron service retries waiting clips every minute.
Backups
deploy/backup.sh dumps the database and archives uploaded files into deploy/backups, keeping 14 days. Run it from cron every night and copy that folder off the server.
Health check
/api/health returns 200 when the app can reach the database. Point your uptime monitor at it.