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:
.htaccess
files are placed in the relevant directory where they need to take effect within the document root. Although NGINX does have an extension that replicates this behaviour, there is a performance hit, therefore moving this logic in the main NGINX config file was preferable;- Attempting to translate each line one by one is not a good approach given these are complitely different systems. Rather adopting the NGINX philosophy is much better;
- Apache
mod_rewrite
is actually quite complex! - Starting with an empty NGINX config file not only speeds up the process, but improves the mental model and understanding of the config file format;
With these points taken into account, essentially I was left needing to implement two simple statements:
- For any request with an uppercase letter,
301
redirect to lowercase; - 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!