Ubuntu Nginx 站点部署:从配置到自动化

最近我把跑在内网 8090 端口的博客服务发布到了公网。域名是 blog.twopair.cn,HTTP 自动跳转 HTTPS。折腾了一番 Nginx,顺手写了个脚本方便以后管理多个站点。

环境准备

Ubuntu 20.04/22.04 上先把基础环境装好。

sudo apt update
sudo apt install nginx unzip -y
sudo ufw allow 'Nginx Full'

配置逻辑

Ubuntu 的 Nginx 目录分成两个文件夹:

  • /etc/nginx/sites-available/ - 存放配置文件,但 Nginx 不读这里
  • /etc/nginx/sites-enabled/ - Nginx 只加载这里的文件(软链接)

管理方式:

  • 上线:ln -s 创建软链接
  • 下线:rm 删掉链接,原文件保留

配置文件详解

以 blog.twopair.cn 为例,配置分为两个 server 块。

证书存放

证书放在 /etc/nginx/cert/<域名>/

  • .pem.crt - 公钥
  • .key - 私钥(chmod 600)

HTTP 跳转 HTTPS

server {
    listen 80;
    server_name blog.twopair.cn;
    return 301 https://$host$request_uri;
}

HTTPS 反向代理

server {
    listen 443 ssl;
    server_name blog.twopair.cn;

    ssl_certificate     /etc/nginx/cert/blog.twopair.cn/fullchain.pem;
    ssl_certificate_key /etc/nginx/cert/blog.twopair.cn/privkey.key;

    client_max_body_size 100M;  # 默认 1MB 太小,改成 100M

    location / {
        proxy_pass http://127.0.0.1:8090;

        # 传递真实 IP 和域名给后端
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

自动化脚本

为了避免每次手动配置,我写了个脚本 vhost_manager.sh,自动完成解压证书、生成配置、创建软链接、重载 Nginx。

脚本保存为 vhost_manager.sh,然后 chmod +x vhost_manager.sh

#!/bin/bash
# Nginx 站点管理工具

set -euo pipefail

NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
CERT_BASE="/etc/nginx/cert"

error() {
    echo "错误:$1" >&2
    exit 1
}

checkRoot() {
    if [ "$(id -u)" -ne 0 ]; then
        error "请使用 root 或 sudo 执行该脚本"
    fi
}

reloadNginx() {
    nginx -t || error "Nginx 配置校验失败"
    systemctl reload nginx
}

case "${1:-}" in
    deploy)
        checkRoot

        DOMAIN="${2:-}"
        PORT="${3:-}"
        ZIP_PATH="${4:-}"

        if [ -z "$DOMAIN" ] || [ -z "$PORT" ] || [ -z "$ZIP_PATH" ]; then
            error "用法: $0 deploy <domain> <port> <cert.zip>"
        fi

        if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
            error "端口号必须是数字"
        fi

        if [ ! -f "$ZIP_PATH" ]; then
            error "证书 ZIP 文件不存在: $ZIP_PATH"
        fi

        CERT_DIR="$CERT_BASE/$DOMAIN"
        CONF_PATH="$NGINX_AVAILABLE/$DOMAIN"

        echo ">>> [1/5] 准备证书目录: $CERT_DIR"
        mkdir -p "$CERT_DIR"

        unzip -j -o "$ZIP_PATH" -d "$CERT_DIR" > /dev/null \
            || error "证书 ZIP 解压失败"

        PEM=$(find "$CERT_DIR" \( -name "*.pem" -o -name "*.crt" \) -type f | head -n 1)
        KEY=$(find "$CERT_DIR" -name "*.key" -type f | head -n 1)

        if [ -z "$PEM" ] || [ -z "$KEY" ]; then
            error "未在证书包中找到 .pem/.crt 或 .key 文件"
        fi

        chmod 600 "$KEY"
        chmod 644 "$PEM"

        echo ">>> [2/5] 生成 Nginx 配置: $CONF_PATH"

        cat > "$CONF_PATH" <<EOF
server {
    listen 80;
    server_name $DOMAIN;
    return 301 https://\$host\$request_uri;
}

server {
    listen 443 ssl http2;
    server_name $DOMAIN;

    ssl_certificate $PEM;
    ssl_certificate_key $KEY;

    ssl_session_timeout 5m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:$PORT;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
    }
}
EOF

        echo ">>> [3/5] 启用站点"
        ln -sf "$CONF_PATH" "$NGINX_ENABLED/$DOMAIN"

        echo ">>> [4/5] 校验并重载 Nginx"
        reloadNginx

        echo ">>> [5/5] 完成"
        echo ">>> https://$DOMAIN -> 127.0.0.1:$PORT"
        ;;

    enable)
        checkRoot
        DOMAIN="${2:-}"
        [ -z "$DOMAIN" ] && error "用法: $0 enable <domain>"

        ln -sf "$NGINX_AVAILABLE/$DOMAIN" "$NGINX_ENABLED/$DOMAIN"
        reloadNginx
        echo ">>> 站点 $DOMAIN 已上线"
        ;;

    disable)
        checkRoot
        DOMAIN="${2:-}"
        [ -z "$DOMAIN" ] && error "用法: $0 disable <domain>"

        rm -f "$NGINX_ENABLED/$DOMAIN"
        reloadNginx
        echo ">>> 站点 $DOMAIN 已下线(配置保留)"
        ;;

    remove)
        checkRoot
        DOMAIN="${2:-}"
        [ -z "$DOMAIN" ] && error "用法: $0 remove <domain>"

        rm -f "$NGINX_ENABLED/$DOMAIN"
        rm -f "$NGINX_AVAILABLE/$DOMAIN"
        rm -rf "$CERT_BASE/$DOMAIN"

        reloadNginx
        echo ">>> 站点 $DOMAIN 已彻底移除"
        ;;

    status)
        echo "--- 已启用站点 ---"
        ls -l "$NGINX_ENABLED" || echo "暂无已启用站点"
        ;;

    *)
        echo "用法:"
        echo "  $0 deploy  <domain> <port> <cert.zip>"
        echo "  $0 enable  <domain>"
        echo "  $0 disable <domain>"
        echo "  $0 remove  <domain>"
        echo "  $0 status"
        exit 1
        ;;
esac

实战示例

博客已经跑在 8090 端口,证书压缩包 blog_cert.zip 也上传了。

一键部署:

./vhost_manager.sh deploy blog.twopair.cn 8090 ./blog_cert.zip

日常操作:

./vhost_manager.sh disable blog.twopair.cn   # 临时下线
./vhost_manager.sh enable blog.twopair.cn    # 重新上线
./vhost_manager.sh remove blog.twopair.cn    # 彻底删除

常见问题

413 Request Entity Too Large

博客上传大图片报错。Nginx 默认限制 1MB。

解决:配置里加 client_max_body_size 100M;。脚本已经自动添加了。