Skip to content

Instantly share code, notes, and snippets.

@ShenZhouHong
Last active December 30, 2023 08:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShenZhouHong/16a4a4f0a40ca60559028aca8101eb60 to your computer and use it in GitHub Desktop.
Save ShenZhouHong/16a4a4f0a40ca60559028aca8101eb60 to your computer and use it in GitHub Desktop.
Cronjob bash script that adds custom include directives to the nginx configuration of Cloudron apps.
#!/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
@ShenZhouHong
Copy link
Author

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 using include custom-nginx-directives.conf;. Note that include 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 90) 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 above script showcased in the 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 gist 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment