Category: Database

  • WordPress Deployment with Nginx Reverse-Proxy and HTTPS for Multiple Domains

    WordPress Deployment with Nginx Reverse-Proxy and HTTPS for Multiple Domains

    About 10 years ago, a bunch of Frameworks was gaining their popularities, while PHP was considered dying. However, up to 2025, WordPress is still the technology behind over 40% of the websites. Powered by PHP 8.x and the Gutenberg block system. WordPress is now a full featured modern CMS. This also became the motivation for me to get into the world of WordPress once again.

    For saving server costs, it might be a good idea to deploy different WordPress instances on the same physical machine. With the Nginx domain name configuration, it is quite easy to serve WordPress for multiple domains.

    Prerequisites

    Before starting, some preparations on the server should be done. More precisely, the following services should be properly setup and configured:

    • Docker
    • Certbot
    • Nginx

    Overview

    The whole system can be devided into 3 layers:

    1. Database: in my case, I’d like to use MariaDB. One can choose any Database supported by WordPress.
    2. Application: of course it is WordPress, and multiple instances can be deployed.
    3. Reverse-proxy: in my case, I’m using Nginx.

    In my setup, database and WordPress are being containerized. Only Nginx is running on the server as a classic daemon. There is no specific reason why I am doing this. The server was there for a while already, and the Nginx has already been installed. If next time I have to do everything once again, I will probably also containerize Nginx.

    Setup Network

    Since database and WordPress instances are all containerized, it would be great if we have a private network to connect both.

    Use the following command to create a network:

    docker network create [NETWORK_NAME]
    docker network ls
    

    On success, the newly created network shall be listed:

    NETWORK ID   NAME             DRIVER    SCOPE
    ...          ...              ...       ...
    ...          [NETWORK_NAME]   bridge    local
    ...          ...              ...       ...
    

    Note that the name [NETWORK_NAME] is only a placeholder, it should be named properly.

    If later we want to remove the network, we could simply do:

    docker network rm [NETWORK_NAME]
    

    Setup Database

    This can be easily done by writing a compose file like this:

    services:
    
      database:
        image: mariadb:latest
        restart: always
        environment:
          - MARIADB_ROOT_PASSWORD_HASH=[PASSWORD_HASH]
        volumes:
          - ./database_data:/var/lib/mysql:Z
        networks:
          - [NETWORK_NAME]
    
    networks:
      [NETWORK_NAME]:
        external: true
    

    Here, [PASSWORD_HASH] is a placeholder, the hash can be generated by PASSWORD() function in SQL. Or one could also find a generator online.

    The path /var/lib/mysql is the path to the database content. To create a volume mapped to the local storage will probably simplify the backup process.

    Since we have created a network in the previous step, and the WordPress will also be setup in the same network. So the port mapping is not necessary.

    To start the database application, use docker compose command:

    docker compose up -d
    

    To login to the database, we have to login to a temporary container first, and run mariadb command connecting with the hostname database:

    docker run -it --network [NETWORK_NAME] --rm mariadb mariadb -h database -u [USERNAME] -p
    

    As usual [USERNAME] here is the placeholder.

    If in the future, we need to stop the container, use following command:

    docker compose down
    

    And then we can start with creating database for the WordPress instances:

    CREATE DATABASE [DB_NAME];
    CREATE USER '[DB_USER]'@'%' IDENTIFIED BY '[DB_PASSWD]';
    GRANT ALL PRIVILEGES ON [DB_NAME].* TO '[DB_USER]'@'%';
    

    Here, [DB_NAME], [DB_USER] and [DB_PASSWD] are placeholders.

    Setup WordPress Instances

    Similar to the database setpu, WordPress setup can also be done with docker compose:

    services:
      wordpress:
        image: wordpress
        restart: always
        ports:
          - [WP_PORT]:80
        environment:
          WORDPRESS_DB_HOST: database
          WORDPRESS_DB_USER: [DB_USER]
          WORDPRESS_DB_PASSWORD: [DB_PASSWD]
          WORDPRESS_DB_NAME: [DB_NAME]
        volumes:
          - ./wordpress_data:/var/www/html
          - ./wordpress_config/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
        networks:
          - [NETWORK_NAME]
    
    networks:
      [NETWORK_NAME]:
        external: true
    

    Since my Nginx is a classic service daemon, so we have to map the container port 80 to host port [WP_PORT] so that the Nginx can access the WordPress instance.

    Two volumes are mapped to the local storage. The first one is generally for the WordPress files; the second one is for setting up the file upload limit. By editing ./wordpress_config/uploads.ini like this:

    upload_max_filesize = 100M
    post_max_size = 100M
    

    One can increase the max size of uploaded files.

    Just like all the other docker compose applications, using following command to start the WordPress instance:

    docker compose up -d
    

    And use the following command to stop it:

    docker compose down
    

    Setup Nginx

    First of all, we need an HTTPS certificate. We can create one using certbot easily:

    certbot --nginx -d [DOMAIN_NAME] -d www.[DOMAIN_NAME]
    

    And then create the site configuration files for Nginx under /etc/nginx/sites-available:

    server {
        listen  [::]:443 ssl;
        listen  443 ssl;
    
        ssl_certificate      /etc/letsencrypt/live/[DOMAIN_NAME]/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/[DOMAIN_NAME]/privkey.pem;
        include              /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam          /etc/letsencrypt/ssl-dhparams.pem;
    
        server_name  [DOMAIN_NAME] www.[DOMAIN_NAME];
    
        client_max_body_size  100M;
    
        location / {
            proxy_pass          http://localhost:[WP_PORT]/;
            proxy_http_version  1.1;
            proxy_set_header    Host $host;
            proxy_set_header    X-Real-IP $remote_addr;
            proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header    X-Forwarded-Host $server_name;
            proxy_set_header    X-Forwarded-Proto $scheme;
            proxy_redirect      off;
        }
    }
    
    server {
        listen  80;
        listen  [::]:80;
    
        server_name  [DOMAIN_NAME] www.[DOMAIN_NAME];
    
        if ($host = www.[DOMAIN_NAME]) {
            return  301 https://$host$request_uri;
        }
    
        if ($host = [DOMAIN_NAME]) {
            return  301 https://$host$request_uri;
        }
    
        return  404;
    }
    

    This setup will use the certificate created by certbot, also redirect all the HTTP requests to HTTPS, and redirect all the HTTPS requests as HTTP request to the WordPress instance.

    And since we have the server_name configuration on line 10, it will only accept queries which match the configured domain. In this way, we can serve multiple domains on a single physical server.

    Then we create a symbol link in the /etc/nginx/sites-enabled folder:

    ln -s /etc/nginx/sites-available/[SITE_CONF_NAME] /etc/nginx/sites-enabled/[SITE_CONF_NAME]
    

    And finally restart the Nginx service:

    systemctl restart nginx
    

    If we visit website now, the WordPress setup page will be displayed.

    Some Discussions

    • I have used separated docker compose file instead of using a single one, because I’d like to have the ability to group the containers according to their usage so that they can be started and stopped separately. In this case, all docker compose file contains exactly one service, which is a coincidence.
    • In the database, we can probably restrict the user scope to database, for this is the only source where the database queries could come from. This might increase the security.