Guide for Adding Custom Nginx Directives to a Cloudron Application
-
Introduction
Cloudron applications are reverse-proxied using Nginx, a high-performance proxy and web server. Cloudron manages the Nginx configuration of applications themselves, but there may be cases where the end user needs to add custom configuration directives on an ad-hoc basis for a specific application. Attempting to modify the application nginx config directly at
/etc/nginx/applications/[app-id]/[app-domain].conf
is not feasible, since these configuration files are ephemeral, and are re-written upon restarts and Cloudron upgrades.However, I developed a workaround that allows users to add Nginx config snippets to Cloudron apps in a way that is persistent, and robust against being over-written. This method uses Nginx's
include
directives, a custom bash script, and a cronjob. This guide presents an overview of this workaround, and shares the custom bash script that I use.Disclaimer
Before proceeding, be advised that Cloudron does not support ad-hoc user modifications to application Nginx configuration. This method is a workaround, and could result in broken reverse-proxy configurations should the user create an invalid Nginx configuration file.
Method
Nginx allows us to embed configuration file snippets using the
include
directive. We'll create a file at/etc/nginx/custom-nginx-directives.conf
containing what we wish to include, and then include this within the application nginx config usinginclude custom-nginx-directives.conf;
. Note thatinclude
allows us to specify either relative, or absolute pathing. On Ubuntu Linux, relative paths are searched for starting from/etc/nginx
.Once you have defined that file, we will use a bash script to add the line
include custom-nginx-directives.conf;
to the application nginx config. On Cloudron, the per-application nginx configuration are templated using EJS from cloudron/box/src/nginxconfig.ejs. The EJS template is complex and has many conditionals, but it has the following invariant structure (i.e. no matter what application, it will always have the following blocks):map $http_upgrade $connection_upgrade { # [Extranuous information removed] } map $upstream_http_referrer_policy $hrp { # [Extranuous information removed] } # http server server { # [Extranuous information removed] } # https server server { # [Extranuous information removed] }
Observe how for every Cloudron app, it will always have two
server
blocks. The first one defines the HTTP (i.e. port 80) listener, and usually contains a redirect to HTTPS. The second one is the HTTPS listener (i.e. port 443), and contains the bulk of the application-specific logic.For my specific use case, I needed to include custom
location
directives for my Cloudron app. This probably represents the most common use case for custom nginx configuration. Hence we need to insert our include directive at the end of the second server block. This can be done by searching for the last closing bracket of the file.It is this invariant which we may take advantage of. The following bash commands will search for the last closing bracket in the nginx file, and use
sed
to insert a line right before it.nginx_config_file="/etc/nginx/applications/[app_id]/[app_domain].conf" include_line="include custom-nginx-directives.conf;" # Find the line number(s) of all the closing brackets (i.e. '}') grep_result=$(grep --line-number '}' "$nginx_config_file") # Grep returns output in the form line_numbers:match. Extract only the line numbers with 'cut' line_numbers=$(echo "$grep_result" | cut --delimiter=':' --fields=1) # Select the last line number using 'tail' last_bracket_line=$(echo "$line_numbers" | tail --lines=1) # Append the $include_line before the last closing bracket in the Nginx configuration file. sed -i "${last_bracket_line}i $include_line" "$nginx_config_file"
Bash Script
We can take the above logic, and implement it in a bash script that does some additional checking and error prevention. We should first make sure that the line is not already present. Likewise, if the line is not present, and we include it, we should also automatically test and reload our nginx configuration so that the changes take effect. Taking the above considerations in mind, we yield the following final script:
#!/bin/bash # # Copyright (c) 2024 Shen Zhou Hong # This code is released under the MIT License. https://mit-license.org/ # # This Bash script checks if the line defined in $include_line is present # in the Nginx configuration file at $nginx_config_file. If the line is not # found, it appends the line before the last closing bracket and tests the # configuration using 'nginx -t'. If the test succeeds, it reloads Nginx with # 'systemctl reload nginx'. # # This file is intended for use with Cloudron in order to add includes to # the nginx configuration of applications in a way that is robust against being # re-written. It should be run using a cronjob as root. # # Example crontab entry (*/30 specifies that it shall run every 30 minutes): # # */30 * * * * /path/to/add_includes.sh 2>&1 set -eEu -o pipefail # Make sure to use absolute pathing if this script is run as a cronjob. # Replace [app_id] and [app_domain] with actual values. nginx_config_file="/etc/nginx/applications/[app_id]/[app_domain].conf" # By default, Nginx's include directive takes either a relative, or an absolute # path. On Ubuntu relative paths are defined in relation to /etc/nginx include_line="include custom-nginx-directives.conf;" # Check if the Nginx configuration file exists if [ ! -f "$nginx_config_file" ]; then echo "Error: Nginx configuration file '$nginx_config_file' not found." exit 1 fi # Check if the line is already present in the config file if grep -q "$include_line" "$nginx_config_file"; then echo "Line is already present in $nginx_config_file. No changes made to file." exit 0 # If the include_line is not present, we will add it to the end of the file, right # before the final closing bracket (i.e. '}'). else # Find the line number(s) of all the closing brackets (i.e. '}') grep_result=$(grep --line-number '}' "$nginx_config_file") # Grep returns output in the form line_numbers:match. Extract only the line numbers with 'cut' line_numbers=$(echo "$grep_result" | cut --delimiter=':' --fields=1) # Select the last line number using 'tail' last_bracket_line=$(echo "$line_numbers" | tail --lines=1) # Append the $include_line before the last closing bracket in the Nginx configuration file. # The sed command with the 'i' operation specifies an insertion at the line number defined # in the $last_bracket_line. sed -i "${last_bracket_line}i $include_line" "$nginx_config_file" echo "Line added successfully to $nginx_config_file." # Test the Nginx configuration if nginx -t; then echo "Nginx configuration test successful. Reloading Nginx..." systemctl reload nginx echo "Nginx reloaded successfully." else echo "Nginx configuration test failed. Please check the configuration manually." exit 1 fi fi
Link to latest version on Github Gist
Crontab
Running the script will perform the include once. However, the changes made to
/etc/nginx/applications
will inevitably be lost upon restart, or platform upgrade. Hence, we will next define a crontab entry that will run the script frequently at a regular interval. Access your crontab as root via:sudo crontab -e
Now add the following entry:
*/30 * * * * /path/to/add_includes.sh 2>&1
The above crontab command will run the script every 30 minutes.
Conclusion
This guide presents a method of adding persistent custom nginx directives to Cloudron applications using a bash script and a crontab. Although it is not a very sophisticated approach, it works well enough for my use case, and I hope it will be useful for other users as well.
In the future, I hope there will be a way for Cloudron to support custom Nginx directives, so that these workarounds are no longer necessary.