Self-Hosted Docker Installation Guide
Deploy ManageLM on your own infrastructure with Docker.
Overview
ManageLM is published as managelm/portal on Docker Hub with two variants:
| Image | Includes | Best for |
|---|---|---|
managelm/portal:latest | Portal only | Production — bring your own PostgreSQL & Redis |
managelm/portal:allinone | Portal + PostgreSQL + Redis | Quick evaluation, testing |
ManageLM Only (Recommended)
The ManageLM Only image (managelm/portal:latest) contains only the ManageLM application. You provide your own PostgreSQL and Redis instances — either as separate Docker containers (included in the default compose file) or as external/managed services. This gives you full control over database configuration, backups, scaling, and upgrades independently of the portal.
All-in-One (with DB)
The All-in-One (with DB) image (managelm/portal:allinone) bundles the portal, PostgreSQL, and Redis into a single container. It is the fastest way to try ManageLM — one container, no external dependencies — but it is not recommended for production because database and cache lifecycle are tied to the container, making independent scaling, backups, and upgrades harder.
Requirements
- Docker Engine 20+ or Docker Desktop
- 2 GB RAM minimum
- 1 GB RAM minimum (+ PostgreSQL 16+ and Redis 7+)
- A domain name with trusted TLS for production. Agents verify the certificate chain against the host's system trust store and reject self-signed or untrusted CAs — this applies even on private networks. See Reverse Proxy for the options.
SERVER_URL must be reachable from the public internet (public DNS + open 443) for it to work. If your ManageLM is private / internal-only (no public DNS or firewalled), the Custom Connector flow will fail at the token-exchange step with an opaque "Authorization failed" error. For those deployments, use the Claude Free / claude_desktop_config.json or Claude Code path instead: mcp-remote runs locally inside Claude Desktop / Claude Code, so private hostnames resolve and no MCP traffic leaves your network. Both options are documented in Connect Claude.
Quick Start
Single container — no external dependencies. Pick whichever style you prefer:
Docker Compose
-
Download the compose file
mkdir managelm && cd managelm curl -O https://app.managelm.com/doc/docker-compose.allinone.yml mv docker-compose.allinone.yml docker-compose.yml -
Edit
docker-compose.yml— set:SERVER_URL(e.g.http://192.168.1.10:3000)SMTP_FROM— sender email
-
Start
docker compose up -d
docker run
Single command, no compose file needed:
docker run -d \
--name managelm \
-p 3000:3000 \
-e SERVER_URL=http://localhost:3000 \
-e SMTP_FROM=noreply@example.com \
-v managelm_pgdata:/data/postgres \
-v managelm_redisdata:/data/redis \
--restart unless-stopped \
managelm/portal:allinone
Then open your SERVER_URL and register — the first user becomes the account owner.
-e DATABASE_URL=postgresql://user:pass@host:5432/managelm (or the equivalent environment entry in compose) — this overrides the embedded DB. Same pattern for Redis with REDIS_URL.
Portal + PostgreSQL + Redis as separate containers. Pick whichever style you prefer:
Docker Compose
-
Download the compose file
mkdir managelm && cd managelm curl -O https://app.managelm.com/doc/docker-compose.yml -
Edit
docker-compose.yml— set:SERVER_URL(e.g.http://192.168.1.10:3000)SMTP_FROM— sender email- Database password — replace
change-me-strong-passwordin both places it appears (DATABASE_URL+postgresservice'sPOSTGRES_PASSWORD); they must match.
-
Start
docker compose up -d
docker run
Start PostgreSQL and Redis first, then the portal:
# PostgreSQL
docker run -d --name managelm-db \
-e POSTGRES_DB=managelm \
-e POSTGRES_USER=managelm \
-e POSTGRES_PASSWORD=your-db-password \
-v managelm_pgdata:/var/lib/postgresql/data \
--restart unless-stopped \
postgres:16-alpine
# Redis
docker run -d --name managelm-redis \
-v managelm_redisdata:/data \
--restart unless-stopped \
redis:7-alpine redis-server --appendonly yes
# Portal
docker run -d --name managelm \
-p 3000:3000 \
-e SERVER_URL=http://localhost:3000 \
-e SMTP_FROM=noreply@example.com \
-e DATABASE_URL=postgresql://managelm:your-db-password@managelm-db:5432/managelm \
-e REDIS_URL=redis://managelm-redis:6379 \
--link managelm-db --link managelm-redis \
--restart unless-stopped \
managelm/portal:latest
Then open your SERVER_URL and register — the first user becomes the account owner.
External Database Setup
When using your own PostgreSQL or Redis instead of the containers from the compose file, follow these steps to prepare them before starting the portal.
PostgreSQL
ManageLM requires PostgreSQL 15+ (16 recommended). The portal runs migrations automatically on startup and needs full control over its schema.
-
Create the database and user
# Connect as a PostgreSQL superuser (e.g. postgres) sudo -u postgres psql -- Create the ManageLM user CREATE USER managelm WITH PASSWORD 'your-strong-password'; -- Create the database owned by that user CREATE DATABASE managelm OWNER managelm; -- Connect to the new database \c managelm -- Grant full schema permissions (required for migrations) GRANT ALL ON SCHEMA public TO managelm; -- If on PostgreSQL 15+, also grant CREATE (changed default in PG15) ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO managelm; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO managelm; \q -
Allow remote connections (if PostgreSQL is on a different host)
# In postgresql.conf — listen on all interfaces listen_addresses = '*' # In pg_hba.conf — allow the portal host (replace with your subnet) host managelm managelm 10.0.0.0/8 scram-sha-256Restart PostgreSQL after editing:
systemctl restart postgresql -
Set
DATABASE_URLin yourdocker-compose.ymlDATABASE_URL=postgresql://managelm:your-strong-password@db-host:5432/managelm -
Test the connection (from the portal host)
psql "postgresql://managelm:your-strong-password@db-host:5432/managelm" -c "SELECT 1;"
DB_SSL=require in your docker-compose.yml. To verify the server certificate, use DB_SSL=verify-ca with DB_SSL_CA=/path/to/ca.pem (mount the CA file into the container).
CREATE privilege on the public schema was revoked for non-owners. If the portal fails to start with "permission denied for schema public", run: GRANT ALL ON SCHEMA public TO managelm;
Redis
ManageLM requires Redis 7+ (or Valkey). It is used for real-time agent communication, session state, and pub/sub — it must be available at all times.
-
Enable persistence (recommended)
# In redis.conf appendonly yes appendfsync everysec -
Set
REDIS_URLin yourdocker-compose.yml# Without authentication REDIS_URL=redis://redis-host:6379 # With authentication (Redis 6+ ACL) REDIS_URL=redis://username:password@redis-host:6379 -
Test the connection
redis-cli -h redis-host ping # Expected: PONG
REDIS_TLS=on in your docker-compose.yml for encrypted connections. Use a rediss:// URL scheme.
Remove compose services
When using external databases, remove the postgres and redis services, their volumes, and the depends_on block from your docker-compose.yml. A minimal compose file looks like:
services:
portal:
image: managelm/portal:latest
ports:
- "3000:3000"
environment:
- SERVER_URL=http://localhost:3000
- SMTP_FROM=noreply@example.com
- DATABASE_URL=postgresql://user:pass@db-host:5432/managelm
- REDIS_URL=redis://redis-host:6379
restart: unless-stopped
First Steps After Install
Once the portal is running:
- Set up a reverse proxy with TLS (nginx or Apache) for production.
- Register your account at your
SERVER_URL. - Configure the LLM — Local (Ollama), Cloud, or Proxied access mode.
- Import skills from the built-in catalog.
- Install an agent on your first server.
- Connect Claude via MCP.
SERVER_URL before registering. Agents derive their WebSocket URL from it, so it must be the exact address reachable from agent machines. See the SERVER_URL reference for proxy / no-proxy guidance.
Reverse Proxy
Place a reverse proxy in front of the portal for TLS. WebSocket support is required for agent connections.
- Public domain, private IPs: use Let's Encrypt with the DNS-01 challenge — works for any domain you control via DNS, regardless of whether the portal is reachable from the public internet. This is the simplest path for most internal deployments.
- Internal (private) CA: distribute the CA root certificate to every agent host's system trust store (
/etc/pki/ca-trust/source/anchors/on RHEL family,/usr/local/share/ca-certificates/on Debian/Ubuntu) before enrolling the agent. - Plain HTTP / no TLS: agents will connect over
ws://, but credentials and task payloads travel unencrypted. Acceptable only for local testing on a trusted LAN, never for production.
nginx
server {
listen 443 ssl http2;
server_name managelm.example.com;
ssl_certificate /etc/ssl/certs/managelm.pem;
ssl_certificate_key /etc/ssl/private/managelm.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
Apache
<VirtualHost *:443>
ServerName managelm.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/managelm.pem
SSLCertificateKeyFile /etc/ssl/private/managelm.key
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://127.0.0.1:3000/$1 [P,L]
ProxyTimeout 86400
</VirtualHost>
Traefik
Traefik discovers the portal from Docker labels — no separate config file. WebSocket upgrades are handled transparently. Add these labels to the portal service in your docker-compose.yml:
labels:
- "traefik.enable=true"
- "traefik.http.routers.managelm.rule=Host(`managelm.example.com`)"
- "traefik.http.routers.managelm.entrypoints=websecure"
- "traefik.http.routers.managelm.tls.certresolver=letsencrypt"
- "traefik.http.services.managelm.loadbalancer.server.port=3000"
Replace letsencrypt with the cert resolver name on your Traefik instance, and remove the ports: mapping from the portal service — with Traefik, the portal is only reached through the proxy. If you don't have Traefik running yet, see the Traefik quick-start for setting up an instance with cert resolvers.
Upgrade and Connection headers for WebSocket (Traefik does this automatically; nginx and Apache need the config above).
SMTP / Email
The portal sends emails for account verification, password resets, and team invitations. When SMTP_HOST is not set, email sending is disabled entirely — no connection attempts are made.
To enable emails, configure an SMTP relay (Brevo, Mailgun, SendGrid, etc.):
SMTP_HOST=smtp.brevo.com
SMTP_PORT=587
SMTP_SECURE=starttls
SMTP_USER=your-username
SMTP_PASS=your-password
Environment Variables
Required
| Variable | Description |
|---|---|
SERVER_URL |
The public URL that browsers and agents use to reach the portal. This must be the externally reachable address — agents derive their WebSocket connection from it.
Behind a reverse proxy (recommended for production): use the proxy's public URL with https://. Do not include the container port — the proxy listens on 443.Example: https://managelm.example.com
Without a reverse proxy (testing / LAN): use the server IP or hostname with http:// and include the container port.Example: http://192.168.1.10:3000
|
SMTP_FROM | Sender email address. Validated at startup but only effective when SMTP_HOST is also set; otherwise no emails are sent. |
POSTGRES_PASSWORD | PostgreSQL password (multi-container only — embedded DB in the all-in-one uses fixed localhost-only credentials) |
Strongly recommended
The all-in-one image auto-generates an ENCRYPTION_KEY on first run and persists it to /data/encryption.key. You only need to set it explicitly if you want to manage the key yourself (e.g. to restore a backup on a new host).
Set this before adding cloud connectors, configuring an LLM API key, enrolling agents, or generating a PKI CA — the portal starts without it (warning only), but any feature that encrypts data at rest will fail until it's configured. Rotating the key invalidates existing encrypted rows, so set it once and keep it.
| Variable | Default | Description |
|---|---|---|
ENCRYPTION_KEY | — | AES-256 master key for secrets at rest: cloud connector credentials, LLM API keys, agent signing keys, PKI CA private keys. Must be a 64-character hex string. Generate with: openssl rand -hex 32 |
Database & Redis
The all-in-one image auto-configures these to the embedded services. Override only if you want to point at an external PostgreSQL or Redis.
The shipped compose file sets these to the bundled postgres and redis services. Override to use external instances.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | set by compose | PostgreSQL connection string. Override for external DB. |
REDIS_URL | set by compose | Redis connection string. Override for external Redis. |
DB_SSL | none | SSL mode (none, require, verify, verify-ca) |
DB_SSL_CA | — | Path to CA certificate file (used with DB_SSL=verify-ca) |
DB_POOL_MAX | 20 | Maximum PostgreSQL connections per portal worker |
REDIS_TLS | auto | Redis TLS (auto = on for rediss:///valkeys://, on = force, off = skip) |
REDIS_DB | 0 | Logical database number (0–15). Useful when sharing a Redis instance. |
SMTP & DKIM
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP server hostname. When empty, email is disabled (no connection attempts). |
SMTP_PORT | 25 | SMTP port (587 for STARTTLS, 465 for implicit TLS) |
SMTP_SECURE | none | none, starttls, tls |
SMTP_USER | — | SMTP authentication username (for external relays) |
SMTP_PASS | — | SMTP authentication password |
DKIM_DOMAIN | — | DKIM signing domain (set to enable DKIM signing) |
DKIM_SELECTOR | default | DKIM selector published in DNS |
DKIM_PRIVATE_KEY | — | Inline DKIM private key in PEM format. Prefer DKIM_PRIVATE_KEY_PATH — env vars expose keys to anything that can read the process environment. |
DKIM_PRIVATE_KEY_PATH | — | Path to DKIM private key file inside the container (read at startup, never logged) |
Optional
| Variable | Default | Description |
|---|---|---|
SERVER_PORT | 3000 | Portal listen port |
CLUSTER_WORKERS | 2 | Node.js cluster workers (set to 1 to disable) |
LOG_LEVEL | info | trace, debug, info, warn, error |
Advanced
| Variable | Default | Description |
|---|---|---|
DEFAULT_TIMEZONE | UTC | Default timezone for the portal UI and notifications |
ACCESS_TOKEN_TTL | 86400 | Access token lifetime in seconds (default 24h). Opaque tokens stored in Redis. |
REFRESH_TOKEN_TTL | 2592000 | Refresh token lifetime in seconds (default 30d) |
TASK_TIMEOUT_SECONDS | 300 | Max synchronous task wait (5 min default) |
TASK_LOG_RETENTION_DAYS | 30 | Task log retention before automatic cleanup |
AUDIT_LOG_RETENTION_DAYS | 90 | Audit log retention before automatic cleanup |
FILE_TRANSFER_MAX_BYTES | 26214400 | Max file transfer size (default 25 MB) |
Updating
docker compose pull
docker compose up -d
Migrations run automatically on startup. Back up first for major updates.
Backup & Restore
# Backup
docker compose stop
docker run --rm \
-v managelm_pgdata:/data/postgres:ro \
-v managelm_redisdata:/data/redis:ro \
-v $(pwd):/backup \
alpine tar czf /backup/managelm-backup-$(date +%F).tar.gz -C / data
docker compose start
# Restore
docker compose down
docker run --rm \
-v managelm_pgdata:/data/postgres \
-v managelm_redisdata:/data/redis \
-v $(pwd):/backup \
alpine tar xzf /backup/managelm-backup-YYYY-MM-DD.tar.gz -C /
docker compose up -d
PostgreSQL holds the durable data and is the only thing you strictly need to back up. Redis is rebuilt from PostgreSQL on portal startup; back it up only if you want to preserve in-flight sessions across restores.
# Backup PostgreSQL (run regularly)
docker compose exec postgres pg_dump -U managelm managelm > backup-$(date +%F).sql
# Restore PostgreSQL
docker compose exec -T postgres psql -U managelm managelm < backup-YYYY-MM-DD.sql
# Optional — Redis snapshot (in-flight session state)
docker compose exec redis redis-cli BGSAVE
docker cp $(docker compose ps -q redis):/data/dump.rdb ./redis-$(date +%F).rdb
Monitoring
# Health check
curl -s http://localhost:3000/health
# Logs
docker compose logs -f portal
# Last 100 lines
docker compose logs --tail 100 portal
Troubleshooting
Portal won't start
- Check the logs first:
docker compose logs portal - Check for port conflicts on 3000 (or whatever
SERVER_PORTyou set). - Verify
DATABASE_URLandREDIS_URLare reachable from the portal container. - Run
docker compose ps—postgresandredismust show "healthy" before the portal will start.
Agents can't connect
- Verify
SERVER_URLis reachable from the agent server. - Check reverse proxy forwards WebSocket headers. See Reverse Proxy.
- Test:
curl -I https://your-hostname/health
Database permission errors
- "permission denied for schema public" — Connect as a superuser and run:
GRANT ALL ON SCHEMA public TO managelm; - "permission denied for relation …" — The user needs ownership or full grants. Re-run:
ALTER DATABASE managelm OWNER TO managelm; - "FATAL: password authentication failed" — Check
DATABASE_URLcredentials match the PostgreSQL user. Test withpsqlfirst. - "could not connect to server: Connection refused" — Verify
listen_addressesinpostgresql.confand checkpg_hba.confallows the portal host. - Managed PostgreSQL (AWS RDS, GCP Cloud SQL, etc.) — The managed user is usually not a superuser. Grant schema access explicitly and ensure the security group / firewall allows the portal.
Email not sending
- Without
SMTP_HOST, email sending is disabled. Set it to your SMTP server to enable emails. - Check
SMTP_FROMin yourdocker-compose.yml— it must be set. - For an SMTP relay, verify
SMTP_HOST,SMTP_PORT, and credentials. - Check logs:
docker compose logs | grep -i mail