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