You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Docker推送私有仓库立即返回Connection Refused问题排查

问题描述

我正在基于Docker API V2规范实现一个简易HTTP Docker Registry,执行docker push 127.0.0.1:5000/debian时立刻报错:

Using default tag: latest
The push refers to repository [127.0.0.1:5000/debian]
Get "http://127.0.0.1:5000/v2/": dial tcp 127.0.0.1:5000: connect: connection refused

奇怪的是,用curl或浏览器访问http://127.0.0.1:5000/v2/完全正常;用官方镜像docker run -d -p 5000:5000 --name registry registry:2.7启动本地仓库时,推送也能成功。

以下是我用Go实现的代码:

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "strings"
    "time"

    "github.com/labstack/echo/v4"
)

var LayerPath string

func init() {
    LayerPath = GetTemporaryDirectory()
    fmt.Printf("Saving artifacts to %s\n", LayerPath)
}

func GetTemporaryDirectory() string {
    tempFolder, err := ioutil.TempDir("", "docker_registry")
    if err != nil {
        panic(err)
    }
    return tempFolder
}

func main() {
    e := echo.New()

    e.GET("/v2/*", GetFallback)
    e.PUT("/v2/*", PutFallback)
    e.POST("/v2/*", PostFallback)
    e.PATCH("/v2/*", PatchFallback)
    e.DELETE("/v2/*", DeleteFallback)
    e.GET("/v2", Root)
    e.HEAD("/v2/:name/blobs/:digest", Exists)
    e.GET("/v2/:name/blobs/:digest", GetLayer)
    e.POST("/v2/:name/blobs/uploads", StartUpload)
    e.PATCH("/v2/:name/blobs/uploads/:uuid", Upload)

    e.Start("127.0.0.1:5000")
}

func GetFallback(c echo.Context) error {
    return echo.NewHTTPError(http.StatusNotFound)
}

func PutFallback(c echo.Context) error {
    return echo.NewHTTPError(http.StatusNotFound)
}

func PostFallback(c echo.Context) error {
    return echo.NewHTTPError(http.StatusNotFound)
}

func PatchFallback(c echo.Context) error {
    return echo.NewHTTPError(http.StatusNotFound)
}

func DeleteFallback(c echo.Context) error {
    return echo.NewHTTPError(http.StatusNotFound)
}

func Root(c echo.Context) error {
    return c.String(http.StatusOK, "OK")
}

func Exists(c echo.Context) error {
    //name := c.Param("name")
    digest := c.Param("digest")
    hash := strings.Split(digest, ":")[1]

    if _, err := os.Stat(filepath.Join(LayerPath, hash)); err == nil {
        fileInfo, _ := os.Stat(filepath.Join(LayerPath, hash))
        c.Response().Header().Set("content-length", fmt.Sprintf("%d", fileInfo.Size()))
        c.Response().Header().Set("docker-content-digest", digest)
        return c.String(http.StatusOK, "OK")
    }

    return echo.NewHTTPError(http.StatusNotFound)
}

func GetLayer(c echo.Context) error {
    //name := c.Param("name")
    digest := c.Param("digest")
    hash := strings.Split(digest, ":")[1]
    path := filepath.Join(LayerPath, hash)

    if _, err := os.Stat(path); err == nil {
        fileInfo, _ := os.Stat(path)
        c.Response().Header().Set("content-length", fmt.Sprintf("%d", fileInfo.Size()))
        file, err := os.Open(path)
        if err != nil {
            return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
        }
        defer file.Close()
        return c.Stream(http.StatusOK, "application/octet-stream", file)
    }

    return echo.NewHTTPError(http.StatusNotFound)
}

func StartUpload(c echo.Context) error {
    name := c.Param("name")
    guid := generateUUID()
    c.Response().Header().Set("location", "/v2/"+name+"/blobs/uploads/"+guid)
    c.Response().Header().Set("range", "0-0")
    c.Response().Header().Set("content-length", "0")
    c.Response().Header().Set("docker-upload-uuid", guid)
    return c.NoContent(http.StatusAccepted)
}

func Upload(c echo.Context) error {
    name := c.Param("name")
    uuid := c.Param("uuid")
    start := c.Request().Header.Get("content-range")
    if start == "" {
        start = "0"
    }
    startPos, _ := strconv.ParseInt(strings.Split(start, "-")[0], 10, 64)

    file, err := os.OpenFile(filepath.Join(LayerPath, uuid), os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
    }
    defer file.Close()

    file.Seek(startPos, io.SeekStart)
    if _, err := io.Copy(file, c.Request().Body); err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
    }

    fileInfo, _ := file.Stat()
    c.Response().Header().Set("range", fmt.Sprintf("0-%d", fileInfo.Size()-1))
    c.Response().Header().Set("docker-upload-uuid", uuid)
    c.Response().Header().Set("location", "/v2/"+name+"/blobs/uploads/"+uuid)
    c.Response().Header().Set("content-length", "0")
    return c.NoContent(http.StatusNoContent)
}

func generateUUID() string {
    hash := sha256.New()
    hash.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
    return hex.EncodeToString(hash.Sum(nil))
}
问题排查与解决

核心原因:缺少HEAD请求处理

Docker客户端执行push时,首先会发送**HEAD /v2/请求**校验仓库可用性,但你的代码仅实现了GET /v2接口,未处理HEAD方法。当服务无法响应HEAD请求时,客户端会判定为连接失败,抛出"connection refused"错误。

curl和浏览器默认使用GET请求,所以能正常访问;官方registry镜像完整实现了Docker API V2的所有必要HTTP方法,包括HEAD,因此推送正常。

修复步骤

  1. 在Echo路由中添加HEAD /v2的处理,直接复用现有的Root函数即可:
    e.HEAD("/v2", Root)
    
  2. 后续完整实现push流程还需补充以下关键接口:
    • PUT /v2/:name/manifests/:reference:处理镜像manifest的上传与存储
    • PUT /v2/:name/blobs/uploads/:uuid?digest=xxx:完成blob上传并校验摘要

验证修复

添加HEAD路由后重新启动服务,执行docker push时,连接拒绝的错误会消失,服务会进入后续的blob上传流程(未实现的接口会返回404,但已解决当前核心问题)。

内容的提问来源于stack exchange,提问作者SoonGuy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 17:20:53