mdbook 与 Nginx 下的服务器管理

mdbook 是 Rust 实现的一个类似于 gitbook 的文档服务器,通过使用 MarkDown 编写文档,mdbook 可将其构建为 HTML、PDF 目标,并可运行 HTTP 服务器,在线提供文档。咱们使用 Nginx 做 mdbook 的反向代理,带来高并发、SSL/TLS 等额外功能。

以下是在建立此种文档服务器时,用到的一些配置与脚本。

include 自己的一些行

include 指令可以重用 Markdown 文档中的一些行。在 linux.md 中写下双花括号括起来的 #include ./linux.md:119:191 便可以引用 linux.md 本身的 119191 行。

服务器管理脚本

文件:srv_incl.sh

INFO_CLR="\033[1;90;1;93m"
SUCESS_CLR="\033[0;90;2;92m"
END_CLR="\033[0m"
ALERT_CLR="\033[5;47;1;31m"
LOG_FILE="${HOME}/log/srv_mgt.log"

declare -A dirs

dirs["rust-lang"]="rust-lang-zh_CN"
dirs["java"]="learningJava"
dirs["ccna60d"]="ccna60d"
dirs["ts"]="ts-learnings"
dirs["www"]="buy-me-a-coffee"
dirs["snippets"]="code_snippets"
dirs["jenkins"]="jenkins_book_zh"
dirs["hpcl"]="hpc-studies"

declare -A ports

ports["rust-lang"]="10443"
ports["java"]="10445"
ports["ccna60d"]="10444"
ports["ts"]="10447"
ports["www"]="10446"
ports["snippets"]="10448"
ports["jenkins"]="10449"
ports["hpcl"]="10450"


COMMANDS=("start" "stop" "restart" "monitor" "status")
OPTIONS=("all")
for name in ${!dirs[@]}; do OPTIONS=(${OPTIONS[@]} "${name}"); done


dir_name=`/usr/bin/dirname $LOG_FILE`
if [ ! -d "${dirname}" ]; then /usr/bin/mkdir -p $dir_name; fi
if [ ! -f "${LOG_FILE}" ]; then touch $LOG_FILE; fi

stop_srv() {
    proc_num=`/usr/bin/netstat -ntlp 2> /dev/null | grep "${ports[$1]}" | wc -l`
    if [ $proc_num -ne 0 ]; then
        echo -e "\n\rStopping $1 ..."
        kill `/usr/bin/netstat -ntlp 2> /dev/null | grep ${ports[$1]} | awk -F' ' '{print $7}' | awk -F'/' '{print $1}'`
        sleep 3
    fi
}

gen_sitemap() {
    echo -e "\r\nGenerating $1 sitemap.xml..."
    if [ ! -f "book/sitemap.xml" ]; then
        mdbook-sitemap-generator -d "$1.xfoss.com" -o book/sitemap.xml
        /usr/bin/sed -i '1 i\<?xml version="1.0" encoding="utf-8" ?>' book/sitemap.xml
        /usr/bin/sed -i 's/\.md/\.html/g' book/sitemap.xml
        /usr/bin/sed -i 's/<loc>/<loc>https:\/\//g' book/sitemap.xml
        /usr/bin/sed -i 's/<urlset>/<urlset xmlns=\"http:\/\/www.sitemaps.org\/schemas\/sitemap\/0.9\">/g' book/sitemap.xml
    fi
}

start_srv() {
    proc_num=`/usr/bin/netstat -ntlp 2> /dev/null | grep "${ports[$1]}" | wc -l`

    if [ $proc_num -ne 1 ]; then
        cd "$HOME/${dirs[$1]}"
        echo -e "\n\rStarting $1 ..."
        mdbook serve . -p "${ports[$1]}" -n 127.0.0.1 > /dev/null 2>&1 &
    fi
}

start_all() {
    for name in ${!dirs[@]}; do start_srv $name; done
}

kill_all() {
    echo -e "\n\rStopping all..."
    /usr/bin/ps -A | grep mdbook | while read -r line; do
    kill $(echo $line | awk -F' ' '{print $1}') && sleep 3
done
}

get_status() {
    echo "---------------------------------------------"
    echo -e " ${INFO_CLR}$1.xfoss.com${END_CLR} 状态:"
    pid=$(/usr/bin/netstat -ntlp 2> /dev/null | grep ${ports[$1]} | awk -F' ' '{print $7}' | awk -F'/' '{print $1}')

    re='^[0-9]+$'
    if ! [[ $pid =~ $re ]] ; then echo -e "${ALERT_CLR}----- Dead !!!!!!!${END_CLR}"
    else
        echo -n -e "${SUCESS_CLR}"
        /usr/bin/ps -p $pid -o pid,vsz=MEMORY -o etime=ELAPSED_TIME -o state=STATE,stime=START_TIME
        echo -n -e "${END_CLR}"
    fi
}

chk_n_restart() {
    resp_code=$(/usr/bin/curl -I "https://$1.xfoss.com/sitemap.xml" 2>/dev/null | head -n 1 | cut -d$' ' -f2)

    if [ $((`date +%s`-`git log -1 --format=%ct`)) -lt 900 ]; then
        echo -e "\r\n$1 content updated, now restarting it..." && do_restart $1;
        echo "`date` - 检查 $1 运行状态并重启服务完成" >> $LOG_FILE
        exit 0
    fi


    if [ "$resp_code" != "200" ]; then
        echo -e "\r\n$1 not running, now starting it" && start_srv $1;
    fi
}

do_mon() {
    cd "$HOME/${dirs[$1]}"

    echo -e "\r\nTrying to checkout $1 ..."

    git pull
    echo "`date` - $1 git checkout 完成" >> $LOG_FILE

    chk_n_restart $1
}

show_status() {
    case $1 in
        "all")
            for name in ${!dirs[@]}; do get_status $name; done
            ;;
        *)
            get_status $1
            ;;
    esac
}

do_start() {
    case $1 in
        "all")
            start_all
            ;;
        *)
            start_srv $1
            ;;
    esac
}

do_stop() {
    case $1 in
        "all")
            kill_all
            ;;
        *)
            stop_srv $1
            ;;
    esac
}

do_restart() {
    case $1 in
        "all")
            kill_all && start_all
            ;;
        *)
            stop_srv $1 && start_srv $1
            ;;
    esac
}


monitor() {
    case $1 in
        "all")
            for name in ${!dirs[@]}; do
                do_mon $name && exit 0
            done
            ;;
        *)
            # do_mon $1 > /dev/null 2>&1 &
            do_mon $1 && exit 0
            ;;
    esac
}

文件:srv_mgt.sh

#!/usr/bin/bash
source $(dirname $0)/srv_incl.sh

declare -A paras

i=1;
for para in "$@"; do paras[$i]=$para && i=$((i + 1)); done

if [ $i != 3 ]; then
    echo "命令行参数问题。仅支持两个参数:start/stop/restart/status, all/service_name"
    echo "command: ${COMMANDS[@]}"
    echo "service_name: ${!dirs[@]}" && exit 1
fi

cmd_okay=0
for cmd in ${COMMANDS[@]}; do
    if [ $1 == $cmd ]; then cmd_okay=1 && break; fi
done

if [ $cmd_okay -eq 0 ]; then
    echo "第一个命令行参数错误"
    echo "可选参数:${COMMANDS[@]}" && exit 1
fi

option_okay=0
for opt in ${OPTIONS[@]}; do
    if [ $2 == $opt ]; then option_okay=1 && break; fi
done

if [ $option_okay -eq 0 ]; then
    echo "第二个命令行参数错误"
    echo "可选参数:${OPTIONS[@]}" && exit 1
fi

case $1 in
    "start")
        do_start $2
        ;;
    "stop")
        do_stop $2
        ;;
    "restart")
        do_restart $2
        ;;
    "status")
        show_status $2
        ;;
    "monitor")
        monitor $2
        ;;
esac

exit 0

Nginx 配置示例

文件:/etc/nginx/nginx.conf

user  unisko nginx;
worker_processes  auto;
load_module modules/ngx_http_cache_purge_module.so;

pid /var/run/nginx.pid;

error_log  /var/log/nginx/error.log notice;


events {
	worker_connections  1024;
}


http {
	include       /etc/nginx/mime.types;
	default_type  application/octet-stream;

	log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
		'$status $body_bytes_sent "$http_referer" '
		'"$http_user_agent" "$http_x_forwarded_for"';

	access_log  /var/log/nginx/access.log  main;

	sendfile        on;
	#tcp_nopush     on;

	keepalive_timeout  65;

	server {
		listen 80 default_server;
		server_name _;
		return 301 https://$host$request_uri;
	}

    #gzip  on;

	include /etc/nginx/conf.d/*.conf;
}

文件:/etc/nginx/conf.d/ccna60d.conf

upstream ccna60d { server 127.0.0.1:10444; }
server {
	server_name ccna60d.xfoss.com;

	location / {
		proxy_pass http://ccna60d;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_cache_bypass $http_upgrade;
	}



    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/ccna60d.xfoss.com-0001/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ccna60d.xfoss.com-0001/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

一个域名下多本书的配置

此需要是通过 Nginx 反向代理中,URL 重写的设置实现的。相较于一个域名一本书的 Nginx 设置,略有差别。

upstream docs-root { server 127.0.0.1:10446; }
upstream xiaohu-zh { server 127.0.0.1:10447; }
upstream xiaohu-en { server 127.0.0.1:10448; }

server {
        server_name docs.xfoss.com;

        location / {
                proxy_pass http://docs-root;
        }

        location /xiaohu/zh/ {
                proxy_pass http://xiaohu-zh/;
        }

        location /xiaohu/en/ {
                proxy_pass http://xiaohu-en/;
        }

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;



        listen 443 ssl;
        ssl_certificate /etc/ssl/certs/Senscomm/cert_chain.pem;
        ssl_certificate_key /etc/ssl/certs/Senscomm/private.key;
}

其中,差别在于这里:

        location /xiaohu/zh/ {
                proxy_pass http://xiaohu-zh/;
        }

proxy_pass http://xiaohu-zh/ 最后多了一个斜杠。

参考:Nginx reverse proxy + URL rewrite

单个域名下同步单个仓库,提供多本书的操作流程

  1. 在代码仓库根目录下建立一本新书对应的目录。此目录下应有 srctheme 目录及 book.toml 文件等必要组成部分,所有内容的 MarkDown 文件都在 src 目录下;

  2. 修改 src_inc.sh 文件,添加上面新书的目录,以及新书的端口;

  3. 修改 Nginx 的配置文件 /etc/nginx/config.d/docs.conf,添加新书的 upstream 设置及反向代理路径;

  4. 运行 srv_mgt.sh start demo-book &systemctl restart nginx 启动新书,并重启 Nginx。

Last change: 2023-07-26, commit: 088e3d7