EspoCRM is an open-source customer relationship manager. This guide deploys MariaDB, the web app, a scheduled-job daemon and a WebSocket server with Docker Compose behind Caddy HTTPS.
Prerequisites
- RamNode KVM VPS running Ubuntu 24.04 LTS (26.04 LTS also works), 2 GB RAM, 1–2 vCPUs and 40 GB disk for a small team.
- Domain such as
crm.example.comwith A record (and AAAA if using IPv6) pointing to the VPS. - Initial root SSH access and SMTP relay credentials for outbound email.
Replace all example domains and passwords. See DNS and Docker Compose.
Prepare the server
Run as root. Verify the deploy SSH login in a second terminal before disabling root login:
apt update && apt -y full-upgrade
apt install -y rsync ufw unattended-upgrades
timedatectl set-timezone UTC
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployOnce verified, run as root:
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'CONFIG'
PermitRootLogin no
PasswordAuthentication no
CONFIG
sshd -t && systemctl restart ssh
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
dpkg-reconfigure -plow unattended-upgradesAllow a custom SSH port before enabling UFW. Docker can bypass UFW, so all app ports below bind only to localhost. See cloud firewall.
Install Docker Engine and Compose
As deploy:
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker deploy
sudo tee /etc/docker/daemon.json > /dev/null <<'JSON'
{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}
JSON
sudo systemctl restart dockerLog out and back in; check docker run --rm hello-world and docker compose version.
Deploy EspoCRM with Docker Compose
sudo mkdir -p /opt/espocrm && sudo chown deploy:deploy /opt/espocrm
cd /opt/espocrm
openssl rand -hex 24 # run three times for DB root, DB user and admin passwordsCreate /opt/espocrm/.env with distinct generated values:
DB_ROOT_PASSWORD=change-me-root
DB_PASSWORD=change-me-db
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-admin
DOMAIN=crm.example.comchmod 600 /opt/espocrm/.envCreate /opt/espocrm/compose.yml:
services:
espocrm-db:
image: mariadb:11.4
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: espocrm
MARIADB_USER: espocrm
MARIADB_PASSWORD: ${DB_PASSWORD}
volumes:
- espocrm-db:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 20s
timeout: 5s
retries: 3
espocrm:
image: espocrm/espocrm:latest
restart: unless-stopped
environment:
ESPOCRM_DATABASE_PLATFORM: Mysql
ESPOCRM_DATABASE_HOST: espocrm-db
ESPOCRM_DATABASE_USER: espocrm
ESPOCRM_DATABASE_PASSWORD: ${DB_PASSWORD}
ESPOCRM_ADMIN_USERNAME: ${ADMIN_USERNAME}
ESPOCRM_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
ESPOCRM_SITE_URL: https://${DOMAIN}
volumes:
- espocrm:/var/www/html
depends_on:
espocrm-db:
condition: service_healthy
ports:
- "127.0.0.1:8080:80"
espocrm-daemon:
image: espocrm/espocrm:latest
restart: unless-stopped
entrypoint: docker-daemon.sh
volumes:
- espocrm:/var/www/html
depends_on:
- espocrm
espocrm-websocket:
image: espocrm/espocrm:latest
restart: unless-stopped
entrypoint: docker-websocket.sh
environment:
ESPOCRM_CONFIG_USE_WEBSOCKET: "true"
ESPOCRM_CONFIG_WEBSOCKET_URL: wss://${DOMAIN}/ws
ESPOCRM_CONFIG_WEBSOCKET_ZERO_M_Q_SUBSCRIBER_DSN: tcp://*:7777
ESPOCRM_CONFIG_WEBSOCKET_ZERO_M_Q_SUBMISSION_DSN: tcp://espocrm-websocket:7777
volumes:
- espocrm:/var/www/html
depends_on:
- espocrm
ports:
- "127.0.0.1:8081:8080"
volumes:
espocrm-db:
espocrm:cd /opt/espocrm
docker compose config --quiet
docker compose up -d
docker compose logs -f espocrmFirst installation may take a couple of minutes. The admin credentials and site URL environment settings apply on first install only; change them later in the Administration panel. Pin the app image to a tested release tag once working.
Configure Caddy and HTTPS
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddyPut this in /etc/caddy/Caddyfile:
crm.example.com {
encode zstd gzip
handle /ws* {
reverse_proxy 127.0.0.1:8081
}
handle {
reverse_proxy 127.0.0.1:8080
}
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy obtains a certificate when DNS resolves and ports 80/443 are reachable. See its logs with journalctl -u caddy -f.
First login and configuration
Open https://crm.example.com and sign in using the admin credentials from .env. In Administration, configure:
- Outbound Emails: authenticated SMTP relay, port 587, TLS and a system From address. Send a test mail. See outgoing SMTP policy.
- Settings: confirm the HTTPS Site URL and choose time zone, date format and currency.
- Scheduled Jobs: confirm recent runs; the daemon replaces a host crontab.
- Authentication: enable two-factor authentication, especially for admins.
- Users: create individual user accounts and avoid daily use of the built-in admin.
If live notifications do not work, check the browser's connection to wss://crm.example.com/ws.
Backups and restore
Back up both MariaDB and the espocrm volume, which contains uploads, data/config.php and custom code. Also keep .env and compose.yml. Create /opt/espocrm/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/espocrm
TS=$(date +%F-%H%M)
DEST=/var/backups/espocrm
mkdir -p "$DEST"
docker compose exec -T espocrm-db sh -c \
'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction espocrm' \
| gzip > "$DEST/db-$TS.sql.gz"
docker run --rm -v espocrm_espocrm:/data:ro -v "$DEST":/backup alpine \
tar czf "/backup/files-$TS.tar.gz" -C /data .
cp .env "$DEST/env-$TS"
cp compose.yml "$DEST/compose-$TS.yml"
chmod 600 "$DEST"/*
find "$DEST" -type f -mtime +14 -deleteThe volume name assumes the project directory is /opt/espocrm; confirm with docker volume ls and replace if needed.
sudo chmod 700 /opt/espocrm/backup.sh
sudo /opt/espocrm/backup.sh
sudo crontab -e
# Add: 15 3 * * * /opt/espocrm/backup.shCopy both backups off the VPS. See backups. To restore on a fresh server with saved .env and compose.yml, start the stack once so volumes exist; stop all app containers while restoring files:
cd /opt/espocrm
docker compose up -d
docker compose stop espocrm espocrm-daemon espocrm-websocket
docker run --rm -v espocrm_espocrm:/data -v /var/backups/espocrm:/backup alpine \
sh -c 'find /data -mindepth 1 -maxdepth 1 -exec rm -rf {} + && tar xzf /backup/files-YYYY-MM-DD-HHMM.tar.gz -C /data'
gunzip -c /var/backups/espocrm/db-YYYY-MM-DD-HHMM.sql.gz | \
docker compose exec -T espocrm-db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" espocrm'
docker compose up -dConfirm the database and file archives have matching timestamps.
Updating
Back up first. Pull the updated image and watch logs:
cd /opt/espocrm
sudo ./backup.sh
docker compose pull
docker compose up -d
docker compose logs -f espocrmDo not use the in-app upgrader with Docker: a container restart could overwrite its changes. Upgrade MariaDB major versions deliberately after a backup.
Troubleshooting
| Symptom | Check |
|---|---|
| Caddy certificate fails | Check dig +short crm.example.com, port 80 and journalctl -u caddy -f. |
| 502 response | Check docker compose ps and docker compose logs espocrm; installation may still be running. |
| Database connection fails | Inspect docker compose logs espocrm-db and verify the original database password in .env; changing it after first boot does not change the stored DB password. |
| Live notifications fail | Confirm ESPOCRM_CONFIG_WEBSOCKET_URL uses wss://crm.example.com/ws and Caddy proxies /ws* to port 8081. |
| Cron not configured | Restart espocrm-daemon and inspect its logs. |
| Emails not sent | Check authenticated SMTP settings and send a test from Outbound Emails. |
| Links point to old domain | Change Site URL under Administration > Settings. |
