#!/bin/bash # nginx-controller.bash - Dynamic Nginx Configuration Controller for Single-Host Containers # # Description: # This script acts as a lightweight, single-host Nginx controller that dynamically manages # Nginx configurations for containers running on the host. It watches container lifecycle # events (start, stop) and updates the Nginx configuration accordingly to route traffic # to containers' internal IPs and ports. # # Features: # - Monitors container events and updates Nginx configurations on container start/stop. # - Automatically creates or removes Nginx reverse proxy configurations for containers. # - Supports dynamic upstream configuration and domain-based reverse proxying. # - Minimal dependencies: Runs with just bash and basic Linux tools (no external services). # # Usage: # Run this script as a background process to continuously watch container events and # update the Nginx configuration dynamically. # # Example: # nohup /bin/bash /path/to/nginx-controller.bash &>/dev/null & # # Requirements: # - Docker or containerd with accessible CLI (`ctr`). # - A running Nginx instance configured with the appropriate site-enabled directory. # # Copyright (c) 2024 Phus Lu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # Author: Phus Lu NGINX_SITES_DIRECTORY=${NGINX_SITES_DIRECTORY:-/etc/nginx/sites-enabled} pidtree() ( # https://superuser.com/a/784102/114255 [ -n "$ZSH_VERSION" ] && setopt shwordsplit declare -A childs while read pid ppid; do childs[$ppid]+=" $pid" done < <(ps -e -o pid= -o ppid=) walk() { for i in ${childs[$1]}; do walk $i done echo $1 } for i in "$@";do walk $i done ) generate_nginx_conf () { local cid=$1 local name=$2 local ipaddr=$3 local port=$4 local domain=$5 echo "# generated by $0 # registered for ${cid} upstream ${name}.internal { server ${ipaddr}:${port}; }" if [ "${domain}" != "" ]; then echo " server { listen 80; listen [::]:80; server_name ${domain}; location / { proxy_pass http://${name}.internal; proxy_http_version 1.1; proxy_set_header Host \$http_host; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection Upgrade; proxy_read_timeout 180; proxy_connect_timeout 180; proxy_send_timeout 180; send_timeout 180; } }" fi } update_nginx_conf () { local ns=$1 local cid=$2 local pid=$3 if [[ $(ctr -n ${ns} container info ${cid}) =~ (\.|\/|host)name\":\ \"/?([^\"]+)\", ]]; then local name=${BASH_REMATCH[2]} else return fi if [[ $(nsenter -t ${pid} -n ip -o -4 a) =~ [0-9]+:\ e[a-z0-9]+\ +inet\ ([0-9\.]+) ]]; then local ipaddr=${BASH_REMATCH[1]} fi if [ -z "$ipaddr" ]; then sleep 5 if [[ $(nsenter -t ${pid} -n ip -o -4 a) =~ [0-9]+:\ e[a-z0-9]+\ +inet\ ([0-9\.]+) ]]; then local ipaddr=${BASH_REMATCH[1]} fi fi local IFS=$'\0' readarray -d '' environ ${NGINX_SITES_DIRECTORY}/${name}.internal if nginx -t; then nginx -s reload fi } remove_nginx_conf () { local ns=$1 local cid=$2 rm -f $(grep -l "^# registered for ${cid}" ${NGINX_SITES_DIRECTORY}/*.internal) if nginx -t; then nginx -s reload fi } list_watch_reconcile () { ctr namespace ls -q | while read -r ns; do ctr -n ${ns} task ls | while read -r cid pid status; do if [ "$status" != "RUNNING" ]; then continue fi update_nginx_conf ${ns} ${cid} ${pid} done done ctr events | while read -r line; do echo "$line" >>/tmp/ctr-events.log if [[ $line =~ \ ([a-z0-9\.\-_]+)\ /tasks/start\ \{\"container_id\":\"([0-9a-f]+)\",\"pid\":([0-9]+) ]]; then update_nginx_conf "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" elif [[ $line =~ \ ([a-z0-9\.\-_]+)\ /tasks/delete\ \{\"container_id\":\"([0-9a-f]+)\" ]]; then remove_nginx_conf "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" fi done } trap 'kill $(pidtree $$)' EXIT list_watch_reconcile