First of all, what is vTiger CRM?

vTiger CRM Open Source is a free, self-hosted customer relationship management platform. It bundles contact management, sales automation, support tickets, inventory and email together in a single web application that runs on PHP and MySQL.

If your company wants a CRM without paying per-user subscriptions or handing your customer data to a SaaS provider, vTiger is one of the most complete open source options. Running it with Docker makes deployment, upgrades and backups much simpler than the classic manual LAMP installation.

⚠️ The official image's credentials are misleading

The official vtigercrm/vtigercrm-8.3.0 image documents that mysql_user and mysql_pass create the database user for your installation. That is not true in practice. I inspected the image and this is what actually happens:

  • The image runs as root and starts a bundled MySQL with root having no password.
  • If you connect to the container after booting up and go to the mysql database and inspect the user table you will see that the user you defined for the service is not created, so if you try to use that user in the startup process via the web panel on the first boot, it won't work, which is frustrating. Untill you use the root user with no password and see that your Internet exposed CRM application is being conected to the database by root with no password at all. That sounds awful to me TBH.

This guide ships a custom image that fixes that behaviour with a dedicated, least-privileged application user. For now we will use this the official image as base image, but something tells me that to create a fully well designed image and deployment would require to do it all from scratch.

What are we using in this guide?

ComponentValue
Base imagevtigercrm/vtigercrm-8.3.0:latest
Our imageCustom build that overrides the bootstrap script
Web serverApache + PHP (bundled in the base image)
DatabaseMySQL (bundled in the base image)
Application uservtiger with full access ONLY on the vtiger database
OrchestrationDocker + Docker Compose

What the custom image does

Instead of creating a *.* superuser, our bootstrap script:

  1. Creates the application database (default vtiger).
  2. Creates the application user (default vtiger) with the password from VTIGER_DB_PASS, bound to loopback only (localhost and 127.0.0.1).
  3. Grants that user ALL PRIVILEGES ON vtiger.* — full access but exclusively on its own database (verified: it cannot read mysql.user or any other schema).
  4. Optionally sets a password for MySQL root via MYSQL_ROOT_PASSWORD (recommended; by default root stays passwordless, which the base image expects).

Because the application and the database live in the same container, the app user does not need (and must not have) access from any other host. Any external agent that has to reach the database gets its own credentials, created manually with the least privileges required (see External database access).

All credentials are configurable with environment variables, so nothing secret lives in the image or the compose file.

Considerations

  • Docker Engine and Docker Compose (v2) installed on the host.
  • A VPS with at least 2 GB of RAM and 2 vCPUs for a small team. The image is not tiny.
  • The container listens on port 80; we map it to a host port.
  • A domain pointing to your server if you want to reach it over the internet (plus HTTPS, recommended).

Templates

The complete template (Dockerfile, bootstrap script, compose file and .env.example) is published in my GitHub profile:

(placeholder — replace <your-username> with the actual repository when published)

  • The same files are shown below so you can follow along.

Step 1: Prepare the environment file

Create a .env file next to your docker-compose.yaml:

# Host port where vTiger will be reachable.
host_port=8080

# Application database credentials (used by the vTiger installer).
VTIGER_DB_NAME=vtiger
VTIGER_DB_USER=vtiger
VTIGER_DB_PASS=replace-with-a-long-random-password

# Optional: set a password for MySQL root (empty = root stays passwordless).
MYSQL_ROOT_PASSWORD=env

VTIGER_DB_PASS is required: the bootstrap script refuses to start without it. Use a long random password.

Step 2: The custom Dockerfile

FROM vtigercrm/vtigercrm-8.3.0:latest

# The official image starts with `./run.sh ${mysql_user} ${mysql_pass}`, which
# on first boot calls `/create_mysql_users.sh`. That script creates a MySQL
# superuser on `*.*` (WITH GRANT OPTION) and leaves `root` passwordless.
# We replace it with our own script that creates a dedicated `vtiger` user with
# full but exclusive access to the `vtiger` database only.

ENV VTIGER_DB_NAME=vtiger \
    VTIGER_DB_USER=vtiger \
    VTIGER_DB_PASS=change-me

COPY create_mysql_users.sh /create_mysql_users.sh
RUN chmod +x /create_mysql_users.shdockerfile

The base image's CMD is ./run.sh ${mysql_user} ${mysql_pass}, and run.sh calls /create_mysql_users.sh on first boot. By overwriting that single script we don't touch Apache, PHP, Composer or service startup.

Step 3: The bootstrap script

Save this as create_mysql_users.sh in the same directory:

#!/bin/bash
#
# Replaces the official vtigercrm `create_mysql_users.sh`.
#
# On first boot (empty MySQL volume) this script:
#   1. Creates the application database (default: `vtiger`).
#   2. Creates the application user (default: `vtiger`) with the password from
#      $VTIGER_DB_PASS, bound to loopback only (`localhost` and `127.0.0.1`).
#   3. Grants the user FULL access ONLY on the application database, never on
#      `*.*`.
#   4. Optionally sets a password for MySQL `root` from $MYSQL_ROOT_PASSWORD.
#
# The application and the database live in the same container, so the app user
# does not need (and must not have) access from any other host.

set -e

VTIGER_DB_NAME="${VTIGER_DB_NAME:-vtiger}"
VTIGER_DB_USER="${VTIGER_DB_USER:-vtiger}"
VTIGER_DB_PASS="${VTIGER_DB_PASS:-}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"

# The app connects to MySQL through the Unix socket (`localhost`) and, in some
# setups, over TCP loopback (`127.0.0.1`). Both are covered here.
APP_HOSTS=("localhost" "127.0.0.1")

if [ -z "$VTIGER_DB_PASS" ]; then
    echo "=> ERROR: VTIGER_DB_PASS is required to create the '${VTIGER_DB_USER}' user."
    exit 1
fi

echo "=> Starting MySQL to prepare the database..."
/usr/bin/mysqld_safe > /dev/null 2>&1 &

RET=1
while [ "$RET" -ne 0 ]; do
    echo "=> Waiting for MySQL to start..."
    sleep 5
    mysql -uroot -e "status" > /dev/null 2>&1
    RET=$?
done

echo "=> Creating database '${VTIGER_DB_NAME}'"
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS \`${VTIGER_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

for host in "${APP_HOSTS[@]}"; do
    echo "=> Creating user '${VTIGER_DB_USER}'@'${host}'"
    mysql -uroot -e "CREATE USER IF NOT EXISTS '${VTIGER_DB_USER}'@'${host}' IDENTIFIED BY '${VTIGER_DB_PASS}';"
    mysql -uroot -e "ALTER USER '${VTIGER_DB_USER}'@'${host}' IDENTIFIED BY '${VTIGER_DB_PASS}';"

    echo "=> Granting FULL access on '${VTIGER_DB_NAME}.*' ONLY to '${VTIGER_DB_USER}'@'${host}'"
    mysql -uroot -e "GRANT ALL PRIVILEGES ON \`${VTIGER_DB_NAME}\`.* TO '${VTIGER_DB_USER}'@'${host}';"
done

mysql -uroot -e "FLUSH PRIVILEGES;"

if [ -n "$MYSQL_ROOT_PASSWORD" ]; then
    echo "=> Setting MySQL root password"
    mysql -uroot -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}';"
    mysqladmin -uroot -p"${MYSQL_ROOT_PASSWORD}" shutdown
else
    mysqladmin -uroot shutdown
fi

echo "=> Done. Database '${VTIGER_DB_NAME}' and user '${VTIGER_DB_USER}' ready."bash

The GRANT ALL PRIVILEGES ON \vtiger\.* (and nothing else), combined with the host being restricted to localhost/127.0.0.1, is what guarantees the user has full control of its own database but cannot touch anything else nor be used from outside the container.

Step 4: The Docker Compose file

services:
  vtiger:
    build: .
    image: vtiger-docker:local
    container_name: vtiger
    restart: unless-stopped
    ports:
      - "${host_port:-8080}:80"
    environment:
      VTIGER_DB_NAME: ${VTIGER_DB_NAME:-vtiger}
      VTIGER_DB_USER: ${VTIGER_DB_USER:-vtiger}
      VTIGER_DB_PASS: ${VTIGER_DB_PASS:?set VTIGER_DB_PASS in .env}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-}
    volumes:
      - vtiger_source_volume:/var/www/html/
      - vtiger_mysql_data_volume:/var/lib/mysql

volumes:
  vtiger_source_volume:
    name: vtiger_source_volume
  vtiger_mysql_data_volume:
    name: vtiger_mysql_data_volumeyaml
  • build: . builds our hardened image from the Dockerfile in this directory.
  • The VTIGER_* variables replace the old mysql_user / mysql_pass. The :? syntax makes Compose fail fast if VTIGER_DB_PASS is missing.
  • The volumes persist the web files and the database.

Step 5: Build and start

docker compose up -d --build
# or
docker compose --env-file .env up -d --buildbash

This is the important part: the vtiger user and database are created on the first boot of an empty MySQL volume. If you already ran the official image with the old volumes, delete them first so the script runs:

docker compose down -v
# You might need to remove the volumes manually
docker volume ls
docker volume rm <volume>
# Clean docker compose start
docker compose up -d --build
# or
docker compose --env-file .env up -d --buildbash

Watch the bootstrap and wait for Apache:

docker compose logs -f vtigerbash

Step 6: Complete the installer

Open the CRM in your browser:

http://localhost:8080text

In the vTiger installer, use the credentials from your .env:

Database host:     localhost
Database name:     vtiger
Database username: vtiger
Database password: <VTIGER_DB_PASS>text

The application now connects with a least-privilege user that only has access to its own database, instead of the *.* superuser the official image creates.

Step 7: Where is my data?

Everything lives in the named volumes, not in the container filesystem:

docker volume lsbash

You should see vtiger_source_volume (web files) and vtiger_mysql_data_volume (database). As long as the names match, a new container reuses the same data.

Step 8: Backups

Back up both the database and the uploaded files:

docker compose exec vtiger mysqldump -u "$VTIGER_DB_USER" -p"$VTIGER_DB_PASS" "$VTIGER_DB_NAME" > vtiger-$(date +%F).sqlbash
docker run --rm -v vtiger_source_volume:/data -v "$PWD":/backup \
  alpine tar czf /backup/vtiger-files-$(date +%F).tar.gz -C /data .bash

Copy both artifacts to storage outside the server and test the restore procedure at least once.

Step 9: Upgrade vTiger

Because data lives in volumes, upgrading is mostly rebuilding with a newer base image:

docker compose pull
docker compose build
docker compose up -dbash

If the schema changed, vTiger may ask you to run the upgrade script from the UI. Always back up before upgrading.

Troubleshooting

The vtiger user/database was not created

The bootstrap script only runs on a fresh MySQL volume. If you reused a volume created by the official image, run docker compose down -v and start again (this deletes the data — back it up first).

Compose refuses to start with set VTIGER_DB_PASS in .env

The :? guard is working. Define VTIGER_DB_PASS in your .env.

Port already in use

Change host_port in .env and recreate:

docker compose up -d --force-recreatebash

I lost the admin password

Reset it from the container with MySQL access. You can clear the password hash and reset it through the UI:

docker compose exec vtiger mysql -u "$VTIGER_DB_USER" -p"$VTIGER_DB_PASS" "$VTIGER_DB_NAME"bash

I want a fresh install

Remove the containers and the volumes (this deletes all data — back up first):

docker compose down -vbash

Security notes

  • Never expose the container directly on the internet without HTTPS. Put it behind a reverse proxy (Nginx or Caddy) and terminate TLS there.
  • The application user is bound to localhost/127.0.0.1 and has ALL only on vtiger.*; it cannot read or modify other databases and cannot be used from outside the container.
  • Set MYSQL_ROOT_PASSWORD to lock down the MySQL root account. The base image still works because it only pings MySQL, not with credentials.
  • Change the admin password after install and enable vTiger's scheduled workflows (cron) so automation runs on time.
  • Monitor the cache and logs directories inside vtiger_source_volume, which grow over time.

External database access

The application user exists only for the app running in the same container. If an external agent needs to reach the database (reporting, ETL, backups, a second app…), do not reuse the vtiger credentials and do not widen its host. Create a separate account with the minimum privileges required, restricted to the specific client IP.

Connect as an administrator:

docker compose exec vtiger mysql -uroot -pbash

Example: a read-only reporting agent from a single IP:

CREATE USER 'vtiger_reporting'@'203.0.113.10' IDENTIFIED BY 'another-strong-password';
GRANT SELECT ON vtiger.* TO 'vtiger_reporting'@'203.0.113.10';sql

Example: an agent that needs to write but only to its own schema:

CREATE USER 'vtiger_etl'@'198.51.100.7' IDENTIFIED BY 'yet-another-password';
GRANT SELECT, INSERT, UPDATE ON vtiger.* TO 'vtiger_etl'@'198.51.100.7';sql

Best practices:

  • Use a specific client IP in the host column (or the network segment) instead of '%'.
  • Grant only the privileges the agent actually needs (SELECT, SELECT, INSERT, UPDATE, …), never ALL ON *.*.
  • Give every agent its own account and password; rotate and revoke them independently.
  • Keep port 3306 closed in the firewall unless an external agent genuinely needs it. Even with MySQL listening on 0.0.0.0 (which the base image sets), a connection only succeeds if a matching user@host account exists.

References

AI-generated image disclosure

The cover image (/images/posts/vtiger-docker-header.webp) was generated with the assistance of an AI model (deepseek-v4-flash). In accordance with Regulation (EU) 2024/1689 (EU Artificial Intelligence Act, Article 50), this content is disclosed as AI-generated. It is an original illustration created for this article without third-party images or logos; product names (vTiger, Docker) appear descriptively and no affiliation or endorsement is implied.