How to Dockerize your Virtualmin Like a Pro (Without Losing Your Mind)

SYSTEM INFORMATION
OS type and version Linux Ubuntu 24.04
Webmin version 2.202
Virtualmin version 7.30.4
Webserver version Hetzner Dedicated

Good evening everyone.

As a first article and to contribute to the community, I share the article I uploaded on my personal website nicolaslagios.

After several failed attempts to dockerize virtualmin , finally the solution was to dockerize database and files and at the same time have the UI for dns and emails and everything else virtualmin gives.

Here is the article with the step by step instructions:

So, you want to dockerize Virtualmin? Stop right there! Trying to shove the entire Virtualmin system into a Docker container is a surefire way to fail. Instead, let’s do something smarter—dockerize only the databases. This approach offers a bulletproof setup with isolated database instances, automatic Virtualmin backups, and a disaster recovery plan that actually works.

In this guide, we’ll show you how to run each database in its own MariaDB instance, reducing the risk of total database failure in case of InnoDB corruption or cyberattacks. Bonus: You’ll still have Virtualmin’s UI to manage DNS, email, and everything else.

Why Combine Docker with Virtualmin?
Because, my dear reader, you get the best of both worlds:

  • Isolated databases: Each database runs in its own container with its own MariaDB instance.
  • Scalability: Easily move containers between servers.
  • Security: If one database is hacked, it doesn’t affect others.
  • Virtualmin UI stays intact: Clients can still manage their hosting environment while databases live inside Docker.

Step 1: Install Docker, Docker-Compose, and Portainer
First things first, install the required software:

apt update && apt install -y docker.io docker-compose

Then, deploy Portainer for a user-friendly container management experience:

docker run -d -p 9000:9000 --name=portainer --restart=unless-stopped -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer-ce

Access Portainer via http://yourdomain:9000 to easily manage your containers.

Step 2: Prepare the Virtualmin Server for Docker
Navigate to your Virtualmin server’s home directory and create a Docker workspace:

cd /home/SERVER_NAME
mkdir docker && cd docker
mkdir mysql database_backups

Step 3: Create a Docker Compose File
Inside the docker folder, create a docker-compose.yml file:

version: '3.8'
services:
  mariadb:
    build: .
    container_name: SERVER_NAME-db
    environment:
      MYSQL_ROOT_PASSWORD: "YOUR_ROOT_PASSWORD"
      MYSQL_DATABASE: "YOUR_DATABASE_NAME"
      MYSQL_USER: "YOUR_DATABASE_USER"
      MYSQL_PASSWORD: "YOUR_DATABASE_PASSWORD"
    volumes:
      - /home/SERVER_NAME/docker/mysql:/var/lib/mysql
    restart: unless-stopped
    networks:
      - SERVER_NAME-db_network
 
networks:
  SERVER_NAME-db_network:
    driver: bridge

Step 4: Create a Dockerfile
Inside the docker folder, create a Dockerfile:

FROM mariadb:latest
COPY my.cnf /etc/mysql/mariadb.conf.d/50-server.cnf

Step 5: Configure MariaDB
Create a my.cnf file inside the docker folder:

[client]
default-character-set = utf8mb4
 
[mysqld]
user = mysql
bind-address = 0.0.0.0
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_allowed_packet = 64M
innodb_buffer_pool_size = 2G
innodb_log_file_size = 512M
tmp_table_size = 256M
max_heap_table_size = 256M
table_open_cache = 8000
log_error = /var/log/mysql/error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-queries.log
long_query_time = 2

Step 6: Deploy the MariaDB Container
Navigate to your Docker directory and launch the container:

cd /home/SERVER_NAME/docker
docker-compose -f docker-compose.yml -p SERVER_NAME-db up -d

Step 7: Connect Your Application to the Dockerized Database
Get the container’s IP by checking Portainer or running:

docker inspect SERVER_NAME-db | grep "IPAddress"

Edit your WordPress wp-config.php or application database config to use the container’s IP:

define('DB_HOST', '172.XX.XX.XX:3306');

Step 8 (Optional): Set Up Automatic Backups
To ensure your database is safe, use Virtualmin’s built-in backup system:

  • Visit: YOUR_DOMAIN:10000/virtual-server/list_sched.cgi?xnavigation=1
  • Schedule backups to a remote storage.

For an additional backup solution, create a database export script:

nano /home/SERVER_NAME/docker/export_db.sh

Paste the following script:

#!/bin/bash
set -e
BASE_DIR="/home/SERVER_NAME/docker"
BACKUP_DIR="$BASE_DIR/database_backups"
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
RETENTION_DAYS=30
LOG_FILE="$BASE_DIR/cron_log.txt"
 
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/db_backup_$DATE.sql"
 
CONTAINER_NAME=$(grep -E '^\s*container_name:' "$BASE_DIR/docker-compose.yml" | awk -F': ' '{print $2}' | xargs)
DB_NAME=$(grep -E '^\s*MYSQL_DATABASE:' "$BASE_DIR/docker-compose.yml" | awk -F': ' '{print $2}' | xargs)
DB_USER=$(grep -E '^\s*MYSQL_USER:' "$BASE_DIR/docker-compose.yml" | awk -F': ' '{print $2}' | xargs)
DB_PASSWORD=$(grep -E '^\s*MYSQL_PASSWORD:' "$BASE_DIR/docker-compose.yml" | awk -F': ' '{print $2}' | xargs)
 
cd "$BASE_DIR" || exit 1
docker exec "$CONTAINER_NAME" mariadb-dump -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" > "$BACKUP_FILE"
tar -czf "$BACKUP_FILE.tar.gz" -C "$BACKUP_DIR" "$(basename "$BACKUP_FILE")"
rm "$BACKUP_FILE"
find "$BACKUP_DIR" -type f -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $BACKUP_FILE.tar.gz" >> "$LOG_FILE"

Make it executable:

chmod +x /home/SERVER_NAME/docker/export_db.sh

To automate daily backups, create a cron job in Virtualmin:

  • Go to YOUR_DOMAIN:10000/cron/?xnavigation=1
  • Add this command:
/home/SERVER_NAME/docker/export_db.sh >> /home/SERVER_NAME/docker/db_backup_log.txt 2>&1

Extra Tip: Want phpMyAdmin?
Download phpMyAdmin, unzip it in your website directory, and modify config.inc.php to set the container’s IP as the database host.

Congratulations, you’ve successfully dockerized Virtualmin’s databases while keeping the UI intact! This setup enhances security, performance, and scalability while ensuring that every database runs in an isolated container. Now, go enjoy your fully dockerized Virtualmin experience!

1 Like

Thanks, this was posted last year FYI

1 Like

That was me saying we have no plans to “Dockerize Virtualmin”, but that Virtualmin would eventually get the ability to manage Docker (Podman, actually, by default) containers for hosting web applications. It remains true that containerizing a big pile of applications like Virtualmin is sort of a sub-optimal way to use Docker.

This seems to only independently containerize the database, which is a start, but it isn’t where I would start if I were trying to improve security, isolation, performance or scalability, and I don’t think it actually does any of those things. I have never seen a system exploited because the database got exploited, though it is a potential attack vector it’s not among the ones I would look at first…in a default Virtualmin system you have to already have exploited the system to the point where you have the ability to run code in order to even talk to the database and most likely only as a domain owner user.

Not saying this is a bad experiment. It’s good to document the surfaces, the places where pieces come together. i.e. the ways Virtualmin and domain owners interact with databases is useful to have defined explicitly like this, for people who need this kind of isolation on a more total scale (they’d need to keep going if they wanted Virtualmin running in something like a Kubernetes cluster or whatever, this is just the beginning).

But, also it’s not what I was saying we were going to work on. There is no overlap, so if people want containerized Virtualmin, it probably won’t come from us any time soon, so I’m glad people are working on it (though I have my concerns about how people assume “in a container” means “faster, more efficient, more scalable, more secure, etc.”, because it’s generally none of those things, at least if everything is still connected to each other in a way that allows Virtualmin to manage everything in the way it currently does).

3 Likes

Hello Joe, I agree with you :slightly_smiling_face: . Anyway, this solution has helped speed up the server because everything is not stored in a single database and I have some clients with very bad and large databases. Additionally, personally, it’s common for me to experience InnoDB corruption, but since implementing this solution, I haven’t had any issues. Also, if you have clients who don’t manage their sites properly, the databases of other clients won’t be at risk. I understand your concerns, but this approach has proven beneficial in these specific cases."

Anyway, I also see it as the beginning of something good and I wanted to share it from my side ( because I was awake for several nights until I found a solution. I hope someone gained some sleep :rofl: ). I hope that at some point, we’ll see it officially. BTW, I’ve created a .sh script that automates the whole process, you just run it before the public_html.

Thank you for the effort you are making for us. virtualmin has personally helped me solve many problems

4 Likes

I would be great if Virtualmin would have a module as powerful as the entire portainer feature set.

But as an install, I like the native way.

1 Like

Sure, installing with a UI is easier and is definitely supported by Virtualmin, but apps like Phpmyadmin or Wordpress are essentially a zip file. You unzip it and it runs.

Anyway, I imagine when we see a version that supports Docker (Podman as Joe said), we’ll probably see additional Docker-related apps.

These 2 apps are easy installs on virtualmin as is but I see alot of app out there are docker only prob because there can package them all up into a zip.
Thinking about wordpress how is the health section like when done via docker?

Shouldn’t that be docker compose -f docker-compose.yml -p SERVER_NAME-db up -d

I get a error docker with docker-compose

Also are you doing this as root or one of the virtulamin server users, prob administrator of that virtual server.

I’m using Rocker to so I’m going off vultr doc to get install commands.