ubuntu server部署nginx

github pages强制使用https访问,即使是硬编码http也不行。此时就需要部署nginx。

本文介绍如何使用docker部署一个nginx,并且配置SSL以支持https访问。

Nginx

涉及到的Docker相关命令参考《Linux常用命令实践》
graph LR
    Browser -->|HTTP 80| Nginx
    Browser -->|HTTPS 443| Nginx
    Nginx -->|HTTP 9003| Spring服务

拉取 Nginx 镜像

1
2
3
4
5
6
# 拉取官方最新版 Nginx 镜像
sudo docker pull nginx:latest
# 拉不下来换个源
docker pull anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/nginx:latest
# 查看已下载的镜像
sudo docker images

运行 Nginx 容器

  • -d: 后台运行容器
  • --name my-nginx: 指定容器名称
  • -p 80:80: 将宿主机的 80 端口映射到容器的 80 端口
1
2
# 运行容器(映射宿主机 80 端口到容器 80 端口)
sudo docker run -d --name my-nginx -p 80:80 nginx

验证运行状态

1
2
3
4
5
# 查看容器状态
sudo docker ps

# 访问测试(返回 Nginx 默认欢迎页)
curl http://localhost

挂载自定义配置和静态文件

1
2
3
4
5
6
7
8
9
10
nginx
├── conf/
│ ├── nginx.conf # 主配置文件
│ └── conf.d
│ └── default.conf # 子配置文件
├── ssl/
│ ├── domain.crt # SSL证书
│ └── domain.key # 私钥
├── html/ # 静态文件目录
└── logs/ # 日志目录

创建本地目录

1
mkdir -p ~/nginx/{conf/conf.d,html,logs,ssl}

文件放在宿主机,然后挂载到容器中。

宿主机 容器内
配置文件 conf/nginx.conf /etc/nginx/nginx.conf
目录 conf/conf.d/ /etc/nginx/conf.d
日志位置 logs/ /var/log/nginx/
项目位置 /usr/share/nginx/html

conf/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
user  nginx;
worker_processes auto;

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

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;
keepalive_timeout 65;

# 包含子配置
include /etc/nginx/conf.d/*.conf;
}

conf.d/default.conf

这里的192.168.0.127为宿主机的固定内网IP,Spring服务直接运行在宿主机上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# HTTP 服务配置
server {
listen 80;
# server_name localhost;

# 可选:HTTP自动跳转HTTPS(如需同时保留两种协议则不要配置此项)
# return 301 https://$host$request_uri;

location / {
# 容器内访问宿主机服务的正确方式
proxy_pass http://192.168.0.127:9003; # 直接使用宿主机局域网 IP
#proxy_pass http://host.docker.internal:9003; # Docker专用DNS(Linux 原生 Docker 环境中默认不生效)
#proxy_pass http://localhost:9003; # 指向本地Spring服务,localhost指容器内
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;
}
}

# HTTPS 服务配置
server {
listen 443 ssl;
# server_name localhost;

# 自签名证书路径
ssl_certificate /etc/nginx/ssl/self_signed.crt;
ssl_certificate_key /etc/nginx/ssl/self_signed.key;

# 禁用过时的加密协议
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

location / {
proxy_pass http://192.168.0.127:9003;
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; # 传递实际协议类型
}
}

html/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<!DOCTYPE html>
<html>
<head><h4>Take it Easy!Please playing Game</h4></head>
<body>
<div></div>
<!-- 4个board -->
<div id="board1" style="position: absolute; width:80px; height:10px; left:420px;
top:555px; background-color: cadetblue;"></div>
<div id="board2" style="position: absolute; width:80px; height:10px; left:520px;
top:555px; background-color: cadetblue;"></div>
<div id="board3" style="position: absolute; width:80px; height:10px; left:620px;
top:555px; background-color: cadetblue;"></div>
<div id="board4" style="position: absolute; width:80px; height:10px; left:720px;
top:555px; background-color: cadetblue;"></div>
<!-- 小球 -->
<div id="ball" class="circle" style="width:20px;
height:20px; background-color:crimson; border-radius: 50%; position:absolute;
left:600px; top:100px"></div>
<!-- 框 -->
<div id="box" style="border: 5px solid #555555; width:400px; height:550px; display=hide"></div>
<!-- 分数 过的board越多,分数越高 -->
<div id="score" style="width:200px; height:10px; position:absolute; left:900px;
font-family:'隶书'; font-size: 30px;">score: 0</div>
<!-- 游戏结束 -->
<div id="gg" style="width:200px; height:10px; position:absolute; left:550px; top:200px;
font-family:'隶书'; font-size: 30px; display: none;">Game Over</div>
<script>
// 设置box的样式
var box = document.getElementById("box");
box.style.position = "absolute";
box.style.left = "400px";
// 设置board的样式
var board1 = document.getElementById("board1");
var board2 = document.getElementById("board2");
var board3 = document.getElementById("board3");
var board4 = document.getElementById("board4");
// 声音
var shengyin = new Audio();
shengyin.src = "声音2.mp3";
shengyinFlag = 0; // 用来表示小球在第几块board上
// 键盘事件函数
var ball = document.getElementById("ball");
document.onkeydown = f;
function f(e){
var e = e || window.event;
switch(e.keyCode){
case 37:
// 按下左键,小球左移,但不要超过左边框
if(ball.offsetLeft>=box.offsetLeft + 10)
ball.style.left = ball.offsetLeft - 8 + "px";
break;
case 39:
// 按下右键,小球右移,但不要超过由边框
if(ball.offsetLeft<=box.offsetLeft+box.offsetWidth-ball.offsetWidth-10)
ball.style.left = ball.offsetLeft + 8 + "px";
break;
case 32:

}
}
// 定义一个分数变量
var fenshu = 0;
// 定义一个函数,移动给定的一个board
function moveBoard(board)
{
var t1 = board.offsetTop;
if(t1<=0)
{
// 如果board移到最上面了,就随机换个水平位置,再移到最下面
t2 = Math.floor(Math.random() * (720- 420) + 420);
board.style.left = t2 + "px";
board.style.top = "555px";
fenshu += 1; //分数增加1
document.getElementById("score").innerHTML = "score " + fenshu;
}
//
else
board.style.top = board.offsetTop - 1 + "px";
}
// 定义小球的速度变量
var startSpeed = 1;
var ballSpeed =startSpeed;
// step函数是游戏界面的单位变化函数
function step()
{
// board直接上下隔得太近,就逐个移动,否则,同时移动
var t1 = Math.abs(board1.offsetTop - board2.offsetTop);
var t2 = Math.abs(board2.offsetTop - board3.offsetTop);
var t3 = Math.abs(board3.offsetTop - board4.offsetTop);
// 定义一个board之间的间隔距离
var t4 = 140;
if(t1<t4)
{
moveBoard(board1);
}
else if(t2<t4)
{
moveBoard(board1);
moveBoard(board2);
}
else if(t3<t4)
{
moveBoard(board1);
moveBoard(board2);
moveBoard(board3);
}
else
{
moveBoard(board1);
moveBoard(board2);
moveBoard(board3);
moveBoard(board4);
}
// 定义小球的垂直移动规则,1、向下匀加速运动,2、如果碰到board就被board持续抬上去,
// 直到按左右键离开了该board

// 如果小球的纵坐标等于某个board的纵坐标,就被抬起
var t5 = Math.abs(ball.offsetTop - board1.offsetTop);
var t6 = Math.abs(ball.offsetTop - board2.offsetTop);
var t7 = Math.abs(ball.offsetTop - board3.offsetTop);
var t8 = Math.abs(ball.offsetTop - board4.offsetTop);
if(t5<=ball.offsetHeight && t5>0 && ball.offsetLeft>=board1.offsetLeft-ball.offsetWidth && ball.offsetLeft<=board1.offsetLeft+board1.offsetWidth)
{
ball.style.top = board1.offsetTop - ball.offsetHeight + "px";
ballSpeed = startSpeed;
if(shengyinFlag != 1)
{
shengyin.play();
shengyinFlag = 1;
}
}
else if(t6<=ball.offsetHeight && t6>0 && ball.offsetLeft>=board2.offsetLeft-ball.offsetWidth && ball.offsetLeft<=board2.offsetLeft+board2.offsetWidth)
{
ball.style.top = board2.offsetTop - ball.offsetHeight + "px";
ballSpeed = startSpeed;
if(shengyinFlag != 2)
{
shengyin.play();
shengyinFlag = 2;
}
}
else if(t7<=ball.offsetHeight && t7>0 && ball.offsetLeft>=board3.offsetLeft-ball.offsetWidth && ball.offsetLeft<=board3.offsetLeft+board3.offsetWidth)
{
ball.style.top = board3.offsetTop - ball.offsetHeight + "px";
ballSpeed = startSpeed;
if(shengyinFlag != 3)
{
shengyin.play();
shengyinFlag = 3;
}
}
else if(t8<=ball.offsetHeight && t8>0 && ball.offsetLeft>=board4.offsetLeft-ball.offsetWidth && ball.offsetLeft<=board4.offsetLeft+board4.offsetWidth)
{
ball.style.top = board4.offsetTop - ball.offsetHeight + "px";
ballSpeed = startSpeed;
if(shengyinFlag != 4)
{
shengyin.play();
shengyinFlag = 4;
}
}
else
{
ballSpeed = ballSpeed + 0.01; // 数字相当于加速度
ball.style.top = ball.offsetTop + ballSpeed + "px";
}
// ballSpeed = ballSpeed + 0.01; // 数字相当于加速度
// ball.style.top = ball.offsetTop + ballSpeed + "px";

// 如果小球跑出来box,就结束游戏
if(ball.offsetTop==0 || ball.offsetTop>=box.offsetTop+box.offsetHeight)
{
clearInterval(gameover);
ball.style.display = 'none';
board1.style.display = 'none';
board2.style.display = 'none';
board3.style.display = 'none';
board4.style.display = 'none';
var gg = document.getElementById("gg"); //显示游戏结束
gg.style.display = 'block';
}
}

var gameover = setInterval("step();", 8);
</script>
</body>
</html>

复制默认配置文件(可选)

1
2
3
4
5
6
# 临时启动容器并复制配置文件
sudo docker run --name my-nginx -d nginx
sudo docker cp temp-nginx:/etc/nginx/nginx.conf ~/nginx/conf/
sudo docker cp temp-nginx:/etc/nginx/conf.d ~/nginx/conf/
sudo docker cp temp-nginx:/usr/share/nginx/html ~/nginx/
sudo docker rm -f temp-nginx

配置 SSL(HTTPS)

生成自签名证书(测试用)

1
2
3
4
5
6
7
8
# 创建证书存放目录
mkdir -p ~/nginx/ssl

# 生成自签名证书(有效期365天)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ~/nginx/ssl/self_signed.key \
-out ~/nginx/ssl/self_signed.crt \
-subj "/CN=localhost"

使用Let’s Encrypt验证以及Certbot自动申请

TODO:参考一下这个如何配置LetsEncrypt并且自动更新证书

安装Certbot

Certbot 支持自动申请 LetsEncrypt 的泛域名证书

1
2
3
# Ubuntu/Debian
sudo apt update
sudo apt install certbot

使用Certbot生成Let’s Encrypt 证书

有http方式验证、DNS手动验证两种方式方式。而http验证需要开放公网的80端口,所以只能选择DNS手动验证方式:

  • certonly: 用于生成 SSL/TLS 证书的工具插件。
  • —email overstars@foxmail.com: Let’s Encrypt 要求提供有效的电子邮件地址
  • —agree-tos:同意 Let’s Encrypt 的服务条款。
  • -d ‘test.com’:指定您想要为其生成 SSL 证书的域名。你可以通过添加多个 -d 选项来同时为多个域名生成证书。
  • —manual:在命令提示符下手动操作来验证您拥有该域名。
  • —preferred-challenges=dns:使用 DNS 验证方式进行证书颁发,需要将一个特定的 TXT 记录添加到 DNS 进行验证。
1
sudo certbot certonly --email overstars@foxmail.com --agree-tos -d overstars.site -d www.overstars.site --manual --preferred-challenges dns

输入Y接受后续发邮件通知,弹出如下讯息:

1
2
3
4
5
6
7
8
9
10
11
Account registered.
Requesting a certificate for overstars.site and www.overstars.site

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.overstars.site.

with the following value:

sk5AiHXTKSkx_82yqZQqhBWUjtUCTL-lEeGtNHQ7R20

DNS验证

在腾讯云购买的域名可以参考腾讯云:DNS 验证,以及这篇博客《Centos服务器上使用acme.sh脚本免费获取SSL泛域名证书并启用Http》

在DNSPod添加如下一条记录,和如上弹出信息对应。

image-20250222182312950

然后输入回车……弹出如下报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.www.overstars.site.

with the following value:

ZXJcRUvUu205vyo2JqRmuzuyyB9FPTDI5eHX3efHRJo

(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet. Note that you might be
asked to create multiple distinct TXT records with the same name. This is
permitted by DNS standards.)

Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.www.overstars.site.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

Certbot failed to authenticate some domains (authenticator: manual). The Certificate Authority reported these problems:
Domain: www.overstars.site
Type: dns
Detail: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.www.overstars.site - check that a DNS record exists for this domain

Hint: The Certificate Authority failed to verify the manually created DNS TXT records. Ensure that you created these in the correct location, or try waiting longer for DNS propagation on the next attempt.

Some challenges have failed.
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.

设置自动续期

使用dns申请的证书不能直接使用certbot renew来更新证书的。但 cerbot 提供了一个 manual-auth-hook hook,可以编写一个脚本,由这个脚本来先完成 DNS 验证,然后再进行 renew。对应的脚本会自动添加 DNS 记录,从而完成 DNS 校验,并自动 renew 证书。

—dry-run:重要提醒:为避免遇到操作次数的限制,加入 dry-run 参数,能够避免操作限制,等执行无误后,再进行真实的renew 操作。

1
crontab -e

添加

1
2
# 每月的第5天的17:13执行任务
17 13 5 * * root /usr/bin/certbot renew --manual-auth-hook /root/au.sh >> /var/log/certbot-renew.log

运行容器并挂载目录

  • -v: 挂载宿主机目录到容器内路径
  • :ro指read-only,默认是rw
1
2
3
4
5
6
7
8
9
docker run -d --name my-nginx \
-p 9005:80 \
-p 9004:443 \
-v ~/nginx/html:/usr/share/nginx/html \
-v ~/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v ~/nginx/conf/conf.d:/etc/nginx/conf.d \
-v ~/nginx/logs:/var/log/nginx \
-v ~/nginx/ssl:/etc/nginx/ssl:ro \
nginx

验证容器到宿主机连通性

1
2
docker exec -it my-nginx ping host.docker.internal
docker exec -it my-nginx curl -v http://172.17.0.1:9005/health

重新加载配置

1
2
3
4
5
6
7
8
docker exec -it my-nginx bash
nginx -t
nginx -s reload
# 容器内安装telnet
apt-get update && apt-get install -y telnet
telnet host.docker.internal 9003
# 成功应显示:Connected to host.docker.internal
curl -v http://host.docker.internal:9003/images/e1266475-c75a-4670-aee0-1231e109886c.jpg