Search code examples
godocker-registry

Docker Push to Private Repo instantly returns "Connection Refused"


Using the docker API spec V2 I am trying to implement a simple Docker Registry over HTTP. Whenever I run docker push 127.0.0.1:5000/debian I instantly receive the following error.

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

The odd thing is if I make a curl request to the URL it works fine (ditto for web browser)

Also I tried running a local registry via Docker image

docker run -d -p 5000:5000 --name registry registry:2.7

Doing it this way the push is successful so there doesn't appear to be an underlying issue with Docker pushing to an insecure registry.

Here is the code I am using to run a simple registry over HTTP.

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))
}


Solution

  • when running docker push any loopback address (localhost, 127.0.01, etc.) refers to the guest OS (in this case Lima VM because I am running Rancher Desktop). Per Lima docs the Host IP is always 192.168.5.2... and sure enough using that IP resulted in a successful image push.