Hello all,
I am not a programmer but have successfully migrated a XAMPP OpenEMR v 7.0.2 to Docker 7.0.2 for our Windows 11 and wanted to share in case anyone else wanted to do the same. We have a small, but growing, practice.
I have spent many hours figuring this out. The easiest way is to migrate the same version of OpenEMR on XAMPP to the same image of OpenEMR in Docker. Make sure the XAMPP v 7.0.2 has had all 3 patches because when you pull the Docker image of v 7.0.2, it will have all the patches. Then, you can upgrade the Docker version to OpenEMR 7.0.3 after a successful migration.
Also, if you are using Two Factor Authentication, turn it off. The authentication mechanism doesn’t carry over in the migration, but all the usernames and passwords will. If you forget to remove the Two Factor Authentication, you will have to find the tables later for Two Factor Authentication and remove them before being able to log into your migrated system. Once migrated over to Docker, just reinstate Two Factor Authentication. Much easier!
Since we are a chiropractic office and don’t use all the features, I believe the steps I provide will migrate everything you require, but I cannot guarantee. There is one step included to migrate the documents (since they are not part of the database tables).
With that being said, the migration to the Docker system will require some work ahead of the migration. For our migration, I used docker secrets to protect the passwords, so you must operate Docker in Swarm mode. We have a self-signed certificate (example.local) that we obtained in order to have https on our local XAMPP system. You will be able to use that for the migration without having to obtain a different certificate. I have also set the Docker container to run a reverse proxy (Nginx) to further protect from outside access to OpenEMR. So, the things you will need to migrate the system are your .crt, .csr, and .key files for ssl certificate, a docker-compose.yml file, a .env file, and a nginx.conf file.
Before starting anything, make a backup of everything. There is an excellent post on the Forum explaining how to make a XAMPP backup of the entire XAMPP folder.
On your C: drive (or whatever drive you choose to use) create a Docker folder and an openemr folder inside this folder (C:\Docker\openemr). Inside the openemr folder, create another folder entitled cert (C:\Docker\openemr\cert). Place your .crt, .key, and .csr into the cert folder. Place your .env, docker-compose.yml, and nginx.conf files in the openemr folder. Also, create a C:\Backup folder.
All commands will be run from PowerShell as an Administrator.
You can create the .env, docker-compose.yml, and nginx.conf files in Notepad.
The below steps are using the following names and passwords, which you should change to whatever is appropriate for your system: OpenEMR service is named openemr_app. OpenEMR database is named openemr_db. Admin user is openemr_admin. Mariadb password is MyRootPassword. Admin user password is MyAdminPassword.
You will be running your XAMPP instance and your Docker instance at the same time, so in order to not have port conflicts, I have set the below configurations to run on port 8043 in the Docker instance. You can also change this to any available port you would like.
Your .env file should look like this:
MYSQL_ROOT_PASSWORD_FILE=/run/secrets/openemr_db_password
MYSQL_USER=openemr_admin
MYSQL_PASSWORD_FILE=/run/secrets/openemr_admin_password
MYSQL_DATABASE=openemr
Your nginx.conf file should look like this:
events {}
http {
server {
listen 443 ssl;
server_name example.local;
client_max_body_size 30M;
ssl_certificate /etc/nginx/certs/example.local.crt;
ssl_certificate_key /etc/nginx/certs/example.local.key;
location / {
proxy_pass http://openemr_app:80;
proxy_http_version 1.1;
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_set_header X-Forwarded-Port 443;
# prevent 502 errors on large headers or cookies
proxy_buffers 16 16k;
proxy_buffer_size 32k;
}
}
}
Your docker-compose.yml file should look like this:
services:
openemr_db:
image: mariadb:10.6
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
secrets:
- openemr_db_password
- openemr_admin_password
environment:
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/openemr_db_password
- MYSQL_USER=openemr_admin
- MYSQL_PASSWORD_FILE=/run/secrets/openemr_admin_password
- MYSQL_DATABASE=openemr
volumes:
- openemr_mysql_data:/var/lib/mysql
deploy:
restart_policy:
condition: any
openemr_app:
image: openemr/openemr:7.0.2
secrets:
- openemr_admin_password
environment:
- MYSQL_HOST=openemr_db
- MYSQL_USER=openemr_admin
- MYSQL_PASS_FILE=/run/secrets/openemr_admin_password
- MYSQL_DATABASE=openemr
volumes:
- openemr_log:/var/log
- openemr_sites:/var/www/localhost/htdocs/openemr/sites
depends_on:
- openemr_db
deploy:
restart_policy:
condition: any
nginx:
image: nginx:stable
ports:
- "8043:443"
volumes:
- ./certs:/etc/nginx/certs:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- openemr_app
deploy:
restart_policy:
condition: any
volumes:
openemr_mysql_data:
external: true
openemr_sites:
external: true
openemr_log:
external: true
secrets:
openemr_db_password:
external: true
openemr_admin_password:
external: true
networks:
default:
name: openemr_net
Now for the steps:
Preparation
- Download, install, and run Docker Desktop.
- Run XAMPP as administrator, open OpenEMR, and log in with each user.
a) Disable Two Factor Authentication (TFA) for all users.
b) Log out. - Start up Windows PowerShell and run as Administrator.
a. Change directory to C:\docker\openemr
cd C:\docker\openemr
-
Create a backup folder - C:\Backups
-
Copy the entire XAMPP openemr folder and paste it into C:\Backups
-
Create Docker project folder - C:\docker\openemr
-
Place your .env and .yml files into C:\docker\openemr.
-
Create Cert folder inside openemr folder - C:\docker\openemr\cert
-
Place your .key, .csr, and .crt files into C:\docker\openemr\cert.
Deploy with Swarm
- In PowerShell, initiate swarm
docker swarm init
- Verify Swarm
Docker info
Swarm should be listed as “active”
Create Docker Secrets for DB Passwords
- Create root password:
echo -n "MyRootPassword" | docker secret create openemr_db_password -
- Create Admin password:
echo -n "MyAdminPassword" | docker secret create openemr_admin_password -
- Verify secrets were created:
docker secret ls
Deploy Stack OpenEMR
- Use this command to spin up the stack containers:
docker stack deploy -c docker-compose.yml openemr
Then wait 60–90 seconds for all services to fully initialize.
Drop Existing Database in Docker (Clean Start)
You will need to drop the existing database from the Docker instance so that you can import your XAMPP database into the container. Otherwise, you will get duplicate tables, and the running instance will default to the original image database, not your XAMPP database. Also, since mariadb utilizes Unix Socket for root authentication, you will have to temporarily suspend privileges in mariadb in order to set root authentication to native password authentication.
Stop mariadb container and open a new PowerShell window; start up a temporary mariadb container where privileges are suspended and alter root and admin back to native password:
- List running docker containers
Docker ps
- List the running stack
docker stack ls
- List the running services
docker service ls
You will take information from the resulting data.
Since you are running in swarm mode, you will have to scale down the service. If you try to stop the service, it will automatically spin up a new container.
- Scale down mariadb (the container automatically prefaces the service name with openemr, so the command must be for openemr_openemr_db, not just openemr_db)
docker service scale openemr_openemr_db=0
- Mount for inspection
docker service inspect openemr_openemr_db --format '{{json .Spec.TaskTemplate.ContainerSpec.Mounts}}' | ConvertFrom-Json
- Skip the grant tables
docker run --rm -it -v openemr_mysql_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=MyRootPassword mariadb:10.6 --skip-grant-tables --skip-networking
Open another PowerShell terminal as Administrator
- Verify the running containers
docker ps
- Enter mariadb
docker exec -it <mariadb_container_id> mariadb -u root
Then in MariaDB:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('MyRootPassword');
FLUSH PRIVILEGES;
EXIT;
Close terminals.
- Close temporary PowerShell and the original PowerShell terminal
Open a new PowerShell terminal as administrator.
- Open new PowerShell terminal and change directory
cd C:\docker\openemr
Start up mariadb
- Scale up mariadb
docker service scale openemr_openemr_db=1
- Log into mariadb
docker exec -it $(docker ps -q -f name=openemr_openemr_db) mariadb -u root -p
Enter the root password
Then in MariaDB:
DROP DATABASE openemr;
SHOW DATABASES;
(If openemr database has not been dropped, rerun the DROP command. If the openemr database has been dropped, create the new empty openemr database
CREATE DATABASE openemr;
- Verify database has been created
SHOW DATABASES;
- Exit
EXIT;
Export OpenEMR Database from XAMPP
- Open phpMyAdmin from XAMPP.
- Export database openemr → Quick → SQL.
a. Click on openemr
b. Click on Export
c. Make sure Quick and SQL are selected
d. The exported database will end up in Downloads folder (This may take a while, depending on the amount of data that needs to be exported.). - Rename the downloaded file as openemr_backup.sql
- Move the exported file from the Downloads folder to the Backups folder (C:\Backups\openemr_backup.sql)
Convert SQL File to UTF8
- Convert the SQL file to UTF8 by running the following command:
Get-Content "C:\Backups\openemr_backup.sql" | Set-Content -Encoding UTF8 "C:\Backups\openemr_backup_utf8.sql"
You will now see the openemr_backup.sql file and the openemr_backup_utf8.sql file in the Backups folder.
Copy the Backup File Into the Database Container
- Assuming your backup file is: C:\Backups\openemr_backup_utf8.sql, run the following command: (This may also take a while, depending on size of database.).
docker cp C:\Backups\openemr_backup_utf8.sql <mariadb_container_id>:/tmp/openemr_backup_utf8.sql
Import the SQL File Into the Database
- First, create openemr_admin and make openemr_admin native password authenticated:
docker exec -it $(docker ps -q -f name=openemr_openemr_db) mariadb -u root -p
Then in MariaDB:
CREATE USER 'openemr_admin'@'localhost' IDENTIFIED BY 'MyAdminPassword';
GRANT ALL PRIVILEGES ON openemr.* TO 'openemr_admin'@'%';
GRANT ALL PRIVILEGES ON openemr.* TO 'openemr_admin'@'localhost';
FLUSH PRIVILEGES;
ALTER USER 'openemr_admin'@'%' IDENTIFIED VIA mysql_native_password USING PASSWORD('MyAdminPassword');
ALTER USER 'openemr_admin'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('MyAdminPassword');
FLUSH PRIVILEGES;
EXIT;
Then, Import the Database
docker exec -it <mariadb_container_id> sh -c "mysql -u openemr_admin -p openemr < /tmp/openemr_backup_utf8.sql"
Verify OpenEMR database transfer occurred by confirming table repopulation
- Login to mariadb
docker exec -it $(docker ps -q -f name=openemr_openemr_db) mariadb -u root -p
Enter root password
Then in MariaDB:
SHOW DATABASES;
USE openemr;
SHOW TABLES;
# If tables are present, transfer was successful, if not, try import again with the docker exec command.
EXIT;
Replace Config, Sqlconf, & Fix Ownership
- Replace config.php with the original XAMPP version:
docker cp C:\xampp\htdocs\openemr\sites\default\config.php <openemr_app_container_id>:/var/www/localhost/htdocs/openemr/sites/default/config.php
- Replace sqlconf with the original XAMPP version:
docker cp C:\xampp\htdocs\openemr\sites\default\sqlconf.php <openemr_app_container_id>:/var/www/localhost/htdocs/openemr/sites/default/sqlconf.php
- Set ownership for sqlconf and config.php:
docker exec -it $(docker ps -qf "name=<full_openemr_app_name>") sh -c "chown apache:apache /var/www/localhost/htdocs/openemr/sites/default/sqlconf.php && \
chmod 640 /var/www/localhost/htdocs/openemr/sites/default/sqlconf.php"
docker exec -it $(docker ps -qf "name=<full_openemr_app_name>") sh -c "chown apache:apache /var/www/localhost/htdocs/openemr/sites/default/config.php && \
chmod 640 /var/www/localhost/htdocs/openemr/sites/default/config.php"
Modify Configuration of Sqlconf File
- Make sure $host = ‘openemr_db’; $login = ‘openemr_admin’; and $pass = ‘MyAdminPassword’; (Shift + I will allow editing. To exit and save, hit escape, then type :wq! and then Enter. You should be returned to the PowerShell prompt. You can verify if the changes were actually saved by running the below command again, and once satisfied, type :q! to exit (quit) back to the PowerShell prompt).
docker exec -it $(docker ps -qf "name=<full_openemr_app_name>") vi /var/www/localhost/htdocs/openemr/sites/default/sqlconf.php
Copy Documents folder from XAMPP to Docker container and fix ownership
- Copy the documents folder from XAMPP to the Docker container by running the following command:
docker cp "C:\xampp\htdocs\openemr\sites\default\documents" <full_openemr_app_name>:/var/www/localhost/htdocs/openemr/sites/default/
If the documents directory already exists in the container, this will merge contents, not delete them.
- Fix ownership of Documents folder
docker exec -it <full_openemr_app_name> sh
chown -R apache:apache /var/www/localhost/htdocs/openemr/sites/default/documents
chmod -R 755 /var/www/localhost/htdocs/openemr/sites/default/documents
exit
Fix Database Paths
- Fix the database paths by entering mariadb
docker exec -it $(docker ps -qf "name=openemr_openemr_db") mysql -u root -p openemr
Enter root password
45. And running the following commands to replace the absolute Windows C:/xampp/… and file://C:/… with the relative documents/… path that OpenEMR expects.
UPDATE documents SET url = REPLACE(url, 'C:/xampp/htdocs/openemr/sites/default/documents', '/var/www/localhost/htdocs/openemr/sites/default/documents') WHERE url LIKE 'C:/xampp%';
UPDATE documents SET url = REPLACE(url, 'file://C:/xampp/htdocs/openemr/sites/default/documents', '/var/www/localhost/htdocs/openemr/sites/default/documents') WHERE url LIKE 'file://C:/xampp%';
EXIT;
Verify all Documents files were routed correctly
- Run the following command to verify all documents were copied over to the Docker container. If there are missing files, rerun the copy process and update the ownership again.
$dbContainer=$(docker ps -qf "name=openemr_openemr_db"); $appContainer=$(docker ps -qf "name=openemr_openemr_app"); $urls=$(docker exec -i $dbContainer sh -c "mysql -u openemr_admin --password='MyAdminPassword' -N -B openemr -e 'SELECT url FROM documents;'" | ForEach-Object { $_.Trim() }); $total=0; $present=0; $missing=0; foreach ($url in $urls) { if ($url -ne "") { $total++; $relativePath=$url -replace '^file://C:/xampp/htdocs/openemr/sites/default/','' -replace '^C:/xampp/htdocs/openemr/sites/default/','' -replace '^/var/www/localhost/htdocs/openemr/sites/default/',''; $exists=$(docker exec $appContainer sh -c "[ -f /var/www/localhost/htdocs/openemr/sites/default/$relativePath ] && echo EXISTS || echo MISSING"); if ($exists -eq "EXISTS") {$present++} else {$missing++} } }; Write-Output "=== Verification Summary ==="; Write-Output "Documents checked: $total"; Write-Output "Present: $present"; Write-Output "Missing: $missing"
This will provide a total of documents reviewed, those existing in the Docker database, and those missing from the Docker database. Since you will have to put your admin password in-line (I could not get it to work any other way), make sure to clear your PowerShell history after performing the migration steps.
Restore unix_socket authentication to root
- Enter MariaDB
docker exec -it $(docker ps -q -f name=openemr_openemr_db) mariadb -u root -p
Enter root password
Then in MariaDB:
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;
FLUSH PRIVILEGES;
EXIT;
Restart Everything
- Restart by shutting down the stack containers and spinning them back up
docker stack rm openemr
docker stack deploy -c docker-compose.yml openemr
- Confirm hosts file has entry for example.local
a. Navigate to C:\Windows\System32\drivers\etc\hosts
b. Ensure hosts file has 127.0.0.1 example.local as an entry
Visit the Site & Log In
- Open a web browser and go to: https://example.local:8043
- Once you have logged in and confirmed that the data migrated appropriately, Reestablish Two Factor Authentication for each user.
I really hope this helps someone out. I practiced on a test environment with minimal fake data and successfully migrated it with the above. I then successfully ran the above steps on our production OpenEMR. It has been running well for about a month. I am now working on reconfiguring the nginx.conf file in order to reverse proxy the OpenEMR portal to our website’s subdomain. The above will get the migration done. Updating to v. 7.0.3 is as simple as shutting down the stack, opening the docker-compose.yml, changing 7.0.2 to 7.0.3, and redeploying the stack (It will take a little longer to spin up because it is pulling the new version.).