CMS

Installing the ModX content management system on OpenResty

published on
ModX

Whilst you will find another article on this site about how to install ModX (Installing the MODX content management system) I decided to write this one for a few reasons.

First, whilst the differences between installing NGinx and OpenResty are fairly minor it is always easier when you can find something online which meets your needs more closely than alternatives using slightly different commands, file paths etc.

Second, this article includes a few minor changes and tries to further simplify the process for you.

Why ModX?

The ModX content management system gives you more flexibility to set up a site the way you want. You don't need to worry about having a theme that only allows you to make certain changes.

Adding content is also very easy to do and uses a powerful backend to power your unique web sites.

Whilst some other content management systems add a lot of bloated code to your site ModX doesn't do this. As a result your web site won't take as long to load as other systems and will run quickly and smoothly.

Let's get started installing ModX on OpenResty!

First, the intial setup

Run sudo apt install lsb-release ca-certificates apt-transport-https software-properties-common language-pack-en-base unzip at the command prompt to get some inital software installed.

Now we will install PHP

Run the following set of commands to get PHP installed before moving on to the configuration stage

sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php7.4-{common, cli, bz2, zip, zlib, curl, intl, mysql, snmp, memcached, imagick, gd, fileinfo, imap, ldap, soap, tidy, xml, simplexml, json, gmp, pspell, mbstring, opcache, fpm, ssh2, imap, redis, apcu, mcrypt, pdo, openssl}
sudo apt install ghostscript imagemagick

Next we configure PHP

First edit www.conf by running sudo nano /etc/php/7.4/fpm/pool.d/www.conf at the command line. Make sure these lines in the file look like the ones below.

listen = /run/php/php7.4-fpm.sock
listen.owner = www-data
listen.mode = 0660

Now edit the php.ini with sudo nano /etc/php/7.4/fpm/php.ini and make the following changes

output_buffering = Off
expose_php = off
max_execution_time = 60
max_input_time = 60
max_input_vars = 1000
memory_limit = 512M
display_errors = Off
display_startup_errors = Off
log_errors = On
html_errors = Off
post_max_size = 8M
upload_max_filesize = 2M
allow_url_fopen = Off
date.timezone = Europe/London
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.revalidate_freq=200
opcache.save_comments=1
cgi.fix_pathinfo=0
extension=php_openssl
extension=fileinfo
extension=pdo_mysql
zlib.output_compression On
zlib.output_compression_level 5

Once done make sure to restart PHP by running sudo systemctl restart php7.4-fpm.

Let's install MariaDB and set up a database

Enter sudo apt-get install mariadb-server to install MariaDB and enter sudo mysql_secure_installation to secure your new installation.

Now let us make sure that MariaDB will only listen to tthe localhost - making sure that remote computers cannot connect - by running sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf and making sure the following two lines are present

bind-address = 127.0.0.1
local-infile = 0

If either of these commands are not in the file make sure to add them next to each other.

Once this is done it is time to create the database that will be used later on when setting up ModX. Run sudo mysql -u root -p and type the following commands at the MariaDB prompt.

CREATE DATABASE modxdb CHARACTER SET utf8mb COLLATE utf8_general_ci;
CREATE USER 'modx'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON modxdb.* TO 'modx'@'localhost';
FLUSH PRIVILEGES;
exit

It is time to install OpenResty

Installing OpenResty takes a few commands. You may find it easier to copy and paste these as some are rather long.

cd ~
sudo apt install wget gnupg ca-certificates
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update
sudo apt install openresty openresty-resty openresty-restydoc openresty-opm openresty-zlib openresty-pcre luarocks
sudo systemctl enable openresty
sudo systemctl start openresty

Setting up OpenResty

Again - there are a lot of commands here - but you will have completed the initial setting up of OpenResty in no time.

Run the following commands

sudo mv /usr/local/openresty/nginx/conf/nginx.{conf,original}
sudo nano /usr/local/openresty/nginx/conf/nginx.conf

In this file add the following contents

user www-data;
worker_processes auto;
pid /usr/local/openresty/nginx/logs/nginx.pid;

events {
    worker_connections  1024;
    use                 epoll; 
    epoll_events        512;
    multi_accept        on;
}

http {
  server_tokens off;
  # change my_server to whatever text you wish to use
  # this can also go in the server block if you wish to change this per site
  #more_set_headers "Server: my_server";

  include       mime.types;
  default_type  application/octet-stream;
  charset utf-8;
  charset_types
    text/css
    text/plain
    text/vnd.wap.wml
    text/javascript
    text/markdown
    text/calendar
    text/x-component
    text/vcard
    text/cache-manifest
    text/vtt
    application/json
    application/manifest+json;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
     '$status $body_bytes_sent "$http_referer" '
     '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/openresty/access.log combined;
    error_log /var/log/openresty/error.log warn;

    client_body_buffer_size 16K;
    client_header_buffer_size 1m;
    client_max_body_size 8m;
    large_client_header_buffers 4 8k;

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;

    gzip            on;
    gzip_vary       on;
    gzip_proxied    any;
    gzip_comp_level 5;
    gzip_buffers    16 8k;
    gzip_min_length 256;
    gzip_types
      application/atom+xml
      application/geo+json
      application/javascript
      application/x-javascript
      application/json
      application/ld+json
      application/manifest+json
      application/rdf+xml
      application/rss+xml
      application/vnd.ms-fontobject
      application/wasm
      application/x-web-app-manifest+json
      application/xhtml+xml
      application/xml
      font/eot
      font/otf
      font/ttf
      image/bmp
      image/svg+xml
      text/cache-manifest
      text/calendar
      text/css
      text/javascript
      text/markdown
      text/plain
      text/xml
      text/vcard
      text/vnd.rim.location.xloc
      text/vtt
      text/x-component
      text/x-cross-domain-policy;
    gzip_disable    "MSIE [1-6]\.";

    reset_timedout_connection on;
    keepalive_timeout 20s;
    keepalive_requests 30;
    client_header_timeout 10;
    client_body_timeout 10;
    send_timeout 10s;

    # aio on;
    directio 4m;
    directio_alignment 512;

    open_file_cache max=1000 inactive=30s; 
    open_file_cache_valid 30s; 
    open_file_cache_min_uses 4; 
    open_file_cache_errors on; 

    # Limits
    limit_req_log_level warn;
    limit_req_zone $binary_remote_addr zone=reqlimit:10m rate=10r/m;

    limit_conn_zone $binary_remote_addr zone=connlimit:100m;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

include ../sites/*;

}

Now run the following commands to continue the configuration process

sudo mkdir /usr/local/openresty/nginx/sites
sudo mkdir /var/log/openresty
sudo echo 'fastcgi_param HTTP_PROXY "";' | sudo tee -a /usr/local/openresty/nginx/conf/fastcgi.conf
sudo echo 'fastcgi_param HTTP_PROXY "";' | sudo tee -a /usr/local/openresty/nginx/conf/fastcgi_params
sudo nano /usr/local/openresty/nginx/sites/default.conf

Make sure to paste the following code into the open file

server {
    # Listen on port 80.
    listen 80 default_server;
    listen [::]:80 default_server;

    # The document root.
    root /usr/local/openresty/nginx/html/default;

    # Add index.php if you are using PHP.
    index index.html index.htm;

    # The server name, which isn't relevant in this case, because we only have one.
    server_name _;

    # When we try to access this site...
    location / {
        # ... first attempt to serve request as file, then as a directory,
        # then fall back to displaying a 404.
        try_files $uri $uri/ =404;
    }

    # Redirect server error pages to the static page /50x.html.
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root /usr/local/openresty/nginx/html;
    }
}

Continuing on with the setup run the following two commands

sudo mkdir /usr/local/openresty/nginx/html/default
sudo nano /usr/local/openresty/nginx/html/default/index.html

and add the following line into the blank editor window

<html><head></head><body</body></html>

We are now getting a bit closer to finishing setting up OpenResty. We need to create another file by running sudo nano /usr/local/openresty/nginx/sites/[domain].conf (where [domain] is your own domain name and adding the following code

server {
  listen 80;
  listen [::]:80;
  server_name [domain].com www.[domain].com;
  root /usr/local/openresty/nginx/html/[domain];
}

Lastly run the following set of commands which will get OpenResty up and running ready for us to install our copy of ModX.

sudo mkdir /usr/local/openresty/nginx/html/[domain]
sudo chown -R www-data:www-data /usr/local/openresty/nginx/html/[domain]
sudo chmod -R 750 /usr/local/openresty/nginx/html/[domain] 
sudo systemctl reload openresty

It is time to secure our domain

It is always a good idea to set up a SSL / TLS certificate for your site. By running the following commands you will have one set up in no time.

sudo apt-get install snapd
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot 
sudo certbot certonly --webroot -w /usr/local/openresty/nginx/html/[domain] -d [domain] -d www.[domain]

Make sure to make a note of where your certificates have been saved.

Updating the ModX server block

The initial server block, telling OpenResty where to find your site was only a basic stub in order to get a working SSL / TLS certificate. We now need to replace this with the actual server block we want to use with our ModX installation.

To do this run the following two commands and enter the code below into the blank file

sudo rm /usr/local/openresty/nginx/sites/[domain].conf
sudo nano /usr/local/openresty/nginx/sites/[domain].conf

#this will redirect any non-https requests to https://www
server {
  listen 80;
  listen [::]:80;
  server_name [domain] www.[domain];
  # Allow access to the ACME Challenge for Let's Encrypt <- <3
  location ~ /\.well-known\/acme-challenge {
    allow all;
  }
  return 301 https://www.[domain]$request_uri permanent;
}

#this wil redirect any https://... to https://www...
server {
  listen 443;
  listen [::]:443;
  server_name [domain];
  ssl_certificate /etc/letsencrypt/live/[domain]/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/[domain]/privkey.pem;
  return 301 https://www.[domain]$request_uri permanent;
}

#this is the https://www... configuration
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name www.[domain];
  root /usr/local/openresty/nginx/html/[domain];

  index index.php;

  access_log /var/log/openresty/[domain]_access.log combined;
  error_log /var/log/openresty/[domain]_error.log warn;

  charset utf-8;
  source_charset utf-8;

  ssl_certificate /etc/letsencrypt/live/[domain]/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/[domain]/privkey.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
  ssl_session_tickets off;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  ssl_stapling on;
  ssl_stapling_verify on;

  # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
  # or just simply openssl dhparam 2048 > /path/to/dhparam
  ssl_dhparam /path/to/dhparam;

  location = /favicon.ico { 
    log_not_found off; 
    access_log off;
  }

  location = /robots.txt { 
    log_not_found off;
    access_log off;
    allow all;
  }

  # Httproxy vulnerability
  proxy_set_header Proxy "";

  # Request headers for overwriting
  proxy_set_header X-Original-URL "";
  proxy_set_header X-Rewrite-URL "";
  proxy_set_header X-Host "";
  proxy_set_header X-Forwarded-Server "";
  proxy_set_header X-HTTP-Host-Override "";
  proxy_set_header Forwarded "";

  # Prevent Information leaks
  proxy_hide_header X-Powered-By;
  proxy_hide_header Server;
  proxy_hide_header X-AspNetMvc-Version;
  proxy_hide_header X-AspNet-Version;

  # http://blog.portswigger.net/2017/07/cracking-lens-targeting-https-hidden.html
  proxy_set_header clientIPAddress "";
  proxy_set_header x-forwarded-for "";
  proxy_set_header client-ip "";
  proxy_set_header forwarded "";
  proxy_set_header from  "";
  proxy_set_header referer "";
  proxy_set_header x-client-ip "";
  proxy_set_header x-originating-ip "";
  proxy_set_header x-wap-profile "";

  # http security headers
  add_header X-Content-Type-Options nosniff;
  add_header X-Frame-Options DENY;
  add_header X-XSS-Protection "1; mode=block";
  add_header Referrer-Policy strict-origin-when-cross-origin;
  add_header X-Permitted-Cross-Domain-Policies none;

  # Add Security cookie flags 
  proxy_cookie_path ~(.*) "$1; SameSite=strict; secure; httponly";

  add_header Content-Security-Policy "base-uri https://www.[domain]; default-src https:; script-src https: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; child-src https://www.[domain]";

  add_header Permissions-Policy "accelerometer=(self), ambient-light-sensor=(self), autoplay=(self), battery=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), document-domain=(self), encrypted-media=(self), execution-while-not-rendered=(self), execution-while-out-of-viewport=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), navigation-override=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), web-share=(self), xr-spatial-tracking=(self)";

  # only add this line once you are sure everything works
  #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;" always;

  # add_header Allow "GET, POST, HEAD";
  if ($request_method !~ ^(GET|HEAD|POST)$ ) {
      return 444;
  }

  # Allow access to the ACME Challenge for Let's Encrypt <- <3
  location ~ /\.well-known\/acme-challenge {
    allow all;
  }

  error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 426 428 429 431 444 449 450 451 500 501 502 503 504 505 506 507 508 509 510 511 /error.html;
  location /error.html {
    internal;
  }
    limit_conn connlimit 1000; # Simultaneous Connections

        # the MODX part
        location @modx-rewrite {
            rewrite ^/(.*)$ /index.php?q=$1&$args last;
        }

        # only enable this caching section once your site is live
        # 1y might be too long and 7d, 1w etc might be better
        #location ~* \.(?:ico|css|js|jpe?g|png|gif|svg|pdf|mov|mp4|mp3|woff|woff2|otf|ttf)$ {
        #  expires 1y;
        #  add_header Pragma public;
        #  add_header Cache-Control "public";
        #  gzip_vary on;
        #}

        # add caching to a particular directory
        #location /assets {
          # this adds an Expires header and a Cache-Control header
        #expires 1y;
        #}

        location = /robots.txt {
          try_files $uri $uri/ @modx-rewrite;
        }

        location /autodiscover/autodiscover.xml {
          return 404;
        }

        # block PHP execution from the listed directory
        #location ~ /assets/uploads/.*\.php$ {
        #  return 403;
        #}

        location / {
            try_files $uri $uri/ @modx-rewrite;
        }

        # protect parts of MODX
        location ~ ^/(\.(?!well_known)|_build|_gitify|_backup|core|config.core.php) {
          rewrite ^/(\.(?!well_known)|_build|_gitify|_backup|core|config.core.php) /index.php?q=doesnotexist;    
        }

        location ~ \.php$ {
                try_files $uri =404;
                fastcgi_split_path_info ^(.+\.php)(.*)$;
                fastcgi_pass   unix:/run/php/php7.4-fpm.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                include /usr/local/openresty/nginx/conf/fastcgi_params;
                fastcgi_ignore_client_abort on;
                fastcgi_param  SERVER_NAME $http_host;
        }

}

That is a lot of text to copy! This is set up to make sure that all requests for your domain name go to https://www.[domain]. If that isn't what you want then feel free to make the necessary changes to your server block.

The block above also contains a lot of instructions to improve the security of your site. You will also notice the following

  • There is a reference to dhparam which you need to take care of
  • The Strict-Transport-Security should only be uncommented once you know that your site will only ever be accessed over https.
  • The cache instructions shown above are all commented out. It is best to leave them this way until you have finished editing your site and it is live, or, ready for the public to view.

Once you have managed to get through this file and made all the changes you need to make sure to run sudo systemctl restart openresty to activate your new server block.

Now we can install ModX

By running the following commands you will download the latest version of ModX (at the time of writing), uncompress it, copy all the files you need into the webroot, set up any extra files or permissions and clean up after yourself.

cd ~
wget https://modx.com/download/direct/modx-3.0.1-pl.zip
unzip modx-3.0.1-pl.zip
cd modx-3.0.1-pl
sudo mkdir /usr/local/openresty/nginx/html/[domain]
sudo cp -r * /usr/local/openresty/nginx/html/[domain]
cd ..
rm -rf modx-3.0.1-pl modx-3.0.1-pl.zip
sudo touch /usr/local/openresty/nginx/html/[domain]/error.html
sudo touch /usr/local/openresty/nginx/html/[domain]/core/config/config.inc.php
sudo chown -R www-data:www-data /usr/local/openresty/nginx/html/[domain]
sudo chmod -R 755 /usr/local/openresty/nginx/html/[domain]

Now to finally setup Modx

Visit [domain]/setup in your browser and follow the installation procedure. At the end make sure that the option to delete the setup folder is ticked.

Congratulations!

You now have a fully working version of ModX set up using OpenResty and just waiting for you to set your creative juices flowing.

There is a lot to learn yet and you can find some initial instructions at https://docs.modx.com.

I hope to be bringing you more ModX articles in the future as I learn and develop my skills.