用 Docker Compose 跑 Nginx 是目前比较省心的部署方式,配置文件管容器参数,目录挂载管持久化,容器删了数据还在。这篇从零开始,把 Nginx 部署、反向代理、HTTPS 证书申请和自动续期整个流程走一遍,顺便记录几个容易踩的坑。
创建目录结构
在服务器上建一个专属目录,把 Nginx 相关的东西都放进去:
mkdir -p nginx-docker/{html,conf.d,logs,certs}
cd nginx-docker
目录结构大概是这样:
nginx-docker/
├── conf.d/ # 站点配置文件
├── html/ # 静态网页
├── logs/ # Nginx 日志
├── certs/ # SSL 证书(后面用)
└── docker-compose.yml
编写 docker-compose.yml
services:
nginx:
image: nginx:latest
container_name: my-nginx
restart: always
ports:
- "80:80"
- "443:443"
extra_hosts:
- "host.docker.internal:host-gateway" # 让容器能访问宿主机
volumes:
- ./html:/usr/share/nginx/html
- ./conf.d:/etc/nginx/conf.d
- ./logs:/var/log/nginx
- ./certs:/etc/nginx/certs
environment:
- TZ=Asia/Shanghai
这里几个要点:restart: always 保证容器挂了自动重启、开机自启;extra_hosts 那行很关键,它让 Nginx 容器能通过 host.docker.internal 访问宿主机上的服务,后面反向代理全靠它;certs 目录先挂上,后面申请证书时直接往里放文件就行。
启动容器:
docker compose up -d
写个维护脚本
每次手动敲 docker exec 命令太麻烦,写个脚本封装常用操作:
#!/bin/bash
CONTAINER_NAME="my-nginx"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
show_help() {
echo -e "${GREEN}Nginx 容器维护脚本${NC}"
echo "用法: ./nginx-manage.sh [命令]"
echo ""
echo "可用命令:"
echo " start - 启动容器"
echo " stop - 停止容器"
echo " down - 停止并删除容器"
echo " test - 测试配置文件语法"
echo " reload - 平滑重载配置"
echo " logs - 查看日志"
echo " status - 查看运行状态"
}
if [ -z "$1" ]; then
show_help
exit 1
fi
check_compose_file() {
if [ ! -f "docker-compose.yml" ] && [ ! -f "docker-compose.yaml" ]; then
echo -e "${RED}错误: 当前目录找不到 docker-compose.yml${NC}"
exit 1
fi
}
case "$1" in
start)
check_compose_file
docker compose up -d
echo -e "${GREEN}容器启动完成${NC}"
;;
stop)
check_compose_file
docker compose stop
;;
down)
check_compose_file
docker compose down
;;
test)
docker exec "$CONTAINER_NAME" nginx -t
;;
reload)
if docker exec "$CONTAINER_NAME" nginx -t; then
docker exec "$CONTAINER_NAME" nginx -s reload
echo -e "${GREEN}配置重载完成${NC}"
else
echo -e "${RED}配置文件有语法错误,放弃重载${NC}"
fi
;;
logs)
check_compose_file
docker compose logs -f nginx
;;
status)
docker ps -f name="$CONTAINER_NAME"
;;
*)
echo -e "${RED}未知命令: $1${NC}"
show_help
exit 1
;;
esac
保存后给执行权限:
chmod +x nginx-manage.sh
以后修改了配置文件,跑 ./nginx-manage.sh reload 就行,脚本会先测语法再重载,不用担心写错配置把服务搞挂。
配置反向代理
假设有个服务跑在宿主机的 127.0.0.1:41373,想通过 news.caiden.asia 这个域名访问。
在 conf.d 目录下新建 news.caiden.asia.conf:
server {
listen 80;
server_name news.caiden.asia;
location / {
proxy_pass http://host.docker.internal:41373;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
注意 proxy_pass 里写的是 host.docker.internal 而不是 127.0.0.1,因为 127.0.0.1 在容器里指的是容器自己,不是宿主机。
重载配置:
./nginx-manage.sh reload
确认域名的 A 记录解析到服务器 IP,防火墙放行 80 端口,就能通过域名访问了。
配置 SSL 证书
用 acme.sh 申请 Let’s Encrypt 的免费证书,到期前自动续期,基本免维护。
安装 acme.sh
curl https://get.acme.sh | sh
source ~/.bashrc
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
修改 Nginx 配置,放行验证请求
Let’s Encrypt 发证书前会访问 http://域名/.well-known/acme-challenge/xxx 来验证域名所有权。在 Nginx 配置里加个 location 处理这个请求:
server {
listen 80;
server_name news.caiden.asia;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
proxy_pass http://host.docker.internal:41373;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
重载配置让规则生效:
./nginx-manage.sh reload
申请证书
~/.acme.sh/acme.sh --issue -d news.caiden.asia -w ~/apps/nginx/html
看到绿色的 Cert success! 就说明申请成功了。
安装证书并设置自动续期
这一步把证书复制到 certs 目录,同时告诉 acme.sh 续期后自动重载 Nginx:
~/.acme.sh/acme.sh --install-cert -d news.caiden.asia \
--key-file /home/ubuntu/apps/nginx/certs/news.caiden.asia.key \
--fullchain-file /home/ubuntu/apps/nginx/certs/news.caiden.asia.cer \
--reloadcmd "sudo docker exec my-nginx nginx -s reload"
以后证书快过期时,acme.sh 的后台定时任务会自动续期,续完自动执行 reloadcmd 重载 Nginx,全程不用人工干预。
启用 HTTPS
最后一次修改 Nginx 配置,把 HTTP 跳转到 HTTPS:
server {
listen 80;
server_name news.caiden.asia;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name news.caiden.asia;
ssl_certificate /etc/nginx/certs/news.caiden.asia.cer;
ssl_certificate_key /etc/nginx/certs/news.caiden.asia.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://host.docker.internal:41373;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
重载配置:
./nginx-manage.sh reload
访问 https://news.caiden.asia,浏览器地址栏会显示安全锁,证书受全球浏览器信任。
踩坑记录
Ubuntu 下用 sh 跑 bash 脚本报语法错误。 Ubuntu 的 sh 默认指向 dash,不支持 function show_help() { ... } 这种写法。要么用 bash nginx-manage.sh 执行,要么把函数定义改成 show_help() { ... }(去掉 function 关键字)。
acme.sh 不要加 sudo 执行。 acme.sh 装在当前用户目录下,加了 sudo 会去 /root 找配置文件,找不到就报错。先确保 certs 目录权限正确:sudo chown -R $USER:$USER /home/ubuntu/apps/nginx/certs,然后去掉命令前面的 sudo 执行。注意 --reloadcmd 里面的 sudo 要保留,不然定时任务没权限重启容器。