Intro
So you’ve stubbled upon this article as you couldn’t find a single source that describes the full process. Welcome to the club, this is a journey and a guide as much to myself as I’ll inevitably forget how to do this in a couple of months after writing this. This guide does not claim to be the best and 100% secure way to do this. This is a way to do this, use at your own risk and adjust to your needs.
This guide uses
- Ubuntu 22.04 VPS
- Docker
- Nginx Docker image
- jwilder/nginx-proxy Docker image
- nginxproxy/acme-companion Docker image
- WordPress Docker image (:php8.2-fpm)
- Mysql Docker image
Part 1: Docker
Follow this official guide.
In case the link no longer works, the short version is as follows.
Out with the old:
$ for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done
Prepare for the new:
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg –dearmor -o /etc/apt/keyrings/docker.gpg
$ sudo chmod a+r /etc/apt/keyrings/docker.gpg
$ echo \
“deb [arch=”$(dpkg –print-architecture)” signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
“$(. /etc/os-release && echo “$VERSION_CODENAME”)” stable” | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
In with the new:
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Check the result:
$ sudo docker run hello-world
Running docker as sudo might not be the best idea so consider:
$ sudo usermod -a -G docker username
where username is your actual username. As incorrectly executed usermod can mess up your user try logging in from a new terminal and executing:
$ groups
You should see docker added you your group list. You can now close the old terminal and continue in the new. If you could not log in in a new terminal don’t close the old one before resolving the usermod mess.
Part 2: Nginx reverse proxy with TLS
Here are the official docs for jwilder/nginx-proxy and nginxproxy/acme-companion but I’ve taken some detours as I focus on using docker compose.
We’ll be using /var/www/ as the base dir for our web apps. Also be mindful of file and directory permissions if errors occur.
So let us
$ mkdir /var/www/nginx-proxy
$ cd /var/www/nginx-proxy
$ nano docker-compose.yml
Which in turn should contain something like this
version: "3.5"
services:
nginx-proxy:
image: jwilder/nginx-proxy
container_name: nginx-proxy
restart: always
environment:
- DEFAULT_EMAIL=me@example.com
ports:
- "80:80"
- "443:443"
volumes:
- certs:/etc/nginx/certs
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- ./config/client_max_body_size.conf:/etc/nginx/conf.d/client_max_body_size.conf
- /var/run/docker.sock:/tmp/docker.sock:ro
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
restart: always
environment:
- NGINX_PROXY_CONTAINER=nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
default:
external: true
driver: bridge
name: nginx-proxy
volumes:
conf:
vhost:
driver: local
driver_opts:
type: none
o: bind
device: "${PWD}/vhosts.d"
html:
certs:
acme:
The example above could use some notes
- restart: always – we want this container to always be up.
- Do change your e-mail.
- Use both ports 80 and 443 as we’ll be setting up Letsencrypt.
- certs, vhost and html volumes will be shared with acme-companion.
- ./config/client_max_body_size.conf will be mapped locally as we want our server to handle larger file uploads.
- acme-companion is our automatic TLS cerificate provide via Letsencrypt.
- NGINX_PROXY_CONTAINER=nginx-proxy if you change Nginx proxy container’s name, please adjust it here as well.
- We also define an nginx-proxy external network which will contain all the containers this reverse proxy will be able to point to.
- The vhost volume will be locally mounted for later per-vhost configuration options.
Let’s continue with creating the ./config/client_max_body_size.conf file
$ mkdir config
$ echo ‘client_max_body_size 16m;’ > ./config/client_max_body_size.conf
Feel free to change the 16 MB limit to something that suits you better. Relevant Nginx docs.
We also need to create vhosts.d/default file
$ mkdir vhosts.d
$ nano vhosts.d/default
And put these contents in:
## Start of configuration add by letsencrypt container
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
auth_request off;
allow all;
root /usr/share/nginx/html;
try_files $uri =404;
break;
}
## End of configuration add by letsencrypt container
$ docker network create nginx-proxy
$ docker compose up -d
Now, if you visit your server’s IP in a browser you should see a 503 Service Temporarily Unavailable which is good as our reverse proxy is now working. While this is fun and all we likely want it to proxy to somewhere though.
Part 3: A WordPress website
While this doesn’t need to be WordPress, it just has to be a Docker container and WP is conveniently packaged into one so I’ll use it as an example. There are of course the official docs which are a useful reference, be mindful of the environment variables.
While we are currently in /var/www/nginx-proxy that’s done for now so let’s:
$ mkdir /var/www/mywebsite.com
$ cd /var/www/mywebsite.com
$ nano docker-compose.yml
And let’s paste the following there
version: '3.5'
services:
nginx:
image: nginx:stable
restart: always
expose:
- 80
environment:
VIRTUAL_HOST: mywebsite.com,www.mywebsite.com
LETSENCRYPT_HOST: mywebsite.com,www.mywebsite.com
depends_on:
- wordpress
volumes:
- ./wordpress:/var/www/html
- ./nginx:/etc/nginx/conf.d
networks:
- nginx-proxy
wordpress:
build:
context: .
dockerfile: Dockerfile-wordpress-with-phpredis
image: wordpress:php8.2-fpm
restart: always
expose:
- 9000
environment:
WORDPRESS_DB_HOST: mywebsite-com-db
WORDPRESS_DB_USER: wordpress-user
WORDPRESS_DB_PASSWORD: super-secret-password
WORDPRESS_DB_NAME: a-database-name
WORDPRESS_REDIS_HOST: mywebsite-redis
WORDPRESS_REDIS_PORT: 6379
depends_on:
- mywebsite-com-db
volumes:
- ./wordpress:/var/www/html
- ./php-uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
networks:
- backend
mywebsite-com-db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: a-database-name
MYSQL_USER: wordpress-user
MYSQL_PASSWORD: super-secret-password
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
networks:
- backend
mywebsite-redis:
image: redis:latest
expose:
- 6379
restart: always
networks:
- backend
volumes:
db:
networks:
backend:
driver: bridge
nginx-proxy:
external: true
driver: bridge
~~ Now let’s unpack this massibe blob as there are aspects that require attention ~~
Notice the slight of hand and a departure from the official docs when we don’t build WordPress directly from the official image. Unfortunately it comes without Redis but we are not savages so we should add it.
$ nano Dockerfile-wordpress-with-phpredis
FROM wordpress:php8.2-fpm
RUN pecl install -f redis \
&& rm -rf /tmp/pear \
&& docker-php-ext-enable redis
Let us not forget the to connect our nginx container to the WordPress container.
$ mkdir nginx
$ nano nginx/wordpress.conf
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html index.htm;
client_max_body_size 16M;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#fixes timeouts
fastcgi_read_timeout 600;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
We should also increase PHP’s maximum size for file uploads. Keeping the size similar or equal to Nginx’s client_max_body_size 16M; is a good idea.
$ nano php-uploads.ini
upload_max_filesize = 16M post_max_size = 16M
So far this should work if you’re deploying a new website. In case you’re deploying one from a backup there are additional steps that are required so it works properly. Maybe another article?
And now we’re ready for
$ docker compose build wordpress
$ docker compose up -d
It will take a while to spin up and it will also take a while to obtain a cerificate, feel free to take a 5-10 minute break or so.
As Redis also needs configuration be sure to add this somewhere in the wp-config.php.
/**
* Redis config
*/
define( 'WP_REDIS_HOST', getenv_docker('WORDPRESS_REDIS_HOST', 'localhost') );
define( 'WP_REDIS_PORT', getenv_docker('WORDPRESS_REDIS_PORT', 6379) );
define( 'WP_REDIS_PASSWORD', getenv_docker('WORDPRESS_REDIS_PASSWORD', '') );
define( 'WP_REDIS_TIMEOUT', getenv_docker('WORDPRESS_REDIS_TIMEOUT', 1) );
define( 'WP_REDIS_READ_TIMEOUT', getenv_docker('WORDPRESS_READ_TIMEOUT', 1) );
define( 'WP_REDIS_DATABASE', getenv_docker('WORDPRESS_REDIS_DATABASE', 0) );
Don’t forget to install the Redis Object Cache plugin to acticate Redis in the WordPress admin panel.
Part 4: A Laravel application
In this case, a Laravel application running on Laradock