WordPress Migration: Moving from Docker to Amazon Lightsail

post2

I would like to share with you a clear guide with detailed explanation of the purpose of used tools how to migrate your test Docker container with preconfigured WordPress webpage to low cost and low-management AWS solution called AWS Lightsail.

The technology stack that this post covers:
– Docker
– MySQL
– Linux
– AWS
– Cloudflare DNS

GitHub Repo: LINK

I like to prototype my ideas before moving them into production, so I often use my homelab to experiment with a wide range of technologies. This blog was no different—I first set it up inside a Docker container. The biggest advantage? It was completely free.

But since I’m also a cloud enthusiast, I wanted to see how the site would run on Amazon Lightsail, which makes it easy to spin up projects quickly at a low cost.

In this post, I’ll walk you through the process of migrating a WordPress site from Docker to Amazon Lightsail. Along the way, you’ll find short videos demonstrating the steps as well as copy-paste commands and code snippets you can use directly on your machine.

Workflow Graph

Below workflow graph shows the components to successfully complete the migration:

WordPress catalogs explanation

wp-content = themes, plugins, uploads – main area where WordPress functionality and design are extended.

wp-config.php = database & security settings -because it contains sensitive information, protecting access to this file is crucial.

.htaccess = server-level rules —incorrect changes can break your entire site.

Docker compose stack

Below you can find Docker compose configuration file that I used for prototyping of WordPress blog:

📋
docker-compose.yml
services:
  # MariaDB Database Service
  mariadb:
    image: mariadb:10.6.4-focal
    container_name: mariadb
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
      TZ: Europe/Warsaw
    expose:
      - 3306
      - 33060
    volumes:
      - mariadb_data:/var/lib/mysql
    networks:
      - wordpressnet

  wordpress:
    image: wordpress:latest

    ports:
      - 8082:80
    restart: always
    environment:
      - WORDPRESS_DB_HOST=mariadb
      - WORDPRESS_DB_USER=wordpress
      - WORDPRESS_DB_PASSWORD=wordpress
      - WORDPRESS_DB_NAME=wordpress
    volumes:
      - wordpress_data:/var/www/html
    networks:
      - wordpressnet

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: phpmyadmin
    restart: always
    environment:
      PMA_ARBITRARY: 1
      TZ: Europe/Warsaw
    ports:
      - "8081:80"
    depends_on:
      mariadb:
        condition: service_started
    networks:
      - wordpressnet


networks:
  wordpressnet:
    driver: bridge
    name: wordpress_bridge
    ipam:
      config:
        - subnet: 172.33.0.0/24


volumes:
  mariadb_data:
  wordpress_data:

Checking running Docker compose stack

Copy to clipboard
# 1.Check existing docker-compose stack
docker-compose ls | grep wordpress

# 2.Check running docker compose processes
docker-compose ps

# 3.Check running docker containers
docker ps -a | egrep 'wordpress|phpmyadmin|mariadb'
## 1
Docker-host:~/wordpress_stack-compose$ docker-compose ls |  grep wordpress
wordpress_stack-compose   running(3)          /home/dwojciech/wordpress_stack-compose/docker-compose.yaml

## 2
Docker-host:~/wordpress_stack-compose$ docker-compose ps

NAME                                  IMAGE                   COMMAND                  SERVICE      CREATED       STATUS      PORTS
mariadb                               mariadb:10.6.4-focal    "docker-entrypoint.s…"   mariadb      5 weeks ago   Up 3 days   3306/tcp, 33060/tcp
phpmyadmin                            phpmyadmin/phpmyadmin   "/docker-entrypoint.…"   phpmyadmin   5 weeks ago   Up 3 days   0.0.0.0:8081->80/tcp, [::]:8081->80/tcp
wordpress_stack-compose-wordpress-1   wordpress:latest        "docker-entrypoint.s…"   wordpress    5 weeks ago   Up 3 days   0.0.0.0:8082->80/tcp, [::]:8082->80/tcp

## 3
Docker-host:~/wordpress_stack-compose$ docker ps -a | egrep 'wordpress|phpmyadmin|mariadb'
4f93c2b0f2a7   phpmyadmin/phpmyadmin             "/docker-entrypoint.…"   5 weeks ago    Up 3 days                   0.0.0.0:8081->80/tcp, :::8081->80/tcp                                          
7a56fe8d7896   mariadb:10.6.4-focal              "docker-entrypoint.s…"   5 weeks ago    Up 3 days                   3306/tcp, 33060/tcp                                                            
c2befafabfd1   wordpress:latest                  "docker-entrypoint.s…"   5 weeks ago    Up 3 days                   0.0.0.0:8082->80/tcp, :::8082->80/tcp     
wordpress stack
WordPress Stack displayed in portainer.io

Docker Compose Stack

You can see above that we can distinguish 3 running containers in docker compose stack:

  • mariadb (database)
  • wordpress
  • phpmyadmin (database manager)

IP address schema 

All of the containers in this stack are running in admin-defined network 172.33.0.0/24.

Volumes

mariadb & wordpress have persistent container volumes, name accrodingly mariadb_data & wordpress_data 

Backup Docker containers data

Step1. Create wp-backup folder

#  Create a backup folder and navigate into it
mkdir -p ~/wp-backup && cd ~/wp-backup

Step2. Create mariaDB SQL dump

# Dump the DB from the mariadb container
docker exec -i mariadb \
  mysqldump -u root -psecret --databases wordpress \
  --single-transaction --quick --routines --events --triggers \
  > wordpress.sql

Step3. Export WordPress Files and Config

Check existing volume names

Verify what are existing persistent volume names created by wordpress_stack run command.

Copy to clipboard
docker volume ls | grep wordpress_stack
dwojciech@Docker-host:~/wp-backup$ docker volume ls | grep wordpress_stack
local     wordpress_stack-compose_mariadb_data
local     wordpress_stack-compose_wordpress_data

In order to copy wordpress media and configuration we can use 2 methods:
Method 1. Run temporary helper container
Method 2. Use docker cp command

Method 1 is quite harder but if you are big fan of containers you will manage it without any problems. 🙂

Let me walk you through both methods.

Method1. Run temporary helper container

Copy wp-content

Copy to clipboard
# Archive wp-content from the named volume to the current host dir (~/wp-backup)
docker run --rm --network none --read-only \
  -v wordpress_stack-compose_wordpress_data:/var/www/html:ro \
  -v "$PWD":/backup \
  alpine sh -c 'cd /var/www/html && tar czf /backup/wp-content.tgz wp-content'

We start a short-lived alpine container just to read the volume and write a tar file, then throw that helper away.  

Syntax clarifications:

– – rm only removes the temporary helper container after it finishes.
It does not stop or remove your running WordPress, MariaDB, or phpMyAdmin containers, and it does not delete the volume.

– – network none completely isolates container from networking, who needs network for temporary container ?!

– – read-only means – temporary container cannot modify any files inside its root filesystem (more security safeguard)

-v allows to mount a volumes to temporary container
volume1 (persistent): wordpress_stack-compose_wordpress_data :maps to: /var/www/html
volume2 (helper): maps ~/wp-backup/ to /backup of alpine container

 Copy wp-config

Copy to clipboard
# Copy wp-config.php into the current host dir (~/wp-backup)
docker run --rm --network none --read-only \
  -v wordpress_stack-compose_wordpress_data:/var/www/html:ro \
  -v "$PWD":/backup \
  alpine sh -c 'cp /var/www/html/wp-config.php /backup/wp-config.php'

 Copy .htaccess

Copy to clipboard
# Copy .htaccess into the current host dir (~/wp-backup)
docker run --rm --network none --read-only \
  -v wordpress_stack-compose_wordpress_data:/var/www/html:ro \
  -v "$PWD":/backup \
  alpine sh -c 'cp /var/www/html/.htaccess /backup/.htaccess'

Method2. Use docker cp command

Copy to clipboard
# docker cp syntax
docker cp <Wordpress CONTAINER_NAME or ID>:SOURCE_PATH DEST_PATH

Copy wp-content

Copy to clipboard
#Example
cd ~/wp-backup
docker cp c2befafabfd1:/var/www/html/wp-content .

Copy wp-config.php

Copy to clipboard
#Example
cd ~/wp-backup
docker cp c2befafabfd1:/var/www/html/wp-config.php .

Copy .htaccess

Copy to clipboard
#Example
cd ~/wp-backup
docker cp c2befafabfd1:/var/www/html/.htaccess .

Step4. Organize Backup Files into wp-backup catalog

Now it’s time to prepare migration notes that will help us to identify specific Docker parameters to be replaced in the Lightsail instance configuration.

touch migrationNotes.txt
echo Current WordPress URL:http://$(hostname -I | awk '{print $1}'):$(docker port c2befafabfd1 | grep "0.0.0.0" | awk -F: '{print $2}') > migrationNotes.txt

cat migrationNotes.txt
#Example
## Current WordpPress URL:http://nexthopcloud.com

Your backup catalog should already have 5 objects:

  • wordpress.sql
  • wp-config.php
  • wp-content
  • .htaccess
  • migrationNotes.txt
# Verify that all files are within backup folder
cd ~/wp-backup
tree -a -L 1

├── .htaccess
├── migrationNotes.txt
├── wordpress.sql
├── wp-config.php
└── wp-content

2 directories, 4 files

Now it’s time to archive wp-backup catalog

# Archive catalog 
## -z: compress archive with gzip;
## -c: create a new archive;
## -v: verbose mode to show progress
## -f: speciy archive file name
tar -zcvf wp-backup.tar.gz wp-backup

# Check file spaces for comparison
du -sh wp-backup
#Example: 317M    wp-backup

du -sh wp-backup.tar.gz
#Exammple: 89M     wp-backup.tar.gz

Configure Lightsail

Lightsail Instance Sizing

When choosing a Lightsail instance size, it’s usually best to start small and scale up as your needs grow. Beginning with a smaller instance keeps costs low and makes it easier to adjust resources later. If your application outgrows the initial setup, you can seamlessly move to a larger Lightsail instance or even migrate to more advanced AWS services such as EC2 or ECS for greater flexibility and performance.

1GB memory, 2vCPU, 40GB SSD is sufficient + notice the First 90 days are free!

lightsail sizing

Instance Static IP

As a prerequisite create and attach static public IP to your Lightsail Intance.
AWS documentation clearly explains this process, check out here: 
Amazon Lightsail User Guide Create and attach a static IP to your Lightsail instance

# Use api request to get your current public IP address
curl https://ipinfo.io/ip

# -> save Public IP result

 

Point DNS A record to Public IP of Lightsail Instance

The prerequisite is to have a domain already purchased. In my case I registered a domain in Cloudflare.

Here is step-by-step procedure of A Record configuration:
1. Log in to Cloudflare and open your domain’s dashboard.
2. Go to the DNS section.
3. Click Records.
dns record
4. Click Add Record.
dns record add
5. Choose A as the record type.
6. In the Name field, enter @ (for the root domain, e.g., nexthopcloud.com).
7. In the IPv4 address field, paste the Public IP address of your Lightsail instance.
8. Set Proxy status to Proxied (orange cloud) if you want Cloudflare’s caching, CDN, and security features.
a root record lightsail
8. Click Save.

Step5. Copy Backup to Lightsail

#Modify key privileges
chmod 400 ~/.ssh/LightsailDefaultKey-eu-central-1.pem

# Login to Lightsail instance 
## where PublicIP = PublicIP of your Lightsail instance
ssh -i ~/.ssh/LightsailDefaultKey-eu-central-1.pem bitnami@<PublicIP>

Download Default Key from Lightsail

download private key

 

Upload private key to Docker-host

I’m using WinSCP to upload downloaded .pem key to Docker-host instance.
Private key should be located in hidden folder ~/.ssh/ and have strictly configured privileges.

scp transfer

You should be able to login and see similar welcome screen:

bitnamiwordpress welcome screen

Copy wp-backup catalog from Docker-host to Lightsail instance.

Docker-host path: ~/wp-backup.tar.gz
Lightsail path: /opt/bitnami/wordpress/wp-backup.tar.gz

# Copy folder from Docker-host to Lightsail instance using scp protocol
## where PublicIP = PublicIP of your Lightsail instance
scp -i ~/.ssh/LightsailDefaultKey-eu-central-1.pem \
~/wp-backup.tar.gz bitnami@<PublicIP>:/opt/bitnami/wordpress/wp-backup.tar.gz
#  Output
## wp-backup.tar.gz                                    100%   89MB  12.0MB/s   00:07

Step6.Extract WordPress Files

In the next step we will extract sql database dump as well as WordPress configuration files fom wp-backup.tar.gz archive.

# Extract catalog
## -x: extract
## -v: verbose mode to show progress
## -f: specify archite file name
sudo tar -xvf wp-backup.tar.gz
cd wp-backup
ls -l
#  Example
## total 6968
## -rw-rw-r-- 1 bitnami bitnami      47 Sep 14 11:18 migrationNotes.txt
## -rw-rw-r-- 1 bitnami bitnami 7115469 Sep  9 16:45 wordpress.sql
## -rw-r--r-- 1 bitnami bitnami    5919 Jul 28 17:16 wp-config.php
##drwxr-xr-x 7 bitnami bitnami    4096 Sep 15 17:07 wp-content

#Prepare paths to important migration files
sql="/opt/bitnami/wordpress/wp-backup/wordpress.sql"
echo $sql
wpcontent="/opt/bitnami/wordpress/wp-backup/wp-content"
echo $wpcontent
wpconfig="/opt/bitnami/wordpress/wp-backup/wp-config.php"
echo $wpconfig

Step7. Recreate WordPress Database

Check your database password and perform first login:

# show the Bitnami app credentials (contains the DB/root password)
sudo cat /home/bitnami/bitnami_credentials
# password value should be displayed

#Connect to your DB
/opt/bitnami/mariadb/bin/mariadb -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 5568
Server version: 11.8.2-MariaDB Source distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current 
#Verify configured databases
SHOW DATABASES;
# Remove existing bitnami_wordpress database
DROP DATABASE bitnami_wordpress;
# Create bitnami_wordpress database from the scratch
CREATE DATABASE bitnami_wordpress;
# Exit mysql session
EXIT;

Step8. Import Database Backup

# Make a backup of your original dump first
cp /opt/bitnami/wordpress/wp-backup/wordpress.sql /opt/bitnami/wordpress/wp-backup/wordpress_original.sql

# Edit the dump file
sudo nano /opt/bitnami/wordpress/wp-backup/wordpress.sql

Changes to make in wordpress.sql dump file

Find all ‘wordpress‘ phrases:

--
-- Current Database: `wordpress`
--

CREATE DATABASE /*!32312 IF NOT EXISTS*/ `wordpress` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `wordpress`;

Replace with bitnami_wordpress:

--
-- Current Database: `bitnami_wordpress`
--

CREATE DATABASE /*!32312 IF NOT EXISTS*/ `bitnami_wordpress` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `bitnami_wordpress`;
## Verify Changes
head -25 wordpress.sql | grep -E "(Database|CREATE DATABASE|USE)"

Import wordpress sql dump into mariadb on Lightsail instance

# Import modified dump file
/opt/bitnami/mariadb/bin/mariadb -u root -p < $sql

# Verify the import
/opt/bitnami/mariadb/bin/mariadb -u root -p
SHOW DATABASES;
USE bitnami_wordpress;
SHOW TABLES;
SELECT  option_name, option_value FROM wp_options WHERE option_name IN ('siteurl', 'home');
EXIT;

Step9. Import WordPress Content

# Backup current wp-content
sudo cp -r /opt/bitnami/wordpress/wp-content /opt/bitnami/wordpress/wp-content-original

# Remove default content
sudo rm -rf /opt/bitnami/wordpress/wp-content/uploads
sudo rm -rf /opt/bitnami/wordpress/wp-content/plugins/*
sudo rm -rf /opt/bitnami/wordpress/wp-content/themes/*

# Copy your content including hidden files
sudo cp -r $wpcontent/. /opt/bitnami/wordpress/wp-content/

# Fix permissions
sudo chown -R bitnami:daemon /opt/bitnami/wordpress/wp-content
sudo chmod -R 755 /opt/bitnami/wordpress/wp-content
sudo find /opt/bitnami/wordpress/wp-content -type f -exec chmod 644 {} \;

Step10. Update wp-config.php Settings

# Update WordPress URLs in wp-config.php
nano -l /opt/bitnami/wordpress/wp-config.php

Instead of default values add your domain DNS address in lines 106 & 107.
Example:
wp config snippet

Step11. Update URLs in the Database

Now it’s time to replace old Docker host IP and port with new DNS name in wordpress database. You are going to use wp search-replace utility.

cd /opt/bitnami/wordpress
# sudo wp search-replace 'old-string' 'new-string' --allow-root
## First Dry Run
sudo wp search-replace 'http://10.130.0.17:8082' 'https://nexthopcloud.com' --all-tables --allow-root --dry-run

# Replacement 
sudo wp search-replace 'http://10.130.0.17:8082' 'https://nexthopcloud.com' --all-tables --allow-root

## Check if any old URLs remained
sudo wp db search '10.130.0.17' --allow-root
# Verify if the values are updated
sudo wp option get siteurl --allow-root
sudo wp option get home --allow-root

Perform Direct Database Check and Fix (if needed)

# Login to DB
/opt/bitnami/mariadb/bin/mariadb -u root -p bitnami_wordpress
#SQL
-- Find all references to the old URL
SELECT * FROM wp_options WHERE option_value LIKE '%10.130.0.17:8082%';
SELECT * FROM wp_postmeta WHERE meta_value LIKE '%10.130.0.17:8082%';
SELECT * FROM wp_posts WHERE post_content LIKE '%10.130.0.17:8082%';

-- Update any found references (be careful with serialized data)
UPDATE wp_options SET option_value = REPLACE(option_value, 'http://10.130.0.17:8082', 'https://nexthopcloud.com') WHERE option_value LIKE '%10.130.0.17:8082%';
UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'http://10.130.0.17:8082', 'https://nexthopcloud.com') WHERE meta_value LIKE '%10.130.0.17:8082%';
UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://10.130.0.17:8082', 'https://nexthopcloud.com') WHERE post_content LIKE '%10.130.0.17:8082%';

Force Elementor to Regenerate everything

## Clear all Elementor data and force regeneration
wp option delete elementor_css_print_method --allow-root
wp option delete elementor_frontend_config --allow-root
wp option update elementor_clear_cache 1 --allow-root

## Force WordPress to regenerate permalinks
wp rewrite flush --allow-root

## Clear all caches
wp cache flush --allow-root

Step12. Restart Apache Service

# Restart Apache to ensure all changes take effect
sudo /opt/bitnami/ctlscript.sh restart apache

Accessibility Test

Ensure you remember credentials used previously on Docker container WordPress as the Lightsail default credentials will no longer work here.

wploginpage

In addition, several effective measures should be implemented to protect login page agains unuauthorized access and brute force attacks:

  1. Use Strong, unique passwords combining uppercase, lowercase letters, numbers, and special characters.
  2. Change the default WordPress login URL from common /wp-login.php or /wp-admin to custom URL, which helps reduce bot attacks.
  3. Limit login attempts to block IP addresses temporarily after repeated failed attempts, preventing brute force attacks.
  4. Enable two-factor authentication (2FA) requiring an additional verification step beyond the password for stronger security.
  5. Use SSL/TLS encryption for secure login data transmission.
  6. Harden the wp-config.php file and remove or disable unused user accounts, plugins, and themes.

Key takeaways

  • Docker-based WordPress migrated to AWS Lightsail.
  • DNS + static IP taken care of.
  • Minimal startup cost, reasonable performance.