这篇文章只讨论新建 Nexus Repository 3,不包含 Nexus 2 数据迁移。先把一套干净的 Nexus 3 跑稳定,再进入下一篇迁移教程,排障边界会更清晰。

系列导航:Kubernetes 重建迁移教程系列。本篇是 Nexus 迁移的前置教程。

示例使用单实例 StatefulSet、外部 PostgreSQL 和持久卷。高可用多实例涉及授权与官方 HA 架构,不能简单把 replicas 从 1 改成 3。

最终结构

flowchart LR
  A["Maven / npm / Docker 客户端"] --> B["Ingress 或内部 VIP"]
  B --> C["nexus Service"]
  C --> D["Nexus StatefulSet 单实例"]
  D --> E["PVC /nexus-data"]
  D --> F["PostgreSQL"]
  G["备份任务"] --> E
  G --> F

Nexus 官方容器把配置、日志和制品数据放在 /nexus-data,容器进程使用 UID 200。这个目录必须持久化并可写。参考官方容器镜像说明。

第 1 步:确定版本、容量和上下文路径

先冻结以下值:

NEXUS_VERSION=<approved-version>
NEXUS_IMAGE=<image-registry>/sonatype/nexus3:${NEXUS_VERSION}
NEXUS_HOST=repo.example.com
NEXUS_CONTEXT=/nexus

不要使用 latest。把镜像同步到内部仓库后记录 digest:

crane digest "$NEXUS_IMAGE"

容量至少考虑:

  • 当前 hosted 制品总量。
  • proxy cache 的上限和清理策略。
  • 数据库、日志、任务临时空间。
  • 备份保留周期。
  • 未来一到两年的增长量。

第 2 步:创建命名空间和 PostgreSQL 数据库

kubectl create namespace repository-system

PostgreSQL 建议由独立数据库平台提供并完成备份。创建专用账号和数据库:

CREATE ROLE nexus LOGIN PASSWORD '<generated-password>';
CREATE DATABASE nexus OWNER nexus ENCODING 'UTF8';

验证 Nexus 节点能连接数据库:

kubectl -n repository-system run pg-check --rm -it --restart=Never \
  --image=<approved-postgres-client-image> -- \
  psql 'postgresql://nexus:<password>@<postgres-host>:5432/nexus' \
  -c 'select current_database(), current_user;'

密码不要出现在 shell history 中。正式环境使用交互输入、临时环境变量或 Secret 管理系统。

第 3 步:创建数据库 Secret

kubectl -n repository-system create secret generic nexus-database \
  --from-literal=jdbc-url='jdbc:postgresql://<postgres-host>:5432/nexus' \
  --from-literal=username='nexus' \
  --from-literal=password='<generated-password>'

官方支持用环境变量、JVM 参数或属性文件配置 PostgreSQL,但不建议混用。容器环境中直接使用环境变量最清晰。参考使用 PostgreSQL 安装 Nexus Repository。

第 4 步:创建 PVC

保存为 00-storage.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nexus-data
  namespace: repository-system
spec:
  storageClassName: <storage-class>
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: <planned-size>

应用并确认绑定:

kubectl apply -f 00-storage.yaml
kubectl -n repository-system get pvc nexus-data -w

PVC 必须显示 Bound。如果是静态本地卷,还要给 Nexus Pod 添加 nodeAffinity,并把节点故障恢复写进 Runbook。

第 5 步:创建 Service

保存为 01-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nexus
  namespace: repository-system
spec:
  selector:
    app.kubernetes.io/name: nexus
  ports:
    - name: http
      port: 8081
      targetPort: http
kubectl apply -f 01-service.yaml

后续若为 Docker hosted/group 仓库分配独立端口,应在 Service 和容器端口中显式添加,不要与管理端口混淆。

第 6 步:创建 StatefulSet

保存为 02-statefulset.yaml:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nexus
  namespace: repository-system
spec:
  serviceName: nexus
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: nexus
  template:
    metadata:
      labels:
        app.kubernetes.io/name: nexus
    spec:
      terminationGracePeriodSeconds: 120
      securityContext:
        fsGroup: 200
        fsGroupChangePolicy: OnRootMismatch
      containers:
        - name: nexus
          image: <image-registry>/sonatype/nexus3:<approved-version>@sha256:<digest>
          ports:
            - { name: http, containerPort: 8081 }
          env:
            - { name: NEXUS_CONTEXT, value: /nexus }
            - name: NEXUS_DATASTORE_NEXUS_JDBCURL
              valueFrom:
                { secretKeyRef: { name: nexus-database, key: jdbc-url } }
            - name: NEXUS_DATASTORE_NEXUS_USERNAME
              valueFrom:
                { secretKeyRef: { name: nexus-database, key: username } }
            - name: NEXUS_DATASTORE_NEXUS_PASSWORD
              valueFrom:
                { secretKeyRef: { name: nexus-database, key: password } }
            - name: INSTALL4J_ADD_VM_PARAMS
              value: >-
                -Xms2g -Xmx2g -XX:MaxDirectMemorySize=2g
                -Djava.util.prefs.userRoot=/nexus-data/javaprefs
          resources:
            requests: { cpu: "2", memory: 4Gi }
            limits: { cpu: "4", memory: 6Gi }
          startupProbe:
            httpGet: { path: /nexus/service/rest/v1/status, port: http }
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 60
          readinessProbe:
            httpGet: { path: /nexus/service/rest/v1/status, port: http }
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 6
          livenessProbe:
            httpGet: { path: /nexus/service/rest/v1/status, port: http }
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 6
          volumeMounts:
            - { name: data, mountPath: /nexus-data }
      volumes:
        - name: data
          persistentVolumeClaim: { claimName: nexus-data }

资源只是起点,应结合索引规模、并发请求和官方系统要求压测后再调整。不要把 JVM 堆直接设置成容器内存上限。

应用前验证:

kubectl apply --server-side --dry-run=server -f 02-statefulset.yaml
kubectl apply -f 02-statefulset.yaml
kubectl -n repository-system rollout status statefulset/nexus --timeout=20m

首次启动可能需要数分钟。startupProbe 应覆盖数据库初始化和插件加载时间,readinessProbe 不应过早放行流量。

第 7 步:排查启动失败

kubectl -n repository-system get pod nexus-0 -o wide
kubectl -n repository-system describe pod nexus-0
kubectl -n repository-system logs nexus-0 --tail=300
kubectl -n repository-system get endpointslice \
  -l kubernetes.io/service-name=nexus -o wide

常见原因:

  • PVC 未绑定,或目录对 UID 200 不可写。
  • PostgreSQL 地址、账号或 TLS 参数错误。
  • 探针路径没有包含 /nexus 上下文。
  • 容器内存太小导致 OOMKilled。
  • 数据库 schema 已被不兼容版本初始化。

connection refused 表示端口尚未监听,首次启动期间应看应用日志,而不是不停删除 Pod。

第 8 步:读取首次管理员密码

kubectl -n repository-system exec nexus-0 -- \
  sh -c 'test -f /nexus-data/admin.password && cat /nexus-data/admin.password'

第一次登录后:

  1. 立即修改管理员密码并保存到密码管理器。
  2. 创建个人管理员账号,避免共享 admin。
  3. 根据安全要求决定是否允许匿名下载。
  4. 配置邮件、审计和清理任务。
  5. 确认 admin.password 文件已被 Nexus 删除或失效。

不要把初始密码写进 Wiki、Git 或聊天记录。

第 9 步:创建 Ingress

保存为 03-ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nexus
  namespace: repository-system
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [repo.example.com]
      secretName: repo-example-com-tls
  rules:
    - host: repo.example.com
      http:
        paths:
          - path: /nexus
            pathType: Prefix
            backend:
              service:
                name: nexus
                port: { number: 8081 }

先不改公共 DNS,用 --resolve 测试:

curl -kfsS --resolve repo.example.com:443:<ingress-address> \
  https://repo.example.com/nexus/service/rest/v1/status

预期返回可用状态。上传大制品前,再验证 Ingress、负载均衡器和 CDN 都没有请求体限制。

第 10 步:创建 Maven 仓库并验证

在 UI 创建:

  • maven-releases:hosted,Version policy 为 Release。
  • maven-snapshots:hosted,Version policy 为 Snapshot。
  • maven-central:proxy,远端为 Maven Central。
  • maven-public:group,包含以上仓库。

客户端仓库地址:

<repository>
  <id>nexus-public</id>
  <url>https://repo.example.com/nexus/repository/maven-public/</url>
</repository>

发布地址:

<distributionManagement>
  <repository>
    <id>nexus-releases</id>
    <url>https://repo.example.com/nexus/repository/maven-releases/</url>
  </repository>
  <snapshotRepository>
    <id>nexus-snapshots</id>
    <url>https://repo.example.com/nexus/repository/maven-snapshots/</url>
  </snapshotRepository>
</distributionManagement>

凭据放在用户 settings.xml 或 CI Secret 中,不要写进项目 POM。

使用一个临时 Maven 项目执行:

mvn -U dependency:get -Dartifact=<group>:<artifact>:<version>
mvn deploy

第 11 步:配置备份与清理任务

备份必须同时覆盖:

  • PostgreSQL 一致性备份。
  • /nexus-data 中的 blob、配置和密钥材料。
  • Kubernetes 清单与 Secret 管理系统中的引用。

数据库与 blob 的备份时间点要协调,否则元数据和文件可能不一致。proxy cache 可设置清理策略,但 hosted 制品删除必须有保留和审计规则。

完成定义

  • StatefulSet 重建后使用同一 PVC 正常恢复。
  • PostgreSQL 和 /nexus-data 均完成恢复演练。
  • Service、Ingress、TLS 和上下文路径一致。
  • Maven 下载、Release 发布、Snapshot 发布均成功。
  • 管理员初始密码已轮换,CI 使用独立最小权限账号。
  • 镜像版本和 digest 已固定,升级回滚步骤已记录。

完成干净部署后,再进入 Nexus 2 到 Nexus 3 的数据迁移,不要把“新平台搭建”和“历史数据导入”混成一次不可回退的操作。