{"id":654,"date":"2026-09-01T12:12:48","date_gmt":"2026-09-01T10:12:48","guid":{"rendered":"https:\/\/techbygiusi.com\/?p=654"},"modified":"2026-09-01T21:13:56","modified_gmt":"2026-09-01T19:13:56","slug":"migrating-a-wordpress-website-from-hostinger-to-a-self-hosted-docker-setup","status":"publish","type":"post","link":"https:\/\/techbygiusi.com\/index.php\/guide\/migrating-a-wordpress-website-from-hostinger-to-a-self-hosted-docker-setup\/","title":{"rendered":"Migrating a WordPress Website from Hostinger to a Self-Hosted Docker Setup"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For this project, I wanted to migrate an existing WordPress website from Hostinger to my own infrastructure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The website was already fully built with WordPress and Elementor and contained an existing MySQL database and a large media library. Instead of rebuilding the website, the goal was to move the complete installation while keeping all pages, plugins, Elementor designs and media intact. The new environment is based on Debian, Docker and Pangolin.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">The New Setup<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The WordPress website runs on a dedicated Debian virtual machine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of installing the complete web stack directly on Debian, WordPress and MySQL run as separate Docker containers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The basic architecture looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"#lb-1-5dd77c57\" class=\"lb-thumb\" aria-label=\"Bild vergr\u00f6\u00dfern\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_07_08-1024x683.png\" alt=\"\" class=\"wp-image-659\" srcset=\"https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_07_08-1024x683.png 1024w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_07_08-300x200.png 300w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_07_08-768x512.png 768w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_07_08.png 1536w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pangolin handles external access to the services, which means the WordPress server itself does not need to be directly exposed to the Internet.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Preparing the Debian Server<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The first step was creating a dedicated Debian VM and installing Docker and Docker Compose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the WordPress environment, I created the following directory structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/opt\/wordpress\/html\nsudo mkdir -p \/opt\/wordpress\/import\ncd \/opt\/wordpress<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>html<\/code> directory contains the actual WordPress installation, while <code>import<\/code> can be used temporarily during the migration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The database uses its own persistent Docker volume.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Creating the WordPress Docker Stack<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The stack consists of two main containers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>WordPress\nwordpress:php8.2-apache\n\nDatabase\nmysql:8.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">WordPress is exposed internally on port <code>8080<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The database is only accessible through the internal Docker network and does not need to be published on the host.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The important part of the setup is persistent storage. The WordPress files are stored under:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/opt\/wordpress\/html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">while the MySQL database is stored in a persistent Docker volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This allows the containers themselves to be recreated or updated without losing the website.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Exporting the Website from Hostinger<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The existing Hostinger installation consists of two important parts:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public_html\nMySQL database<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The complete <code>public_html<\/code> directory needs to be downloaded from Hostinger.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This includes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wp-admin\nwp-content\nwp-includes\nwp-config.php\nplugins\nthemes\nuploads<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For downloading the WordPress files, I recommend using <strong>FTP with FileZilla<\/strong>. Especially for larger WordPress installations, FileZilla makes it easy to transfer the complete <code>public_html<\/code> directory while keeping the original folder structure intact.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create an FTP account in Hostinger or use the existing FTP credentials, connect to the server with FileZilla, and download the complete <code>public_html<\/code> directory to your local machine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The MySQL database also needs to be exported separately as an SQL file. This can be done through the Hostinger control panel using phpMyAdmin or the available database export function.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In my case, the WordPress files were around 38 GB, mainly because of the existing media library.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Transferring the WordPress Files<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The exported <code>public_html<\/code> directory can be copied directly to the persistent storage of the new server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I used:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/opt\/wordpress\/import\/public_html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">as the temporary migration location.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the transfer was complete, I copied the existing WordPress installation into the directory used by the Docker container:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cp -a \/opt\/wordpress\/import\/public_html\/. \/opt\/wordpress\/html\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>-a<\/code> option preserves the directory structure and file attributes while copying the complete installation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Afterwards, the size can be verified with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>du -sh \/opt\/wordpress\/html<\/code><\/pre>\n\n\n\n<h6 class=\"wp-block-heading\">Importing the MySQL Database<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The SQL export is copied to the WordPress server as well:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/opt\/wordpress\/backup.sql<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Before importing it, the database references from the old hosting environment should be adjusted to match the new Docker database.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In my setup, the new database is called:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wordpress<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The SQL dump can then be imported directly into the MySQL container:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker exec -i wordpress-db \\\nmysql -u wordpress -p\"PASSWORD\" wordpress \\\n&lt; backup.sql<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After the import, the database can be verified with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker exec wordpress-db \\\nmysql -u wordpress -p\"PASSWORD\" \\\n-e \"USE wordpress; SHOW TABLES;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The existing WordPress tables should now be visible, including tables such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wp_posts\nwp_postmeta\nwp_options\nwp_users\nwp_usermeta\nwp_terms<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">as well as the tables created by Elementor and other installed plugins.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Updating wp-config.php<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The existing <code>wp-config.php<\/code> can continue to be used, but the database connection needs to match the new Docker environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The important settings are:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>define('DB_NAME', 'wordpress');\ndefine('DB_USER', 'wordpress');\ndefine('DB_PASSWORD', 'YOUR_PASSWORD');\ndefine('DB_HOST', 'wordpress-db');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because both containers are connected through Docker networking, WordPress can use the MySQL container name as the database host.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Starting WordPress<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">Once the files and database are in place, the containers can be started:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The status can be checked with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">WordPress is internally available through:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>http:\/\/SERVER-IP:8080<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A simple test can also be performed directly from the server:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -I http:\/\/127.0.0.1:8080<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">At this point, the original WordPress installation is already running on the new self-hosted server.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Publishing WordPress Through Pangolin<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">For external access, I use Pangolin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of forwarding ports directly from the Internet to the WordPress VM, Pangolin provides access to the internal service. I created resources for:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>der-stammtisch.com\nwww.der-stammtisch.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both resources point to the WordPress service running on port <code>8080<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The public DNS records for the website point to the Pangolin edge server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Existing mail records such as MX, SPF, DKIM and mail-related CNAME records can remain unchanged because they are independent from the WordPress hosting.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Enabling HTTPS<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">HTTPS is handled by Traefik on the Pangolin edge server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s Encrypt is configured as the certificate resolver, allowing Traefik to automatically request and renew certificates for the published domains.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After publishing the resources, the certificates can be verified with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl s_client \\\n-connect der-stammtisch.com:443 \\\n-servername der-stammtisch.com &lt;\/dev\/null 2&gt;\/dev\/null \\\n| openssl x509 -noout -subject -issuer -dates<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same check can be performed for the <code>www<\/code> domain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl s_client \\\n-connect www.der-stammtisch.com:443 \\\n-servername www.der-stammtisch.com &lt;\/dev\/null 2&gt;\/dev\/null \\\n| openssl x509 -noout -subject -issuer -dates<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Both domains are now protected with automatically managed Let\u2019s Encrypt certificates.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Using WWW as the Primary Domain<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">For the final setup, I decided to use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/www.der-stammtisch.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">as the canonical website address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The root domain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/der-stammtisch.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">redirects visitors to the <code>www<\/code> version.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This keeps the website consistent and prevents WordPress and Elementor from generating resources with different hostnames.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Updating Elementor URLs<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">Because the website was migrated and the primary URL was changed to the <code>www<\/code> version, Elementor\u2019s stored URLs also need to be updated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Elementor provides a built-in tool for exactly this purpose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In WordPress, navigate to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Elementor\n\u2192 Tools\n\u2192 Replace URL<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The old URL is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/der-stammtisch.com\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and the new URL is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/www.der-stammtisch.com\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Running the replacement updates Elementor\u2019s stored references to the new canonical address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is especially important for generated CSS files, locally stored Google Fonts, images and other Elementor resources.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, the website uses fonts such as Bagel Fat One, which Elementor stores locally inside the WordPress uploads directory.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After replacing the URL, the public website and Elementor editor use the same resources and styling.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Cleaning Up the Migration Files<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">Once the website has been tested successfully, the temporary migration files are no longer required.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The imported copy can be removed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rm -rf \/opt\/wordpress\/import\/public_html<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The SQL migration files can also be deleted:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rm -f \/opt\/wordpress\/backup.sql\nrm -f \/opt\/wordpress\/backup-original.sql<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The production WordPress files remain safely stored under:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/opt\/wordpress\/html<\/code><\/pre>\n\n\n\n<h6 class=\"wp-block-heading\">Adding Immich for Photos and Videos<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">With the website now running on my own infrastructure, I also added Immich to the server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of storing large collections of photos and videos directly inside the WordPress media library, Immich provides a dedicated platform for managing them. Immich runs as another Docker stack and is published through Pangolin using its own subdomain.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The actual media storage is located on my NAS. This gives the environment a clear separation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>WordPress\nWebsite and articles\n\nImmich\nPhotos and videos\n\nNAS\nMedia storage\n\nPangolin\nSecure external access<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The NAS share is mounted on the Debian host and then made available to the Immich containers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This also means the large media collection does not have to live on the VM\u2019s local system disk.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Final Architecture<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">After the migration, the complete environment looks roughly like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"#lb-2-caf8673a\" class=\"lb-thumb\" aria-label=\"Bild vergr\u00f6\u00dfern\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_11_54-1024x683.png\" alt=\"\" class=\"wp-image-660\" srcset=\"https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_11_54-1024x683.png 1024w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_11_54-300x200.png 300w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_11_54-768x512.png 768w, https:\/\/techbygiusi.com\/wp-content\/uploads\/2026\/09\/ChatGPT-Image-1.-Sept.-2026-12_11_54.png 1536w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\"><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">WordPress and Immich remain internal services, while Pangolin handles external access and TLS termination.<\/p>\n\n\n\n<h6 class=\"wp-block-heading\">Conclusion<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">The existing WordPress and Elementor website is now completely self-hosted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The migration preserved the original website, including its database, Elementor pages, plugins, themes and media files. There was no need to rebuild the site from scratch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Moving WordPress into Docker also gives me much more control over the environment. WordPress, MySQL and Immich are separated into individual services, persistent data is stored independently from the containers, and external access is handled centrally through Pangolin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final result combines a traditional WordPress website with a modern self-hosted infrastructure while keeping the individual components simple and maintainable.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>For this project, I wanted to migrate an existing WordPress website from Hostinger to my own infrastructure. The website was already fully built with WordPress and Elementor and contained an existing MySQL database and a large media library. Instead of rebuilding the website, the goal was to move the complete installation while keeping all pages, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[15],"tags":[12,30,31],"class_list":["post-654","post","type-post","status-publish","format-standard","hentry","category-guide","tag-linux","tag-setup","tag-wordpress"],"_links":{"self":[{"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/posts\/654","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/comments?post=654"}],"version-history":[{"count":3,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/posts\/654\/revisions"}],"predecessor-version":[{"id":688,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/posts\/654\/revisions\/688"}],"wp:attachment":[{"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/media?parent=654"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/categories?post=654"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/techbygiusi.com\/index.php\/wp-json\/wp\/v2\/tags?post=654"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}