使用 Nginx-proxy + Let's Encrypt + Docker 部署 Nexus Repository

摘要: 本文介绍如何使用 Nginx-proxy 和 Let’s Encrypt 依托 Docker 和 Docker Compose 部署 Nexus Repository。并且 Nexus Repository 支持 Docker 的 Connector。通过本文可以快速部署一台 Nexus Private Repository,并且提供如下支持:SSL注册及更新,Docker Connector支持等。

Introduction

阅读本文前,您需要了解 Docker, Docker Compose 以及 Nginx 的基本知识。

传统部署方法:如果熟悉服务器部署,我们一般的部署方式,首先运行服务,然后将服务端口暴露出来,通过端口访问测试服务,然后安装 nginx 并配置 nginx reverse proxy 将指定的域名或路径 proxy 到指定的端口。如果需要 SSL 支持,需要自行使用 cerbot 来注册 let‘s encrypt 证书,然后在nginx 中配置。同时 3 个月后需要手动 renew 证书。

我可以看到整个过程比较繁琐,而且很难使用自动化脚本执行。下面介绍一下新的思路:基于 Docker 以及 nginx-proxy 部署方法。

首先所有的服务全部由 Docker 部署,这样不需要安装,只需要关注配置。其次我们使用 nginx-proxy 来实现 reverse proxy 自动化配置,然后我们使用 docker-letsencrypt-nginx-proxy-companion 实现 Let‘s Encrypt,最后我们使用 Docker Compose 来编排管理所有服务。

具体架构如下:

enter image description here

Preparation

  1. 一台安装有 Docker 和 Docker Compose 的 VPS,其中 80 和 443 需要允许访问
  2. 一个域名,并且域名 DNS 解析到 VPS 的 IP

假设我们的域名为: mengxin.science
VPS IP 为: 192.168.1.111
我们DNS增加如下记录:

  1. 根域名: @ 192.168.1.111
  2. test子域名:test 192.168.1.111
  3. nexus子域名:nexus 192.168.1.111

Nginx-proxy & Let’s Encrypt

在 VPS 上,首先我们需要通过 Docker Compose 部署环境,已经有人分享了 compose 的编排配置:docker-compose-letsencrypt-nginx-proxy-companion。所以根据其 README 可以快速的部署一套环境:

  1. docker-gen: 负责生成配置文件
  2. nginx-proxy: 负责 Nginx 反向代理
  3. letsencrypt-nginx-proxy-companion:负责注册证书

docker-gen

It can be used to generate various kinds of files for:

  • Centralized logging - fluentd, logstash or other centralized logging tools that tail the containers JSON log file or files within the container.
  • Log Rotation - logrotate files to rotate container JSON log files
  • Reverse Proxy Configs - nginx, haproxy, etc. reverse proxy configs to route requests from the host to containers
  • Service Discovery - Scripts (python, bash, etc..) to register containers within etcd, hipache, etc..

nginx-proxy

这里实际上并没有使用 nginx-proxy docker 镜像,而是使用的 nginx 的 template。

letsencrypt-nginx-proxy-companion

enter image description here

Nexus Compose

有了这个基础环境,我们部署一个应用就非常简单,这里我们部署一个 nexus repository 服务器,这个例子相对较为复杂,所以通过这个例子我们可以在现有基础上进行自定义。

需求

  1. 首先我们需要部署一个nexus 服务器,域名为 nexus.mengxin.science,内部端口默认 8081
  2. 我们需要支持 docker 的连接,内部 connector 端口设置为 8082

Docker Compose 编排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: "3"
services:
nexus3:
image: sonatype/nexus3
container_name: nexus3
expose:
- "8081"
- "8082"
environment:
- VIRTUAL_HOST=nexus.mengxin.science
- VIRTUAL_PORT=8081
- LETSENCRYPT_HOST=nexus.mengxin.science
- [email protected]
volumes:
- ./nexus-data:/nexus-data

# restart: always
networks:
default:
external:
name: webproxy

这里有几个关键点:

外部网络设置

如果我们希望这个应用使用我们的环境部署,就需要加入到其网络中,所以我们使用外部网络指定配置来配置网络为环境网络,其名称这里是默认,如果你修改了网络名,这里也要相对应的更改。具体配置参考: https://docs.docker.com/compose/networking/#use-a-pre-existing-network

1
2
3
4
networks:
default:
external:
name: webproxy

VIRTUAL_HOST & VIRTUAL_PORT

1
2
- VIRTUAL_HOST=nexus.mengxin.science
- VIRTUAL_PORT=8081

这两个配置是使用 nginx 自动 proxy 应用的关键,通过这两个配置,我们会发现先一旦应用启动后, nginx-web 的配置文件会更新为:

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
# nexus.mengxin.science
upstream nexus.mengxin.science {
## Can be connected with "webproxy" network
# nexus3
server 192.168.48.5:8081;
}
server {
server_name nexus.mengxin.science;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
return 301 https://$host$request_uri;
}
server {
server_name nexus.mengxin.science;
listen 443 ssl http2 ;
access_log /var/log/nginx/access.log vhost;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_certificate /etc/nginx/certs/nexus.mengxin.science.crt;
ssl_certificate_key /etc/nginx/certs/nexus.mengxin.science.key;
ssl_dhparam /etc/nginx/certs/nexus.mengxin.science.dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/certs/nexus.mengxin.science.chain.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
include /etc/nginx/vhost.d/nexus.mengxin.science;
location / {
proxy_pass http://nexus.mengxin.science;
}
}

这里会增加一个 upstream nexus.mengxin.science 然后将 location / proxy 到 http://nexus.mengxin.science;。 这里需要了解一下基本的 Nginx 知识。实际就是实现,当我们访问 nexus.mengxin.science,nginx 首先找到: server 配置为 server_name nexus.mengxin.science; 的配置项 (这里我们先忽略 SSL),然后根据配置将请求 proxy 到指定的 8081 端口,从而将请求送达 nexus repository service。

VIRTUAL_PORT 默认是 80, 如果不设置,默认 proxy 到 80,在 nginx 中也不需要显示的配置。

LETSENCRYPT_HOST & LETSENCRYPT_EMAIL

1
2
- LETSENCRYPT_HOST=nexus.mengxin.science
- [email protected]

这两个配置使用用来获取 SSL 证书,并且生成 nginx 的 SSL 相关配置。我们可以在上面的生成的 nginx 的配置中看到,80 端口的访问直接重定向到 443 端口了。443 端口配置了证书等信息。

自定义 VHOST NGINX 配置

我们需求中有一条是支持 docker,但是在 nexus repository 中,docker 有独立的端口,所以目前的配置不足以完成 docker 请求的 proxy。所以我们需要自定义。

我们可以参考 nginx-proxy 的文档 per-virtual_host 部分: https://github.com/jwilder/nginx-proxy#per-virtual_host

原理很简单,因为在自动生成的 ngxin 配置中有一句配置:

1
include /etc/nginx/vhost.d/nexus.mengxin.science;

如果我们观察 nginx-data 目录 (这是我们配置环境的时候将 ngxin 的配置映射出来的目录,里面包含所有的 nginx 的配置),其中有一个 vhost.d 的子目录,这个子目录中默认只有一个 default 的文件,如果我们保持这个目录为 default,上面的配置就会是:

1
include /etc/nginx/vhost.d/default;

所以我们想自定义 nginx 配置有两个途径

  1. 修改 default,所有的没有单独配置 vhost 的 server 配置都会更改
  2. 添加一个和 VHOST 名字相同的文件在 vhost.d 中,这样生成的 ngxin 的配置文件就是默认 include 这个文件,这样我们就实现了为每个服务单独配置。

这里我们只需要对 nexus.mengxin.science 进行配置,所以我们添加 nexus.mengxin.sciencevhost.d 文件中。具体配置如下:

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
## Start of configuration add by letsencrypt container
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
auth_request off;
allow all;
root /usr/share/nginx/html;
try_files $uri =404;
break;
}
## End of configuration add by letsencrypt container

keepalive_timeout 5 5;
# allow large uploads
client_max_body_size 1G;
location = / {
# redirect to docker registry
if ($http_user_agent ~ docker ) {
proxy_pass http://nexus3:8082;
}
proxy_pass http://nexus3:8081;
#proxy_set_header Host $host;
#proxy_set_header X-Real-IP $remote_addr;
#proxq_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~* /v2(.*) {
proxy_pass http://nexus3:8082;
}

这个配置是参考了一篇文章:
Setting up a Docker Private Registry with Authentication Using Nexus and Nginx

配置思路就是如果访问来自 docker (使用 agent 判断),那么服务就 proxy 到 nexus docker connector 的端口 (8082),否则就到服务端口 8081。另外为了访问和测试 docker v2 api,我们把 v2 路径也 proxy 到 8082,当然如果有其他的服务使用 v2 路径,这个配置需要修改,目前没有在 nexus 中发现这个路径。

其中 nexus3 是我们在 前面 docker-compose 配置中配置的 service 名称,在 Docker 同一个网络中,可以用来访问指定服务,原理就像我们在内网中每台电脑都有一个主机名,可以用来代替 IP 访问。

这里还需要注意 Nginx 配置路径的问题,因为顶层的配置已经包含了 / 路径,所以不能再使用想用的配置了。我们需要简单的了解一下 nginx 配置路径的方法和优先级。

最后附一张 Nexus Docker Connector 的配置,我们只需要 http 端口即可

Ref: Nginx

location [ = | ~ | ~* | ^~ ] uri { … }
location @name { … }

语法规则很简单,一个location关键字,后面跟着可选的修饰符,后面是要匹配的字符,花括号中是要执行的操作。

修饰符

  • = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
  • ~ 表示该规则是使用正则定义的,区分大小写。
  • ~* 表示该规则是使用正则定义的,不区分大小写。
  • ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

匹配过程

对请求的url序列化。例如,对%xx等字符进行解码,去除url中多个相连的/,解析url中的.,..等。这一步是匹配的前置工作。

location有两种表示形式,一种是使用前缀字符,一种是使用正则。如果是正则的话,前面有~或~*修饰符。

具体的匹配过程如下:

首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来。

如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。

然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。

如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。

基于以上的匹配过程,我们可以得到以下两点启示:

  1. 使用正则定义的location在配置文件中出现的顺序很重要。因为找到第一个匹配的正则后,查找就停止了,后面定义的正则就是再匹配也没有机会了。

  2. 使用精确匹配可以提高查找的速度。例如经常请求/的话,可以使用=来定义location。

示例

接下来我们以一个例子来具体说明一下匹配过程。

假如我们有下面的一段配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location = / {
[ configuration A ]
}

location / {
[ configuration B ]
}

location /user/ {
[ configuration C ]
}

location ^~ /images/ {
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}

请求/精准匹配A,不再往下查找。

请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

请求/user/index.html匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。

请求/user/1.jpg匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

请求/images/1.jpg匹配D。首先进行前缀字符的查找,找到最长匹配D。但是,特殊的是它使用了^~修饰符,不再进行接下来的正则的匹配查找,因此使用D。这里,如果没有前面的修饰符,其实最终的匹配是E。大家可以想一想为什么。

请求/documents/about.html匹配B。因为B表示任何以/开头的URL都匹配。在上面的配置中,只有B能满足,所以匹配B。

location @name的用法

@用来定义一个命名location。主要用于内部重定向,不能用来处理正常的请求。其用法如下:

1
2
3
4
5
6
7
location / {
try_files $uri $uri/ @custom
}

location @custom {
# ...do something
}

上例中,当尝试访问url找不到对应的文件就重定向到我们自定义的命名location(此处为custom)。

值得注意的是,命名location中不能再嵌套其它的命名location。

URL尾部的/需不需要

关于URL尾部的/有三点也需要说明一下。第一点与location配置有关,其他两点无关。

  1. location中的字符有没有/都没有影响。也就是说/user/和/user是一样的。

  2. 如果URL结构是https://domain.com/的形式,尾部有没有/都不会造成重定向。因为浏览器在发起请求的时候,默认加上了/。虽然很多浏览器在地址栏里也不会显示/。这一点,可以访问[baidu](https://www.baidu.com/)验证一下。

  3. 如果URL的结构是https://domain.com/some-dir/。尾部如果缺少/将导致重定向。因为根据约定,URL尾部的/表示目录,没有/表示文件。所以访问/some-dir/时,服务器会自动去该目录下找对应的默认文件。如果访问/some-dir的话,服务器会先去找some-dir文件,找不到的话会将some-dir当成目录,重定向到/some-dir/,去该目录下找默认文件。可以去测试一下你的网站是不是这样的。

总结

location的配置有两种形式,前缀字符和正则。查找匹配的时候,先查找前缀字符,选择最长匹配项,再查找正则。正则的优先级高于前缀字符。

正则的查找是按照在配置文件中的顺序进行的。因此正则的顺序很重要,建议越精细的放的越靠前。

使用=精准匹配可以加快查找的顺序,如果根域名经常被访问的话建议使用=。

Summary

  1. https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion/blob/master/.github/README.md
  2. https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/wiki/Docker-Compose
  3. https://github.com/jwilder/nginx-proxy