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,因此推送正常。
修复步骤
- 在Echo路由中添加
HEAD /v2的处理,直接复用现有的Root函数即可:e.HEAD("/v2", Root) - 后续完整实现push流程还需补充以下关键接口:
PUT /v2/:name/manifests/:reference:处理镜像manifest的上传与存储PUT /v2/:name/blobs/uploads/:uuid?digest=xxx:完成blob上传并校验摘要
验证修复
添加HEAD路由后重新启动服务,执行docker push时,连接拒绝的错误会消失,服务会进入后续的blob上传流程(未实现的接口会返回404,但已解决当前核心问题)。
内容的提问来源于stack exchange,提问作者SoonGuy
相关产品推荐
相关产品推荐

