---
url: /article/6emcwc0t/index.md
---
## 前言

本地开发项目经常需要修改代码，之后重新构建镜像，然后推送到镜像仓库，接着在部署机上面拉取新镜像重建容器。但是默认情况下是推送到 DockerHub 的，每次走公网实在是没有必要，因此在内网中部署一个镜像仓库是很有必要的。

## 1. 反向代理

因为 docker 默认是不信任 http 请求的，可以修改配置让它信任，但是我的内网环境下已经有了 Caddy 反代服务，并且也有了 https 证书，所以就规范一些，分配一个子域名给镜像仓库服务：`dcoker.mayee.example.com`。反代配置如下(192.168.0.100:5000 为镜像仓库的地址)：

```plain title="Caddyfile"
# 原有配置略 ...
dcoker.mayee.example.com {
    # 部署镜像仓库服务(Registry)的 ip 和 port
    reverse_proxy 192.168.0.100:5000 {
        # 关键配置：传递 Host 和 Proto 头
        header_up Host {host}
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Proto {scheme}
    }
}
# 原有配置略 ...
```

然后重启 Caddy 容器，使配置生效。

## 2. 部署镜像仓库

由于我的需求是纯内网环境下，因此不考虑安全和鉴权的问题，简单使用即可。所以选择了[registry](https://hub.docker.com/_/registry)和相应的 UI 服务[docker-registry-ui](https://hub.docker.com/r/joxit/docker-registry-ui)。

```yaml title="compose.yaml"
version: '3'

services:
  registry:
    image: registry:latest
    container_name: registry
    restart: always
    ports:
      - "5000:5000"
    environment:
      # ====== 基础配置 ======
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_HTTP_SECRET: "mayee"
      # ====== 日志与监控优化 ======
      OTEL_TRACES_EXPORTER: "none"
      REGISTRY_LOG_LEVEL: "warn"
      # ====== ⚠️ CORS 跨域配置 (UI 必须) ======
      # 允许所有域名访问 (内网环境为了方便直接开 *)
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: "['*']"
      # 允许的方法
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: "['HEAD', 'GET', 'OPTIONS', 'DELETE']"
      # 允许的 Header
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: "['Authorization', 'Accept', 'Cache-Control']"
      # 允许前端读取的 Header (比如读取 Docker 的 Digest)
      REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: "['Docker-Content-Digest']"

    volumes:
      - ./data:/var/lib/registry

  registry-ui:
    image: joxit/docker-registry-ui:main
    container_name: registry-ui
    restart: always
    ports:
      - "15000:80"
    environment:
      - REGISTRY_URL=https://dcoker.mayee.example.com
      - REGISTRY_TITLE=Mayee Registry
      - DELETE_IMAGES=true
      - SINGLE_REGISTRY=true
    depends_on:
      - registry
```

运行起来即可，访问部署机`http://192.168.0.100:15000`即可看到 ui 页面。

这里我们允许通过页面删除镜像，但是 Docker Registry 的一个经典机制：UI 上点击删除，只是删除了“引用”（Tag 和 Manifest），也就是相当于删除了文件的“快捷方式”，但实际的数据块（Blobs/Layers）依然躺在硬盘里占用空间。Docker 官方这样设计是为了防止并发读写时的冲突，但对于我们个人 NAS 来说，这就是单纯的“占着茅坑不拉屎”，所以得“办了它”。

可以直接手动执行`docker exec -i registry bin/registry garbage-collect --delete-untagged /etc/docker/registry/config.yml`，如果提示`/etc/docker/registry/config.yml`文件找不到，可以用`docker exec -it registry find / -name "config.yml"`命令找出文件的位置。例如我的文件真实位置在`/etc/distribution/config.yml`，替换为真实文件位置后再次执行。

但总是手动执行是麻烦的，可以用定时任务来执行。而有的系统，例如：飞牛(FnOS)。它安装的 docker 不是 root 用户组的，导致在普通用户下执行 docker 命令需要加 sudo。我们也可以把 docker 加入到 root 组，但是为了不破坏系统原本的设计用意，还是选择以 root 身份来定时执行脚本。

1. 切换到 root 用户`sudo -i`。
2. 创建任务脚本`/root/docker-gc.sh`。

```bash title="docker-gc.sh"
#!/bin/bash
echo "=========================================="
echo "Starting Docker Registry GC at $(date)"
# 1. 执行垃圾回收
# --delete-untagged: 清理未被标签引用的孤儿层（慎用，但在个人开发环境通常很有用）
/usr/bin/docker exec -i registry bin/registry garbage-collect --delete-untagged /etc/distribution/config.yml
# 2. (可选) 重启 Registry 容器
# 某些版本的 Registry 在 GC 后如果不重启，统计数据可能不更新，或者不释放文件句柄
/usr/bin/docker restart registry
echo "Finished Docker Registry at $(date)"
echo "=========================================="
```

3. 赋予执行权限`chmod +x /root/docker-gc.sh`。
4. 创建定时任务，执行`crontab -e`会打开定时任务的 nano 编辑器，在新的一行添加`0 4 * * 1 /bin/bash /root/docker-gc.sh >> /var/log/registry-gc.log 2>&1，就会在`每周一的凌晨 4:00 整执行一次`，如果推送到比较频繁，可以调小执行周期。之后按下`ctrl+x`和`y\`保存即可。
5. 退出 root 用户\`exit。

这样一来，当通过 ui 页面删除了镜像，那些镜像就成了没有标签的孤儿层，之后脚本触发时会扫描到这些镜像，然后从硬盘上真正删除。

## 3. 开发中使用

由于 docker 的网站被墙了，通常需要配置国内镜像源。开发机上可以编辑`/etc/docker/daemon.json`，加上镜像源地址，保存后重启 docker `sudo systemctl restart docker`。开发构建镜像时，为了构建多平台镜像会使用`buildx`，但 buildx 构建过程是在一个独立的容器里进行的，它默认不读取宿主机的`daemon.json`，所以我们得给它一个配置用，配置好国内镜像源。

1. 在项目根目录下创建文件`buildkitd.toml`。

```toml title="buildkitd.toml"
debug = false

[registry."docker.io"]
  # 国内镜像源，或者你的阿里云加速地址
  mirrors = ["https://docker.m.daocloud.io", "https://dockerproxy.net"]

[registry."dcoker.mayee.example.com"]
  http = false
  insecure = false
```

2. 镜像构建脚本`docker.sh`，一次性推送到公有合私有仓库，也可以去掉推送公有仓库，只推私仓。

```bash title="docker.sh"
#!/bin/bash
set -e

echo "🔨 [1/2] 构建 Jar 包 ..."
# maven 离线模式打包，跳过测试
./mvnw clean package -o -DskipTests
if [ $? -ne 0 ]; then echo "❌ 构建 Jar 包失败"; exit 1; fi

echo "🐳 [2/2] 构建 Docker 镜像 ..."
# 确保创建了 buildx 实例 (只需运行一次，如果已存在会跳过)
docker buildx create --name builderx --use --driver docker-container --config buildkitd.toml 2>/dev/null || true
docker buildx inspect --bootstrap
# Buildx 特性，打多架构镜像必须直接 push 到仓库，不能保存在本地 images 列表
docker buildx build \
    --platform linux/arm64,linux/amd64 \
    -f Dockerfile \
    -t dcoker.mayee.example.com/mayeee/homebox:2.0.0 \
    -t dcoker.mayee.example.com/mayeee/homebox:latest \
    -t mayeee/homebox:2.0.0 \
    -t mayeee/homebox:latest \
    --push .
```

3. Dockerfile 示例。

```plain
FROM eclipse-temurin:25-jre-alpine
# 安装运行时依赖
RUN apk add --no-cache fping openssh-client openssl tzdata
# 创建运行目录
WORKDIR /app
# 从 builder 镜像拷贝 jar
COPY target/homebox.jar ./homebox.jar
# 暴露端口
EXPOSE 8000
# 启动命令
ENTRYPOINT ["java", "-jar", "homebox.jar"]
```

如此，即可实现纯内网环境下开发和部署私有项目，并且用国内镜像源无网络问题干扰。
