在实际项目里,很多团队都会遇到这样一类需求:代码不能直接依赖公网上的镜像仓库和依赖源,测试环境需要一个稳定、可控、可复现的内部网络空间,服务之间的调用不能靠 IP 硬编码,重启一次机器就全乱套。这个“内部网络空间”就是本文要讨论的私有内网环境。它不是某一款软件,而是一套由容器网络、内部域名解析、私有镜像仓库、私有依赖源、统一入口和可观测性共同组成的工程组合。本文将围绕六种可以落地的手法展开,从零搭建一套能在单机或小集群上跑起来的私有内网开发环境,每一步都给出完整配置和验证命令。读者对象是有一定 Linux 和 Docker 基础、希望把测试环境做得更规范的开发者,也适合刚接触容器编排、想找一个完整跟练项目的同学。跟着做完,你应当能理解每个组件的职责边界,知道为什么这么配,以及出问题该从哪一层查起。
1. 背景与核心概念
1.1 什么是私有内网环境
先给一个通俗的解释:私有内网环境就是团队自己圈出来的一块“网络自留地”。在这块地里,服务之间用固定的域名互相访问,镜像和依赖包从自己的仓库拉取,对外只留一个入口,日志和健康状态集中可查。它和公网环境最大的区别在于“边界可控”——谁能进来、进来能访问什么、数据存在哪里,都由我们自己决定。
从工程角度给一个更准确的定义:私有内网环境是在宿主机或主机集群之上,通过容器网络划分出的独立二层/三层地址空间,配合内部 DNS 完成服务发现,配合私有制品仓库完成依赖隔离,配合反向代理完成统一入口,最终形成的一套闭环的、不依赖外部网络的运行环境。
这里需要区分两个容易混淆的概念。容器网络是网络层的隔离,解决的是“服务能不能互通”;私有制品仓库是依赖层的隔离,解决的是“构建能不能成功”。很多同学只做了前者,结果容器之间能 ping 通,但 mvn package 依然卡在下载依赖上,就是因为只解决了一半问题。
1.2 为什么团队需要自己搭一套
最直接的原因是稳定性。公网仓库会限流、会抖动、会出现某个版本被下架的情况。一旦构建链路依赖了这些不确定因素,CI 就会变成“玄学失败”。把镜像和依赖沉淀到内网,构建时间从几分钟降到几十秒是很常见的收益。
第二个原因是可复现。内网仓库里的制品是“冻结”的,今天构建出来的镜像,三个月后还能用同样的依赖重新构建出一样的结果。这对于排查线上问题、做版本回溯非常关键。
第三个原因是安全边界。私有内网环境天然限制了访问来源,数据库、缓存、内部管理后台都不需要直接暴露到宿主机之外的网络。即使后面要做端口映射,也只需暴露一个统一入口,其余服务全部隐藏在内网地址段后面。需要注意的是,内网并不等于安全,仍然要做认证、最小权限和备份,这一点会在第 11 节展开。
1.3 六种手法总览
本文要讲的六个手法,按依赖顺序排列,后面每一步都建立在前一步之上:
| 序号 | 手法 | 解决的问题 | 核心组件 |
|---|---|---|---|
| 1 | 规划网段与自定义网络 | 地址不冲突、服务可定位 | Docker bridge network |
| 2 | 内部域名解析 | 摆脱 IP 硬编码 | CoreDNS |
| 3 | 私有镜像仓库 | 镜像不落公网 | registry + 自签证书 |
| 4 | 私有依赖源 | 依赖不走公网 | Nexus |
| 5 | 统一入口 | 只暴露一个端口 | Nginx 反向代理 |
| 6 | 健康检查与备份 | 环境可运维 | 探针 + 脚本 |
这六步的顺序不是随便排的。网络是一切的基础,DNS 依赖固定 IP,仓库依赖 DNS,反向代理依赖仓库里的镜像,健康检查和备份则依赖前面所有服务都跑起来了。所以跟练的时候建议严格按顺序来,跳步很容易在中间卡住却不知道是哪一层的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 机器与系统要求
本文示例在一台 Linux 机器上完成,配置建议 4 核 8G 起步,磁盘 100G 以上,因为 Nexus 和镜像仓库都会持续占用空间。操作系统使用主流的 Linux 发行版即可,命令以通用 POSIX 写法为主。如果你在 macOS 或 Windows 上跟练,Docker Desktop 也能跑通大部分步骤,但自签证书的信任路径和文件权限会有差异,需要按实际系统调整。
网络方面,需要确认宿主机上没有被占用的地址段。本文统一使用 172.30.0.0/24 作为内网地址段,这个网段在多数家庭和办公网络里不会冲突。如果你的环境里已经有服务在用这个段,请整体替换成其他私有段,例如 172.31.0.0/24 或 10.20.0.0/24,但替换时要保证后文所有配置文件同步修改,否则会出现“容器起来了但互相不通”的典型问题。
2.2 工具与版本说明
本文涉及的组件都是长期维护的开源项目,具体版本请按你所在环境的实际情况选择。示例中使用的镜像标签以官方仓库当前可用的稳定标签为准,如果拉取失败,请到对应项目的发布页确认最新稳定标签,不要直接使用来源不明的第三方镜像。
| 工具 | 用途 | 说明 |
|---|---|---|
| Docker Engine | 容器运行时 | 需支持自定义 bridge 网络与固定 IP |
| Docker Compose | 编排 | 使用 v2 语法,命令为 docker compose |
| CoreDNS | 内网 DNS | 镜像 coredns/coredns |
| registry | 私有镜像仓库 | 官方 registry 镜像 |
| Nexus 3 | 私有依赖源 | 支持 Maven / npm / PyPI 等格式 |
| Nginx | 反向代理 | 建议使用 alpine 版本减小体积 |
| openssl | 生成自签证书 | 系统自带即可 |
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。特别提醒一点:docker compose 和 docker-compose 是两个不同的命令,前者是 v2 插件,后者是 v1 独立二进制,语法有细微差别。本文全部使用 v2 写法,如果你执行时报 command not found,说明你的环境只有 v1,需要按 v1 的写法调整,或者升级到 v2。
2.3 目录结构
为了让配置可维护,本文使用统一的目录结构。所有内容放在一个根目录下,每个服务一个子目录,证书单独存放。这样做的好处是备份时只需要打包一个目录,迁移时整体拷贝即可。
bash复制private-net/
├── docker-compose.yml # 主编排文件
├── certs/ # 自签证书目录
│ ├── ca.crt
│ ├── ca.key
│ ├── registry.crt
│ └── registry.key
├── coredns/
│ └── Corefile # DNS 配置
├── registry/
│ └── config.yml # 仓库配置
├── nginx/
│ └── conf.d/
│ └── default.conf # 反向代理配置
├── nexus/
│ └── data/ # Nexus 持久化数据
└── scripts/
├── healthcheck.sh # 健康检查脚本
└── backup.sh # 备份脚本
创建这个结构的命令很简单,一条 mkdir -p 就能搞定。这里需要注意 nexus/data 目录的权限问题,Nexus 容器内的运行用户对目录有写权限要求,如果宿主机上目录属主不对,启动后会出现“数据目录不可写”的报错。建议先创建目录,启动一次容器让它自己初始化权限,而不是提前手工 chown 成任意用户。
bash复制mkdir -p private-net/{certs,coredns,registry,nginx/conf.d,nexus/data,scripts}
cd private-net
3. 手法一:规划网段与自定义网络
3.1 为什么要先规划网段
Docker 默认会创建 bridge 网络,地址段通常是 172.17.0.0/16。这个默认网络有两个问题:一是不支持通过服务名做稳定解析之外的固定 IP 分配,二是所有没指定网络的容器都会挤在里面,地址分配不可控。当你想给仓库配一个固定的内部域名时,IP 一变域名就失效了。
所以第一步不是装软件,而是先声明一个自己管理的网络,并给它一个明确的子网。这样后面每个服务的 IP 都是我们指定的,DNS 记录才不会失效。这也是很多人搭内网环境失败的第一个坑:先起了容器,后面才想起来要固定 IP,结果只能全部重建。
3.2 创建自定义桥接网络
自定义网络可以直接写在 Compose 文件里,由 Compose 负责创建,也可以在命令行手动创建。写在文件里的好处是“声明式”,换台机器 docker compose up 就自动建好,不依赖任何手工前置步骤。下面是网络声明的写法,注意 ipam 段里要显式给出 subnet 和 gateway。
yaml复制# 文件路径:private-net/docker-compose.yml(网络声明片段)
networks:
privnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.30.0.0/24
gateway: 172.30.0.1
driver: bridge 表示使用桥接模式,容器之间通过宿主机上的虚拟网桥互通。subnet 是分配的地址池,gateway 是网桥在宿主机侧的地址,容器访问外部网络时会经过它。这里需要注意的是,subnet 不能和宿主机已有的路由冲突,如果冲突,Compose 会直接报 Pool overlaps with other one on this address space,看到这个报错就说明你的地址段选重了,换一个段即可。
3.3 为服务分配固定 IP
有了网络之后,在每个服务的定义里通过 networks 的列表形式指定 ipv4_address,就能把 IP 钉死。下面的片段演示了四个核心服务的地址分配,后续所有配置都基于这张“地址表”。
yaml复制# 文件路径:private-net/docker-compose.yml(服务网络片段)
services:
coredns:
networks:
privnet:
ipv4_address: 172.30.0.13
registry:
networks:
privnet:
ipv4_address: 172.30.0.10
nexus:
networks:
privnet:
ipv4_address: 172.30.0.11
gateway:
networks:
privnet:
ipv4_address: 172.30.0.12
把这张表记下来会省很多事:172.30.0.10 是镜像仓库,172.30.0.11 是依赖源,172.30.0.12 是统一入口,172.30.0.13 是 DNS。后面写反向代理和证书的时候会反复用到。验证网络是否生效,可以起一个临时容器进去看 IP,或者直接用 docker network inspect,后者不需要起容器,更轻量。
bash复制docker network inspect private-net_privnet \
--format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
# 期望输出:172.30.0.0/24
4. 手法二:内网域名解析(CoreDNS)
4.1 为什么不用 hosts 写死
最省事的做法是给每个容器加 extra_hosts,或者直接改宿主机的 /etc/hosts。小规模确实能用,但一旦服务超过五六个,就会出现三个问题:一是每加一个服务要改 N 个文件,漏改一处就解析失败;二是 hosts 不支持泛解析,子域名场景没法覆盖;三是容器重建后 IP 变了,hosts 里的记录就过期了。
引入一个轻量 DNS 就是为了把这些映射关系收敛到一个地方。CoreDNS 编译出来只有几十兆,配置文件就是一个文本文件,改完重载即可,非常适合做内网解析。它不是必须的,但在“六个手法”体系里,它是把前一步的固定 IP 变成可读域名的关键一环。
4.2 Corefile 配置
CoreDNS 的核心配置叫 Corefile,语法是“区块 + 插件”。下面这份配置做了三件事:用 hosts 插件维护静态记录、用 forward 把非本域的请求转发出去、用 log 和 errors 插件输出排查信息。
text复制# 文件路径:private-net/coredns/Corefile
.:53 {
hosts {
172.30.0.10 registry.internal
172.30.0.11 nexus.internal
172.30.0.12 gateway.internal
172.30.0.13 dns.internal
fallthrough
}
forward . 223.5.5.5 1.1.1.1
log
errors
cache 30
}
逐行解释一下。.:53 表示监听所有域名的 53 端口。hosts 块里按“IP 域名”的格式写静态记录,fallthrough 的作用是:如果查询的域名不在这份静态列表里,就继续交给下一个插件处理,而不是直接返回 NXDOMAIN。forward 把剩余查询转发给上游解析器,这里配了两个地址做冗余,实际使用时请替换成你所在网络环境允许使用的解析服务。cache 30 表示缓存 30 秒,内网记录变动不频繁,适当缓存能减少查询压力。
这里有个常见误区:很多人忘了写 fallthrough,结果内网域名能解析,但访问任何外部域名都失败,容器里 apt 或 pip 直接卡死。原因就是 hosts 插件对未知域名默认返回空结果,不再往下传递。加上 fallthrough 才能实现“内网优先、外部兜底”的效果。
4.3 验证解析是否生效
配置写好后,先单独起 DNS 容器,再在另一个容器里用 nslookup 或 getent hosts 验证。用 Compose 起单个服务可以用 docker compose up -d coredns,注意这里会同时创建网络,符合我们“网络先行”的预期。
yaml复制# 文件路径:private-net/docker-compose.yml(coredns 服务片段)
services:
coredns:
image: coredns/coredns:latest
container_name: priv-coredns
restart: unless-stopped
command: -conf /etc/coredns/Corefile
volumes:
- ./coredns/Corefile:/etc/coredns/Corefile:ro
ports:
- "172.30.0.13:53:53/udp"
- "172.30.0.13:53:53/tcp"
networks:
privnet:
ipv4_address: 172.30.0.13
启动后验证:
bash复制docker run --rm --network private-net_privnet --dns 172.30.0.13 \
busybox:latest nslookup registry.internal
期望看到返回 172.30.0.10。如果返回超时,优先检查两件事:容器是否真的在运行(docker compose ps),以及 --dns 指定的地址是否和容器实际 IP 一致。这里用 busybox 只是因为它体积小、自带 nslookup,你也可以换成任意带解析工具的基础镜像。
5. 手法三:私有镜像仓库与自签证书
5.1 生成自签证书
私有仓库默认走 HTTP,但很多客户端工具和集群组件要求 HTTPS,而且内网传输也应该加密。正式环境建议使用内部 CA 签发的证书,本文演示自签证书的完整流程,目的是让你理解证书链是怎么串起来的。
先生成 CA 私钥和 CA 证书,再用 CA 去签仓库的服务端证书。注意服务端证书的 subjectAltName 必须包含你实际访问用的域名,否则浏览器和 Docker 客户端都会报证书不匹配。
bash复制# 1. 生成 CA 私钥与自签 CA 证书(有效期 3650 天)
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
-keyout certs/ca.key -out certs/ca.crt \
-subj "/C=CN/O=PrivateNet/CN=PrivateNet-CA"
# 2. 生成服务端私钥与证书签名请求
openssl req -newkey rsa:4096 -nodes \
-keyout certs/registry.key -out certs/registry.csr \
-subj "/C=CN/O=PrivateNet/CN=registry.internal"
# 3. 编写扩展配置,声明 SAN
cat > certs/registry.ext <<'EOF'
subjectAltName=DNS:registry.internal,IP:172.30.0.10
extendedKeyUsage=serverAuth
EOF
# 4. 用 CA 签发服务端证书
openssl x509 -req -in certs/registry.csr -CA certs/ca.crt -CAkey certs/ca.key \
-CAcreateserial -out certs/registry.crt -days 3650 \
-extfile certs/registry.ext
这段命令里最容易出错的是第 3 步。省略 subjectAltName 时,现代 TLS 客户端会忽略 CN 字段,直接判定证书无效。extendedKeyUsage=serverAuth 则明确这张证书只用于服务端身份验证,避免用途混淆。私钥文件权限建议设为 600,不要让它出现在任何镜像层里。
5.2 registry 配置与启动
registry 的配置文件是一个 YAML,主要声明监听地址、证书路径和存储路径。下面这份配置同时开了 HTTP 和 HTTPS,HTTP 只用于本机调试,正式使用只保留 HTTPS。
yaml复制# 文件路径:private-net/registry/config.yml
version: 0.1
log:
level: info
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: false
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
https:
addr: :5443
certificate: /certs/registry.crt
key: /certs/registry.key
delete.enabled: false 是刻意保留的默认值。开启删除后可以通过 API 删镜像,但删除操作不可逆且容易误删,建议在生产环境的仓库里保持关闭,清理工作走定期归档策略,而不是手工删除。headers 里加 nosniff 是一个低成本的加固措施,防止内容类型嗅探带来的风险。
对应的 Compose 服务定义如下:
yaml复制# 文件路径:private-net/docker-compose.yml(registry 服务片段)
services:
registry:
image: registry:2
container_name: priv-registry
restart: unless-stopped
volumes:
- ./registry/config.yml:/etc/docker/registry/config.yml:ro
- ./certs:/certs:ro
- registry-data:/var/lib/registry
networks:
privnet:
ipv4_address: 172.30.0.10
volumes:
registry-data:
这里用命名卷而不是绑定宿主机目录,是为了避免宿主机文件权限与容器内用户不一致导致的写入失败。如果你确实需要直接看到存储目录,可以改成绑定挂载,但要保证目录属主与容器内运行用户一致。
5.3 客户端信任与推送验证
服务端证书是自签的,客户端默认不信任,所以要让客户端把 CA 证书放进信任目录。Docker 的做法是在 /etc/docker/certs.d/<域名>:<端口>/ 下放一份 ca.crt。目录名必须和访问时使用的“域名:端口”完全一致,写错一个字符就会报 x509: certificate signed by unknown authority。
bash复制# 宿主机上信任 CA
sudo mkdir -p /etc/docker/certs.d/registry.internal:5443
sudo cp certs/ca.crt /etc/docker/certs.d/registry.internal:5443/ca.crt
# 把内网域名指向仓库 IP(客户端侧解析,见第 4 节说明)
echo "172.30.0.10 registry.internal" | sudo tee -a /etc/hosts
# 验证推送与拉取
docker tag busybox:latest registry.internal:5443/demo/busybox:v1
docker push registry.internal:5443/demo/busybox:v1
docker pull registry.internal:5443/demo/busybox:v1
推送成功会看到类似 v1: digest: sha256:... size: ... 的输出。如果卡在 Retrying,先 curl -k https://registry.internal:5443/v2/ 看看仓库本身是否响应,再检查证书 SAN 和信任目录。判断顺序很重要:先确认服务活着,再确认 TLS 通,最后确认认证和权限,这样可以避免在错误的方向上浪费时间。
6. 手法四:私有依赖源(Nexus)
6.1 Nexus 在内网里的角色
镜像解决的是“运行环境”的隔离,依赖源解决的是“构建过程”的隔离。Java 项目的 pom.xml 依赖、前端项目的 node_modules、Python 的 pip install,默认都会去公网拉取。把这些请求指向内网的 Nexus,就形成了一个代理加缓存的中间层:第一次拉取时 Nexus 去上游取并缓存,第二次开始全部命中本地缓存。
Nexus 支持多种仓库格式,常用的是 maven2(代理 Maven Central)、npm(代理 npm registry)、pypi(代理 Python 包)。每种格式通常需要建两个仓库:一个 proxy 类型负责代理上游,一个 group 类型负责把多个仓库聚合起来对外提供一个统一地址。客户端只需要配 group 的地址,后续要加新的上游源时不用改客户端配置,这是它比直接改地址更优雅的地方。
6.2 启动 Nexus 并完成初始化
Nexus 对内存有要求,建议给容器至少 2G 内存。首次启动需要初始化数据目录,耗时可能在一分钟以上,这期间健康检查会失败,属于正常现象,不要急着重启。
yaml复制# 文件路径:private-net/docker-compose.yml(nexus 服务片段)
services:
nexus:
image: sonatype/nexus3:latest
container_name: priv-nexus
restart: unless-stopped
volumes:
- ./nexus/data:/nexus-data
environment:
- INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx2048m -XX:MaxDirectMemorySize=2048m
networks:
privnet:
ipv4_address: 172.30.0.11
启动后先看日志确认初始化完成,再取管理员初始密码。Nexus 3 的管理员初始密码存放在数据目录下的 admin.password 文件里,第一次登录后系统会要求修改,修改完成后该文件会被删除,这是正常的安全设计。
bash复制docker compose up -d nexus
docker compose logs -f nexus # 看到 "Started Nexus Repository Manager" 即可
docker exec priv-nexus cat /nexus-data/admin.password
拿到密码后通过 http://<宿主机IP>:8081 登录,按向导修改密码。接着在界面里创建三个仓库:maven-proxy(类型 maven2,proxy)、npm-proxy(类型 npm,proxy)、pypi-proxy(类型 pypi,proxy),以及对应的 group 仓库。创建完成后,客户端就只需要指向 group 地址。
6.3 客户端指向内网源
配置客户端时,核心思路是“覆盖默认源地址 + 保留兜底”。下面分别给出 Maven、npm 和 pip 的最小配置示例。这三个文件的路径都是约定俗成的,放在项目根目录或用户目录下即可生效。
xml复制<!-- 文件路径:项目根目录/settings.xml(Maven 片段) -->
<settings>
<mirrors>
<mirror>
<id>nexus-internal</id>
<name>Internal Nexus</name>
<url>http://nexus.internal:8081/repository/maven-group/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
</settings>
ini复制# 文件路径:项目根目录/.npmrc
registry=http://nexus.internal:8081/repository/npm-group/
strict-ssl=false
ini复制# 文件路径:~/.pip/pip.conf
[global]
index-url = http://nexus.internal:8081/repository/pypi-proxy/simple/
trusted-host = nexus.internal
需要重点提醒的是 strict-ssl=false 和 trusted-host。这两个配置的作用是跳过 TLS 校验,只适合在内网 HTTP 场景下临时使用。如果后续给 Nexus 配了 HTTPS 证书,应当把这两项删掉,改为正确配置 CA 信任。长期保留跳过校验的配置会掩盖真实的证书问题,属于典型的“今天省事、明天踩坑”。
验证是否真的走了内网源,最简单的方法是清掉本地缓存后重新构建,观察 Nexus 界面的请求日志或缓存占用是否增长:
bash复制# Maven 验证
mvn -s settings.xml dependency:resolve -U
# npm 验证
npm config get registry
npm view lodash version
# pip 验证
pip download requests -d /tmp/pipcheck -v 2>&1 | head -20
7. 手法五:统一入口(Nginx 反向代理)
7.1 为什么需要统一入口
前面几步已经把仓库和依赖源都跑起来了,但它们各自监听不同端口,客户端要记一堆地址。更麻烦的是,每加一个服务就要多开一个宿主机端口,暴露面越来越大。统一入口解决的就是这个问题:所有服务共用 80/443,通过域名区分,宿主机上只映射一个端口。
这样做还有两个附带好处。第一,可以在入口层统一做访问日志、限流、基础认证,不用每个服务单独实现。第二,后续要加 HTTPS、加路径重写、加跨域头,都只需要改一处 Nginx 配置,服务本身完全不用动。
7.2 反向代理配置
下面这份配置以 server_name 区分不同内部域名,把请求转发到对应的容器地址。注意 proxy_set_header Host $host 这一行,很多应用会根据 Host 头生成绝对链接或做路由,不传会导致跳转地址错误。
nginx复制# 文件路径:private-net/nginx/conf.d/default.conf
server {
listen 80;
server_name registry.internal;
client_max_body_size 0;
location / {
proxy_pass http://172.30.0.10:5000;
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;
}
}
server {
listen 80;
server_name nexus.internal;
client_max_body_size 1024m;
location / {
proxy_pass http://172.30.0.11:8081;
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_read_timeout 300s;
}
}
client_max_body_size 0 表示不限制请求体大小,这对镜像推送是必要的,因为镜像层动辄几百兆。Nexus 那边给到 1024m 而不是无限,是为了留一个上限防止异常请求打满磁盘。proxy_read_timeout 300s 针对 Nexus 首次代理上游依赖时可能出现的长时间等待,默认 60 秒容易在冷缓存场景下超时中断。
对应的 Compose 服务定义:
yaml复制# 文件路径:private-net/docker-compose.yml(gateway 服务片段)
services:
gateway:
image: nginx:alpine
container_name: priv-gateway
restart: unless-stopped
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
ports:
- "8080:80"
networks:
privnet:
ipv4_address: 172.30.0.12
7.3 验证统一入口
验证分两步:先确认 Nginx 配置语法正确,再确认转发链路通。
bash复制docker exec priv-gateway nginx -t
curl -I -H "Host: registry.internal" http://127.0.0.1:8080/v2/
curl -I -H "Host: nexus.internal" http://127.0.0.1:8080/
第一条命令应输出 syntax is ok 和 test is successful。第二条命令如果返回 200 或 401,都说明转发成功——registry 在未认证时返回 401 是预期行为,不要误判为故障。这里用 -H "Host: ..." 模拟域名访问,是为了在不改本机 hosts 的情况下验证配置,这在排查阶段非常实用。
8. 手法六:健康检查、日志与备份
8.1 用健康检查判断真实可用
容器状态显示 Up 并不代表服务可用。Nexus 启动过程中进程在、端口没监听,这种情况很常见。Compose 支持 healthcheck,通过周期性执行命令判断服务是否真正就绪,depends_on 配合 condition 还能控制启动顺序。
yaml复制# 文件路径:private-net/docker-compose.yml(健康检查片段)
services:
registry:
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --no-check-certificate https://127.0.0.1:5443/v2/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
nexus:
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8081/ || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 120s
start_period 是给服务留的启动宽限期,这期间检查失败不计入 retries。Nexus 初始化慢,给 120 秒比较稳妥。注意不同基础镜像里可用的工具不一样,registry 镜像里通常没有 curl,但有 wget,所以在写检查命令前先 docker exec 进去确认一下工具是否存在,否则健康检查永远失败,容器会被反复重启。
8.2 日志与磁盘的日常观察
内网环境的日志不需要一上来就上 ELK 这类重型方案,先把“看日志”这件事变得顺手更重要。Docker 的 json-file 日志驱动默认不限制大小,长时间运行会把磁盘写满,这是最容易被忽略的运维隐患。建议在 Compose 里统一加上轮转配置。
yaml复制# 文件路径:private-net/docker-compose.yml(日志轮转片段)
services:
nexus:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
这段配置表示单个日志文件最大 50M,最多保留 5 个,也就是单服务最多占用 250M 日志空间。仓库类服务可以适当调大,DNS 这类低日志量的服务可以调小。日常排查时用 docker compose logs --tail=200 <服务名> 就够用,配合 grep 过滤关键字效率更高。
8.3 备份脚本
备份的原则是“备份配置 + 备份数据 + 验证可恢复”。配置是文本,直接打包即可;数据目录需要先停服务或者使用支持在线备份的方式,直接拷贝运行中的目录可能得到不一致的快照。
bash复制#!/usr/bin/env bash
# 文件路径:private-net/scripts/backup.sh
set -euo pipefail
BACKUP_DIR="/data/backup/private-net"
STAMP="$(date +%Y%m%d_%H%M%S)"
mkdir -p "${BACKUP_DIR}/${STAMP}"
# 1. 备份配置文件与证书
tar -czf "${BACKUP_DIR}/${STAMP}/config.tar.gz" \
docker-compose.yml coredns registry nginx certs
# 2. 暂停 nexus 后备份数据目录,保证一致性
docker compose stop nexus
tar -czf "${BACKUP_DIR}/${STAMP}/nexus-data.tar.gz" nexus/data
docker compose start nexus
# 3. 清理 14 天前的备份
find "${BACKUP_DIR}" -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
echo "backup done: ${BACKUP_DIR}/${STAMP}"
脚本里 set -euo pipefail 的作用是让任何一步失败都立刻终止,避免出现“配置备份成功但数据备份失败”却不自知的情况。stop 加 start 会导致 Nexus 短暂不可用,建议放在业务低峰期执行。更重要的是,备份脚本必须配一个恢复演练:每隔一段时间拿备份文件在测试机还原一次,只备份不演练等于没有备份。
9. 完整跟练:一键编排与验收
9.1 完整 Compose 文件
把前面各节的片段合并,就得到一份可以整体启动的编排文件。这里保留了所有关键配置项,方便你对照检查自己写的版本。
yaml复制# 文件路径:private-net/docker-compose.yml
services:
coredns:
image: coredns/coredns:latest
container_name: priv-coredns
restart: unless-stopped
command: -conf /etc/coredns/Corefile
volumes:
- ./coredns/Corefile:/etc/coredns/Corefile:ro
ports:
- "172.30.0.13:53:53/udp"
- "172.30.0.13:53:53/tcp"
networks:
privnet:
ipv4_address: 172.30.0.13
registry:
image: registry:2
container_name: priv-registry
restart: unless-stopped
volumes:
- ./registry/config.yml:/etc/docker/registry/config.yml:ro
- ./certs:/certs:ro
- registry-data:/var/lib/registry
networks:
privnet:
ipv4_address: 172.30.0.10
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --no-check-certificate https://127.0.0.1:5443/v2/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
nexus:
image: sonatype/nexus3:latest
container_name: priv-nexus
restart: unless-stopped
volumes:
- ./nexus/data:/nexus-data
environment:
- INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx2048m -XX:MaxDirectMemorySize=2048m
networks:
privnet:
ipv4_address: 172.30.0.11
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8081/ || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 120s
gateway:
image: nginx:alpine
container_name: priv-gateway
restart: unless-stopped
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
ports:
- "8080:80"
networks:
privnet:
ipv4_address: 172.30.0.12
depends_on:
- registry
- nexus
volumes:
registry-data:
networks:
privnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.30.0.0/24
gateway: 172.30.0.1
9.2 启动顺序与命令
启动时建议分两批:先起 DNS 和仓库这类“被依赖”的服务,再起入口。虽然 Compose 有 depends_on,但它默认只保证启动顺序,不保证依赖服务“就绪”,真正的就绪控制要靠健康检查配合 condition: service_healthy,这个写法在不同 Compose 版本支持程度不完全一致,所以更稳妥的做法是分批手动起。
bash复制# 第一批:网络、DNS、仓库、依赖源
docker compose up -d coredns registry nexus
# 等待健康检查通过
docker compose ps
# 第二批:统一入口
docker compose up -d gateway
# 查看整体状态
docker compose ps --format "table {{.Name}}\t{{.Status}}"
期望看到四个容器的状态都是 Up,其中 registry 和 nexus 显示 healthy。如果某个容器反复重启,用 docker compose logs --tail=100 <服务名> 看具体报错,不要盲目 down -v,那会连同数据卷一起删掉。
9.3 验收清单
启动完成后,按下面这张清单逐项确认,全部通过才算环境搭好:
| 验收项 | 命令 | 期望结果 |
|---|---|---|
| DNS 解析 | nslookup registry.internal 172.30.0.13 |
返回 172.30.0.10 |
| 仓库服务 | curl -k -I https://registry.internal:5443/v2/ |
200 或 401 |
| 依赖源 | curl -I http://nexus.internal:8081/ |
200 |
| 统一入口 | curl -I -H "Host: registry.internal" http://127.0.0.1:8080/v2/ |
200 或 401 |
| 镜像推送 | docker push registry.internal:5443/demo/busybox:v1 |
推送成功 |
| 依赖拉取 | mvn -s settings.xml dependency:resolve |
构建成功 |
这张清单的价值在于“分层验证”。DNS 不通就先别查仓库,仓库不通就别查 Nginx,逐层推进能省掉大量无谓的猜测。
10. 常见问题与排查思路
10.1 高频问题对照表
内网环境搭建过程中遇到的问题有很强的规律性,下面把最常见的几类整理成表,方便快速定位。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 启动报地址池重叠 | subnet 与宿主机已有路由冲突 | 换一个未使用的私有网段 |
| 容器间 ping 不通 | 未加入同一网络或 IP 被占用 | 检查 networks 声明与 ipv4_address 唯一性 |
| 内网域名解析超时 | 客户端未指定 DNS 或 UDP 53 被占用 | 检查 --dns 参数与端口占用 |
| 外部域名全部解析失败 | Corefile 缺少 fallthrough |
补上兜底转发配置 |
| 推送镜像报证书错误 | SAN 不匹配或信任目录名错误 | 重新签证书,核对 /etc/docker/certs.d/ 目录名 |
| Nexus 启动后立即退出 | 数据目录权限不足 | 让容器自行初始化目录,检查属主 |
| 健康检查一直失败 | 容器内没有对应命令 | 先 exec 进去确认工具是否存在 |
| 大镜像推送 413 | 入口层请求体限制过小 | 调大 client_max_body_size |
| 依赖拉取很慢 | 走了公网或冷缓存 | 确认客户端源地址指向内网 group |
10.2 分层排查方法
遇到问题时,最忌讳的是“从最外层开始猜”。推荐的排查顺序是从内往外:先确认容器网络通不通,再确认 DNS 能不能解析,然后确认服务端口是否监听,接着确认 TLS 是否握手成功,最后才看反向代理。
bash复制# 第 1 层:网络与容器
docker network inspect private-net_privnet --format '{{json .Containers}}'
# 第 2 层:DNS
docker run --rm --network private-net_privnet --dns 172.30.0.13 \
busybox:latest nslookup nexus.internal
# 第 3 层:端口
docker run --rm --network private-net_privnet busybox:latest \
nc -zv 172.30.0.11 8081
# 第 4 层:TLS
openssl s_client -connect 172.30.0.10:5443 -servername registry.internal </dev/null
# 第 5 层:入口
curl -v -H "Host: nexus.internal" http://127.0.0.1:8080/
每一层都能给出明确的“通/不通”结论,这样就不会出现“我觉得是 Nginx 的问题但改了三次都没好”的情况。openssl s_client 那一条尤其有用,它会直接打印证书链和 SAN 列表,证书配错一眼就能看出来。
10.3 错误示例与正确示例
有一个非常典型的问题值得单独讲:DNS 配置写了但解析不生效。错误的写法是这样的:
text复制# 错误示例:缺少 fallthrough,且上游地址不可达
.:53 {
hosts {
172.30.0.10 registry.internal
}
forward . 127.0.0.1
}
这段配置有两个问题。一是没有 fallthrough,非内网域名查不到就返回空;二是 forward 指向了 127.0.0.1,而容器内的 127.0.0.1 是容器自己,不是宿主机,必然转发失败。正确的写法在 4.2 节已经给出:补上 fallthrough,并把上游地址换成真实可达的解析服务。
另一个典型问题是镜像推送时的 413。很多人第一反应是改 registry 的配置,实际上请求在 Nginx 那一层就被拦下了,压根没到 registry。判断方法是看 Nginx 的访问日志里是否有 413 记录,如果有,改 client_max_body_size 就对了;如果日志里是 200 但客户端仍报错,才需要去查 registry。
11. 最佳实践与工程建议
11.1 安全边界与权限
私有内网环境不等于安全环境。第一,端口映射要收敛,宿主机上只暴露必要的入口端口,其余服务全部只在内网地址段可达。第二,仓库和依赖源必须开启认证,匿名可写的仓库等于把供应链交给任何人,尤其是私有镜像仓库,一旦被推送恶意镜像,所有拉取方都会中招。第三,证书私钥的权限要收紧到 600,并且不要放进任何镜像层或代码仓库。
还有一点容易被忽略:Nexus 的管理员账号不要在日常构建中使用,应该为 CI 单独创建只读权限的账号,需要发布的流水线再用有写权限的独立账号。这就是最小权限原则的落地方式,虽然多了一步配置,但能在出事时把影响范围限制住。
11.2 配置管理与版本化
所有配置文件都应该进版本控制,包括 Compose 文件、Corefile、Nginx 配置和 registry 配置。但证书私钥、管理员密码这类敏感信息不要进仓库,建议通过环境变量或外部密钥管理注入。如果团队规模不大,至少要做到“配置文件有变更记录”,这样某天环境坏了,可以对比上一个可用版本找出改了哪一行。
建议给内网环境也打上版本标签。比如每次变更后记录一条“环境版本 v1.3,变更内容:Nexus 升级、新增 pypi 代理”,配合备份文件的时间戳,回溯时效率会高很多。这件事看起来繁琐,真出问题时能省下几个小时。
11.3 可维护性与扩展方向
从单机环境往多机扩展时,最需要提前考虑的是“哪些东西是有状态的”。镜像仓库和 Nexus 的数据都是有状态的,扩容时不能简单加副本,需要考虑共享存储或者主从方案。DNS 是无状态的,可以多实例部署并通过上游负载均衡,但要注意内网客户端的 DNS 配置需要指向多个地址做冗余。
监控方面,建议至少采集三类指标:服务存活状态(健康检查结果)、磁盘使用率(仓库和 Nexus 最容易撑爆磁盘)、请求延迟(入口层的响应时间)。前两类是保命指标,第三类是体验指标。日志方面不必一上来就上重型方案,先把轮转配好、把关键错误打出来,就已经能覆盖大多数排查场景。
最后给一条实践建议:这套环境搭好之后,写一份“一页纸运维手册”,内容包括启动命令、停止命令、备份命令、常见故障处理入口和责任人。内网环境往往不是天天动,过三个月再来看,没有手册的话连从哪台机器登录都要想半天。
12. 学习路线与延伸实践
把本文的六个手法串起来看,其实是一条很清晰的链路:先划地址空间,再给地址起名字,然后放制品,最后收口到入口并补上运维能力。这条链路不只适用于单机 Docker,往上走一步就是容器编排平台的服务发现与配置管理,往下走一步就是更细的制品治理,比如镜像扫描、依赖漏洞检测、制品保留策略。
如果你想继续深入,建议按这个顺序推进:先把现有的 Compose 环境改造成带健康检查和自动重启的稳定版本,跑上一周观察日志和磁盘增长;然后尝试给 Nexus 加上 HTTPS 并去掉客户端的所有“跳过校验”配置;接着引入镜像清理策略,按标签规则保留最近若干个版本;最后再考虑把 DNS 和入口做成多实例,验证单点故障时的表现。每一步都能独立验证,不会一次性引入太多变量。
动手时优先关注三个风险点:一是地址段冲突,这在多团队共用一台机器时非常常见;二是数据目录的备份与恢复演练,只备份不恢复等于没做;三是证书有效期,自签证书同样会过期,建议在日历里设置提醒,或者在监控里加一条证书剩余天数告警。先把这三点守住,剩下的优化都可以慢慢来。环境搭好只是开始,能稳定跑住、出问题能快速定位,才是这套东西真正的价值。
