Code Locket Menu

Migrating doCMS from Apache to NGINX

Blog post created on 2023-05-14

Sys AdminContent Management SystemsWeb

Most of the websites we've built at dodify are powered by doCMS, a custom made Content Management Systems (CMS) that satisfied some specific needs common to our customers. The CMS is developed to work both on Windows and Linux based platforms, but was historically tied to Apache as the underlying web server.

On production servers, mostly due to habit, I used CentOS but given the project maintenance changes in recent years, it was time to shift my production environments to something else, and the obvious choice was Debian (topic for another post).

Along with updating my configurations to work with Debian, it was time to support (and use) NGINX as the underlying web server (the actual topic of this post).

Apache doCMS configuration

doCMS, like many other PHP based frameworks and systems, uses the concept of a single entrypoint request handler (run.php), including for static files (there is a good reason for this). This allowed the Apache configuration to be very minimal and the only logic tied to Apache itself was simply redirecting (301 HTTP) any URL with uppercase characters to lowercase, mostly for enforcing clean URL structure and performance.

To implement the request handler and redirect logic in the easiest possible manner, I leveraged a single .htaccess file dropped in the document root folder of the CMS. The challenge before me was therefore converting this file to an equivalent NGINX configuration.

The actual .htaccess file used is below:

IndexIgnore */*

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    
    # Redirect to lower case
    RewriteCond %{REQUEST_URI} !/run\.php
    RewriteCond %{REQUEST_URI} [A-Z]
    RewriteRule (.*)           ${lc:$1} [R=301,L]

    # Allow request handler
    RewriteCond %{REQUEST_FILENAME} /run\.php
    RewriteRule .*                  - [L]

    # Redirect to the doCMS request handler
    RewriteRule ^ /run.php [L]
</IfModule>

NGINX doCMS configuration

Although I have little to no experience with NGINX I assumed this was going to be quick and easy, especially given there are many translation tools available online, such as the one provided by Winginx.

Turns out, however, that the online conversion tools, for this specific example, return a completely wrong conversion and immediately, after some quick testing, I realised my day was going to disappear learning NGINX config file structure.

First, a few handy concepts and decisions that led my approach:

With these points taken into account, essentially I was left needing to implement two simple statements:

  1. For any request with an uppercase letter, 301 redirect to lowercase;
  2. Forward all requests to run.php;

Simple right?

Redirecting to lower case

This was actually very easy, and many solutions can be found online:

location ~ [A-Z] {
    rewrite_by_lua_block {
        ngx.redirect(string.lower(ngx.var.uri), 301);
    }
}

The above code, placed within a server block, will match any URL with an uppercase letter ([A-Z]) and redirect to lower case using Lua.

Forwarding to run.php

This is where the fun starts. Just like redirecting to lowercase, many examples are available online, and one of these was indeed my starting point:

location / {
    include snippets/fastcgi-php.conf;

    # Check PHP version when installing
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root/run.php;
}

The code above matches all requests (location /) and forwards them to the PHP handler.

It worked, but only sometimes.

Note that I had nothing else in my config file, but for reasons unknown to me, the forwarding would happen only if the given URL matched an actual file in the NGINX document root. For any other URL, a native NGINX 404 response was being returned.

After attempting many variations of the above code, and running out of Stack Overflow questions, I was about to give up, when I finally decided to switch my brain on.

Somewhere, something, was taking over control of URLs that did not map to a file on the file system. First idea, grep the NGINX configuration folder for 404.

And immediately, the guilty line:

try_files $fastcgi_script_name =404;

This line is in fact part of the default FastCGI PHP configuration found at /etc/nginx/snippets/fastcgi-php.conf and essentially takes over whenever a request does not map to a PHP file on the filesystem.

Commenting the file instantly fixed all problems. Final full config, including both HTTP and HTTPS delivery, found below:

#
# Default nginx server configuration for doCMS
#
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # SSL configuration
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    # Replace with correct certificate location
    ssl_certificate /etc/ssl/certs/cloudflare.pem;
    ssl_certificate_key /etc/ssl/private/cloudflare.key;

    root /var/www/doCMS;

    # Replace with correct hostname
    server_name HOST.dodify.net;

    location ~ [A-Z] {
        rewrite_by_lua_block {
            ngx.redirect(string.lower(ngx.var.uri), 301);
        }
    }

    location / {
        include snippets/fastcgi-php.conf;

        # Check PHP version when installing
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root/run.php;
    }
}

What's next?

Now it's time to delve into NGINX peformance tuning and CentOS to Debian changes!