When I started the previous version of my blog site (joeplaa.com WordPress blog), I didn't knew how to host WordPress myself. So I took a subscription to a webhosting service at TransIP. They not only offered hosting, but also a one button WordPress installation. This is not something special, but a great way for people to get started.
Although the speed and the price were good, something was missing. I didn't need it, but I wanted more control. With more control I mean setting up advanced caching (Redis and Nginx) and installing the latest MySQL version (8.0).
That meant getting my own VPS. As I already researched and implemented this for jodiBooks, it was a logical choice to also use AWS for my blog. And as I couldn't find a proper guide I created this tutorial. One that is not based on the standard Bitnami image.
As far as I know AWS has two services that are suitable for hosting WordPress: EC2 and Lightsail. EC2 is the advanced option which comes with a lot of additional options. For a simple blog Lightsail is good enough, much simpler and cheaper.
Looking at pricing comparing Lightsail with a hosting solution is fairer as it isn't a full VPS. It doesn't have dedicated vCPU cores and network connections are shared. It does include data traffic and dns management. If you choose to use an EC2 instance, bare in mind that it needs to be extended with a Route 53 hosted zone ($0,50 per month) and you pay for data transfer (first 100GB per month are free).
In this guide I'll show you how to setup WordPress on a Lightsail instance. I will do that in 3 parts. The first 2 are essential, the last one optional but recommended.
Option | vCPU | MEM | Disk | Cost/month* |
---|---|---|---|---|
Webhosting TransIP | - | - | 10 GB | € 5.99 |
VPS TransIP | 1 | 1 GB | 50 GB | € 10.00 |
AWS Lightsail 1GB | 1 (10%) | 1 GB | 40 GB | $ 5.00 |
AWS EC2 t3a.micro | 2 (10%) | 1 GB | 40 GB | $ 10.22 |
*Pricing as of 23 February 2022
Before we can start with Lightsail, we have to get an account. You don't need to be a business to register with AWS, but you do need a credit card. When you successfully registered the account, you have to secure it.
Using the root account needs to be avoided and instead an admin account should be made. I have described how to do that here: Hosting ASP.NET apps on AWS part 4.
Now that we have our account and have logged in with our admin user, we can go to the Lightsail dashboard. Here we click on the Create instance button. I already had an instance running, but otherwise the dashboard would show a big Create button.
After clicking that button select the region closest to you or your intended visitors to make the latency of your website as low as possible. For me that is Frankfurt
. Optionally you can also select the availability zone, but for a single instance that doesn't matter.
Next we can select the OS-only, Linux/Unix
, and the OS type, Ubuntu 20.04
. If you want an easy installation and your application is listed, you can also select Apps + OS
. This will use a Bitnami image with everything preinstalled en configured.
Scrolling down further, we can select some optional items. For now we leave all of these settings on the default values. Further down, we can also select the instance size. WordPress will run fine on all of them. Our intended setup uses a little more memory, so we need at least the second (1 GB) option.
As the last step we enter the instance name.
After we click the Create instance button the instance will be initiated. This can take a few minutes. After that the instance will be accessible through the dashboard.
I wanted to move my domain to AWS. This is not necessary. You can also use any other registrar, as long as you can configure your DNS and nameserver settings. In the first part of Hosting ASP.NET apps on AWS part 7 I describe how to register and/or transfer a domain to Route 53. You can stop when you arrive at the "DNS management" heading. We'll do that in Lightsail.
In the Lightsail dashboard go to the Networking tab. Now click the Create static IP button.
Select the region. Use the same as your instance. I choose Frankfurt
.
Next we select the instance (I can't as all my instances already have a static IP attached). And we give it a name. Create the IP.
With the IP address created and the nameservers of the domain accessible, we now click Create DNS zone.
Enter the domain, for me that's joeplaa.com
and click Create DNS zone at the bottom of the page.
Enter the following records one by one:
Type | Subdomain | Resolves to |
---|---|---|
A |
@ |
<your static ip address> |
CNAME |
www |
<your main domain> |
Now scroll down. You'll find a list with the 4 nameservers for your DNS zone. Go to your domain registrar and change their nameservers to the Lightsail ones.
Something I didn't understand at first is that you had to configure these servers with your registrar, in my case Route 53. Once I knew, I still had to find out where and how to configure the nameservers in Route 53. Go to your "Registered domains" in the Route 53 dashboard and open the domain for the DNS zone we are configuring in Lightsail.
Now that the instance is running, we can connect to it through SSH. This will all be command line, so no nice GUI.
If you use Windows, install OpenSSH. On Mac or Linux you can use the terminal.
Check here how to connect.
Once you're connected start with updating the OS:
sudo apt update && sudo apt upgrade -y
Hint: Cheatsheet
We start with installing the firewall. In Ubuntu this is UFW, UncomplicatedFireWall.
sudo apt update && sudo apt install ufw
Now the first thing we do is configure the default settings. We want to allow all outgoing traffic and deny all incoming traffic.
sudo ufw default allow outgoing
sudo ufw default deny incoming
All incoming ports are closed now, but we want to be able to SSH (PuTTY) into the instance. To do that we need to configure that in the firewall. We do that by allowing port 22 to accept traffic. Find your IP address.
sudo ufw allow ssh from <ip address>
We also want our WordPress page to be accessible. To do that we have to allow traffic on ports 80 and 443.
sudo ufw allow http
sudo ufw allow https
Lastly we enable the firewall.
sudo ufw enable
You will be warned that enabling the firewall may disrupt existing ssh connections. We've opened that port, so we can type y and hit Enter.
Check the firewall settings with the following command:
sudo ufw status verbose
Now go to the Lightsail dashboard, click on your instance and select the Networking tab. Here you'll see another firewall. This is the firewall AWS provides. It sounds like doing the firewall in Linux is double the work and unnecessary, but better be safe and enable both. Again the ports 22
, 80
and 443
need to be open. For port 22 we enable the "Restrict to IP address" option and add our home/office address (https://whatsmyip.com/).
Preferably you only use the RAM of your instance, because that is the fastest option to store temporary data. As we only have 1 GB, this is quite tight. So the second option would be to create swap space on the local SSD. A Lightsail instance however doesn't have that, so we are forced to forgo on best-practices and use the attached drive which is essentially a network volume.
Source: How do I allocate memory to work as swap space in an Amazon EC2 instance by using a swap file?
AWS recommends creating swap space that is "2x the amount of RAM but never less than 32 MB". You can create a new volume (disk) specifically for swap, but I've added it to the main volume.
We will create a 2 GB swap space (64 blocks of 32 MB). In the command line type:
sudo dd if=/dev/zero of=/swapfile bs=32M count=64
Update the read and write permissions for the swap file:
sudo chmod 600 /swapfile
Set up a Linux swap area:
sudo mkswap /swapfile
Make the swap file available for immediate use by adding the swap file to swap space:
sudo swapon /swapfile
Verify that the procedure was successful:
sudo swapon -s
Enable the swap file at boot time by editing the /etc/fstab
file. Open the file in the editor:
sudo nano /etc/fstab
Add the following line at the end of the file (use arrow keys and paste the line with right-mouse button), save the file (ctrl+x), and then exit (y and hit Enter):
/swapfile swap swap defaults 0 0
When starting a new SSH session, Ubuntu will display the amount of used memory, but you can also see it by using top
or htop
. These can be opened by simply typing top
or htop
in the terminal.
Sources:
https://www.wpintense.com/author/davehilditch/
https://spinupwp.com/hosting-wordpress-yourself-nginx-php-mysql/
sudo apt update && sudo apt install software-properties-common
curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=mariadb-10.6
sudo add-apt-repository ppa:nginx/stable
sudo apt update && sudo apt install snapd fail2ban mariadb-server nginx php8.1-fpm php8.1-mysql php8.1-redis php8.1-common php8.1-curl php8.1-dom php8.1-imagick php8.1-mbstring php8.1-zip php8.1-intl redis
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Download zip file and extract:
cd ~
sudo apt install wget unzip -y
wget https://cdn.joeplaa.com/wordpress/lemp-config.zip
unzip lemp-config.zip
sudo cp lemp-config/etc/* /etc/ -R
rm -r lemp-config*
Update log location for Fail2ban:
sudo nano /etc/fail2ban/jail.d/wordpress.conf
Change <SITE>
to your site name.
[wordpress]
enabled = true
port = http,https
filter = wordpress
action = iptables-multiport[name=wordpress, port="http,https", protocol=tcp]
logpath = /var/log/nginx/<SITE>-access.log
maxretry = 3
bantime = 600
Enable site in Nginx:
sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/
Create a cache folder:
sudo mkdir /var/www/cache/wordpress -p
sudo chown www-data:www-data /var/www/cache/ -R
Change domain name(s) server_name
in Nginx config. We also change the log files to our name: demosalon-access.log
and demosalon-error.log
.
# This config file uses nginx fastcgi-cache
fastcgi_cache_path /var/www/cache levels=1:2 keys_zone=wordpress:100m inactive=60m;
server {
# Add your domain name(s)
server_name demosalon.nl www.demosalon.nl;
listen 443;
listen [::]:443;
index index.php index.htm index.html;
root /var/www/html;
# Logging
access_log /var/log/nginx/demosalon-access.log;
error_log /var/log/nginx/demosalon-error.log;
# Exclusions
include snippets/exclusions.conf;
# Security
include snippets/security.conf;
# Static Content
include snippets/static-files.conf;
# Fastcgi cache rules
include snippets/fastcgi-cache.conf;
include snippets/limits.conf;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-params.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
# Skip cache based on rules in snippets/fastcgi-cache.conf.
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Define memory zone for caching. Should match key_zone in fastcgi_cache_path above.
fastcgi_cache wordpress;
# Define caching time.
fastcgi_cache_valid 60m;
#increase timeouts
fastcgi_read_timeout 6000;
fastcgi_connect_timeout 6000;
fastcgi_send_timeout 6000;
proxy_read_timeout 6000;
proxy_connect_timeout 6000;
proxy_send_timeout 6000;
send_timeout 6000;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
}
server {
server_name demosalon.nl www.demosalon.nl;
listen 80;
}
Press ctrl+x, y and enter to save and close.
Check if you made no errors and restart Nginx and other services:
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl restart fail2ban
sudo systemctl restart mysql
sudo systemctl restart php8.1-fpm
sudo systemctl restart redis-server
Create a MySQL/MariaDB database. Log in to MariaDB:
sudo mysql
Create the database by running the following SQL commands one line at a time, including the ";".
wordpress_db
jodibooks
CREATE DATABASE wordpress_db DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'jodibooks'@'localhost' IDENTIFIED BY 'CHOOSEASTRONGPASSWORD';
FLUSH PRIVILEGES;
EXIT;
Get an SSL certificate with Certbot. Add both domains: with and without www
or just the one if you use a subdomain (e.g. blog.jodibooks.com):
sudo certbot --nginx
Verify the automatic changes to the Nginx config made by Cerbot:
sudo nano /etc/nginx/sites-available/default
It should now look like this (edit if necessary):
# This config file uses nginx fastcgi-cache
fastcgi_cache_path /var/www/cache levels=1:2 keys_zone=wordpress:100m inactive=60m;
server {
# Add your domain name(s)
server_name demosalon.nl www.demosalon.nl;
listen 443;
listen [::]:443;
index index.php index.htm index.html;
root /var/www/html;
# Logging
access_log /var/log/nginx/demosalon-access.log;
error_log /var/log/nginx/demosalon-error.log;
# Exclusions
include snippets/exclusions.conf;
# Security
include snippets/security.conf;
# Static Content
include snippets/static-files.conf;
# Fastcgi cache rules
include snippets/fastcgi-cache.conf;
include snippets/limits.conf;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-params.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
# Skip cache based on rules in snippets/fastcgi-cache.conf.
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Define memory zone for caching. Should match key_zone in fastcgi_cache_path above.
fastcgi_cache wordpress;
# Define caching time.
fastcgi_cache_valid 60m;
#increase timeouts
fastcgi_read_timeout 6000;
fastcgi_connect_timeout 6000;
fastcgi_send_timeout 6000;
proxy_read_timeout 6000;
proxy_connect_timeout 6000;
proxy_send_timeout 6000;
send_timeout 6000;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off; access_log off;
}
# ssl
ssl_certificate /etc/letsencrypt/live/demosalon.nl/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/demosalon.nl/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.demosalon.nl) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = demosalon.nl) {
return 301 https://$host$request_uri;
} # managed by Certbot
server_name demosalon.nl www.demosalon.nl;
listen 80;
return 404; # managed by Certbot
}
After editing check if you made no errors and restart Nginx and other services:
sudo nginx -t
sudo systemctl restart nginx
Browse to your website both through http and https: https://demosalon.nl
. In both cases you should get a 404 error. This means your Nginx setup is working.
Download and install WordPress:
wget https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
cp -r wordpress/ /var/www/html
rm -r wordpress latest.tar.gz
Link to database in wp-config.php
with the credentials from step 9:
sudo nano /var/www/html/wp-config.php
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'database_name_here' );
/** MySQL database username */
define( 'DB_USER', 'username_here' );
/** MySQL database password */
define( 'DB_PASSWORD', 'password_here' );
/** MySQL hostname */
define( 'DB_HOST', 'localhost' );
Add secrets in wp-config.php
. Generate them here: https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY', 'B)>D }8E]ZM|7XT72cP.;{Ux^y}!E&!L,_zb)Sj:}O`z5s *yK/4pW!@S4exDmy}');
define('SECURE_AUTH_KEY', 'Sfsl9c3UkGVz.G>W*y1<5|se<m|Yw8P*jpUW|zlwXc*m@=PCJ|*KG:SJGUY 1M!Q');
define('LOGGED_IN_KEY', 'Y;M*#U.-d}|dU|U33,0``E[Vx^EZ@^Z<yggv[!<`b5lyO7.|0++KblY:H3<C=mq6');
define('NONCE_KEY', 'mF;cPsy48{SO(Jz9Y%fHjoGILIANQ|5-^ErIaw.B,Tje*m:uj+|o$yW:-Bw:TmZ[');
define('AUTH_SALT', 'R4XfVmPo6Aq(T&6Rw1+-#tw;^Fj.^oZ=I5>,SLlC2/9qsy|,yf4z 4#6CNK(|i2{');
define('SECURE_AUTH_SALT', ' -vUqT|1Fh*IYV9!LMW&l6*jb/, sQ; G^U=1CW7)~_CPi}]T;WE/<_r%3lND-ii');
define('LOGGED_IN_SALT', 'M-p<i)0!_*3}]jNar G8|[#:t!N-Z&9r#N_-b!Y3KXECT<QN1 8ya3k$Ua4WCaW`');
define('NONCE_SALT', 'OC4@ J-j}lCX6-D?Cq(.W+YHf*PbI>^^oW|_P;|]o@xCNZGm~G*!0V(d73wxYDAD');
Run the install script: https://example.com/wp-admin/install.php