{"id":1377,"date":"2025-07-15T15:26:28","date_gmt":"2025-07-15T15:26:28","guid":{"rendered":"https:\/\/paoloronco.it\/?p=1377"},"modified":"2025-11-18T10:08:38","modified_gmt":"2025-11-18T10:08:38","slug":"1377-2","status":"publish","type":"post","link":"https:\/\/paoloronco.it\/en\/1377-2\/","title":{"rendered":"Replication and Failover of a Docker Environment: From Proxmox Container to the Cloud"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Running a website on a local server (on-premises) offers many advantages in terms of control and customization, but it carries a significant risk: business continuity.<br>A server in your home or office cannot guarantee uptime. <strong>100%<\/strong> \u2014all it takes is a power outage, a hardware problem, or, as in my case, being away on vacation, and the site could be offline for hours or days.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#039;s why I decided to implement a solution <strong>Replication + Failover to Cloud<\/strong>, which allows you to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Replicate periodically<\/strong> WordPress content and database from local server to cloud<\/li>\n\n\n\n<li><strong>Perform an automatic failover<\/strong> (or semi-automatic) in case the local server is offline<\/li>\n\n\n\n<li>Still have the ability to manage and update services remotely<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">How does my solution work?<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Local Replication \u2192 Cloud<\/strong>\n<ul class=\"wp-block-list\">\n<li>Backup scripts of the WordPress web servers and database are run every night.<\/li>\n\n\n\n<li>Files are transferred via <code>scp<\/code> to the cloud server with SSH key authentication<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Restore to the Cloud<\/strong>\n<ul class=\"wp-block-list\">\n<li>In the morning, cronjobs on the cloud server run the restore scripts: the containers are updated and the database is restored.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Failover<\/strong>\n<ul class=\"wp-block-list\">\n<li>If your local server is online, Cloudflare balances the traffic.<\/li>\n\n\n\n<li>If the local server is offline, traffic switches to the cloud server without manual intervention<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Cloud Management<\/strong>\n<ul class=\"wp-block-list\">\n<li>Portainer to manage Docker<\/li>\n\n\n\n<li>Fail2Ban and basic security hardening<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">When the local container is UP:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u2705 Handles requests normally<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">When the local container is DOWN:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u2705 Cloudflare routes requests to the cloud VM<br>\u2705 The site remains online, services continue to function<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading has-text-align-center has-x-large-font-size\">Tutorial:<\/h2>\n\n\n\n<p class=\"has-text-align-center has-large-font-size wp-block-paragraph\"><strong>OnPremise:<\/strong><\/p>\n\n\n\n<h4 class=\"wp-block-heading has-medium-font-size\">a) Backup of Webserver Docker Volumes<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">For each dockerized service (like NGINX or others), I create a backup of the volumes using a temporary Alpine container.<br>Finally, I move everything to the cloud via <strong>SCP<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Example - <strong>Sample Web Server<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br><br>DATE=$(date +&quot;%Y%m%d-%H%M&quot;)<br>BACKUP_DIR=&quot;\/root\/backups&quot;<br>CLOUD_USER=&quot;insert_username&quot;<br>CLOUD_IP=&quot;insert_cloud_ip&quot;<br>SSH_KEY=&quot;enter_ssh_key&quot;<br>REMOTE_PATH=&quot;\/home\/inserisci_username\/webserver_backup_${DATE}.tar.gz&quot;<br><br>mkdir -p ${BACKUP_DIR}<br>cd ${BACKUP_DIR}<br><br># Backup Docker Volumes<br>docker run --rm -v insert_volume_id1:\/volume -v $(pwd):\/backup alpine sh -c &quot;cd \/volume &amp;&amp; tar czf \/backup\/nginx_backup.tar.gz .&quot;<br>docker run --rm -v insert_volume_id2:\/volume -v $(pwd):\/backup alpine sh -c &quot;cd \/volume &amp;&amp; tar czf \/backup\/html_backup.tar.gz .&quot;<br><br># Create single archive and transfer to cloud<br>tar czf webserver_backup_${DATE}.tar.gz nginx_backup.tar.gz html_backup.tar.gz<br>scp -i ${SSH_KEY} webserver_backup_${DATE}.tar.gz ${CLOUD_USER}@${CLOUD_IP}:${REMOTE_PATH}<br><br>rm -f nginx_backup.tar.gz html_backup.tar.gz<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">b) WordPress Database Dump<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">I take a SQL dump from the MySQL\/MariaDB container and transfer it to the cloud:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br><br>DATE=$(date +&quot;%Y%m%d-%H%M&quot;)<br>LOCAL_DUMP=&quot;\/root\/wordpress_db_backup_${DATE}.sql&quot;<br>CLOUD_USER=&quot;insert_username&quot;<br>CLOUD_IP=&quot;insert_cloud_ip&quot;<br>SSH_KEY=&quot;enter_ssh_key&quot;<br>DB_USER=&quot;insert_db_user&quot;<br>DB_PASS=&quot;enter_db_password&quot;<br>DB_NAME=&quot;insert_db_name&quot;<br><br>docker exec insert_db_container_name mysqldump --no-tablespaces -u ${DB_USER} -p&quot;${DB_PASS}&quot; ${DB_NAME} &gt; ${LOCAL_DUMP}<br><br>scp -i ${SSH_KEY} ${LOCAL_DUMP} ${CLOUD_USER}@${CLOUD_IP}:\/home\/insert_username\/<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">c) Automation with Cronjob<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Crontab example:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>0 3 * * * \/root\/scripts\/replica_webserver.sh<br>0 4 * * * \/root\/scripts\/replica_wordpress.sh<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"has-text-align-center has-large-font-size wp-block-paragraph\"><strong>Cloud:<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">a) Restoring Web Server Volumes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On the cloud, I unpack the received backups and restore them to Docker volumes:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br><br>BACKUP_FILE=$(ls -t \/home\/insert_username\/webserver_backup_*.tar.gz | head -n 1)<br>RESTORE_DIR=&quot;\/root\/restore_temp&quot;<br><br>mkdir -p ${RESTORE_DIR}<br>tar xzf ${BACKUP_FILE} -C ${RESTORE_DIR}<br><br>docker run --rm -v insert_volume_id1:\/data -v ${RESTORE_DIR}:\/backup alpine sh -c &quot;rm -rf \/data\/* &amp;&amp; tar xzf \/backup\/nginx_backup.tar.gz -C \/data&quot;<br>docker run --rm -v insert_volume_id2:\/data -v ${RESTORE_DIR}:\/backup alpine sh -c &quot;rm -rf \/data\/* &amp;&amp; tar xzf \/backup\/html_backup.tar.gz -C \/data&quot;<br><br>rm -rf ${RESTORE_DIR}<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">b) WordPress Database Restore<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br><br>DUMP_PATH=$(ls -t \/home\/insert_username\/wordpress_db_backup_*.sql | head -n 1)<br>CONTAINER_DB=&quot;enter_container_db_name&quot;<br>DB_USER=&quot;insert_db_user&quot;<br>DB_PASS=&quot;enter_db_password&quot;<br>DB_NAME=&quot;insert_db_name&quot;<br><br>docker cp &quot;$DUMP_PATH&quot; &quot;$CONTAINER_DB:\/wordpress_db_backup.sql&quot;<br><br>docker exec -i &quot;$CONTAINER_DB&quot; mysql -u &quot;$DB_USER&quot; -p&quot;$DB_PASS&quot; &quot;$DB_NAME&quot; -e &quot;<br>SET FOREIGN_KEY_CHECKS=0;<br>DROP TABLE IF EXISTS wp_commentmeta, wp_comments, wp_links, wp_options, wp_postmeta, wp_posts, wp_term_relationships, wp_terms, wp_term_taxonomy, wp_usermeta, wp_users;<br>SET FOREIGN_KEY_CHECKS=1;&quot;<br><br>docker exec -i &quot;$CONTAINER_DB&quot; sh -c &quot;mysql -u $DB_USER -p\\&quot;$DB_PASS\\&quot; $DB_NAME &lt; \/wordpress_db_backup.sql&quot;<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">c) Automation with Cronjob on the Cloud<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Crontab example:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>0 5 * * * \/root\/scripts\/restore_webserver.sh<br>0 6 * * * \/root\/scripts\/restore_wordpress.sh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"has-text-align-center has-large-font-size wp-block-paragraph\"><strong>Cloudflare<\/strong>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On both machines (Proxmox and Cloud), I enable the Cloudflare tunnel:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cloudflared service install insert_token_tunnel<br><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cloudflare automatically manages traffic based on the availability of your on-premises container or cloud VM.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>","protected":false},"excerpt":{"rendered":"<p>Gestire un sito web su un server locale (on-premises) offre molti vantaggi in termini di controllo e personalizzazione, ma porta con s\u00e9 un rischio importante: la continuit\u00e0 operativa.Un server in casa o in ufficio non pu\u00f2 garantire uptime del 100% \u2014 basta un blackout, un problema hardware o, come nel mio caso, la lontananza durante &hellip; <a href=\"https:\/\/paoloronco.it\/en\/1377-2\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Replica e Failover di un Ambiente Docker: Dal Container Proxmox al Cloud&#8221;<\/span><\/a><\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[23],"class_list":["post-1377","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-portfolio"],"_links":{"self":[{"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/posts\/1377","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/comments?post=1377"}],"version-history":[{"count":7,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/posts\/1377\/revisions"}],"predecessor-version":[{"id":1386,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/posts\/1377\/revisions\/1386"}],"wp:attachment":[{"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/media?parent=1377"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/categories?post=1377"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/paoloronco.it\/en\/wp-json\/wp\/v2\/tags?post=1377"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}