💡

Go言語のDockerチュートリアルでcurl: (52) Empty reply fromエラーが出た原因と対処法

2025/01/03に公開

はじめに

Go言語の勉強をしています。

Docker環境を構築したいと思い、Go language-specific guideのチュートリアルを進めていました。

https://docs.docker.com/guides/golang/

進めている最中にcurl: (52) Empty reply fromというエラーが出てしまったため、原因と対処法をまとめます。

問題

Use containers for Go developmentの章で問題が起きました。

この章のチュートリアルを進めると、curl localhostの実行結果としてHello, Docker! (0)が出力される想定となっています。

$ curl localhost
Hello, Docker! (0)

ところが、自分の手元で実行すると次のエラーが出てしまいます。

$ curl localhost
curl: (52) Empty reply from server

ここに至るまでの自分の書いたコードに誤りがあるのかと思い、公式ドキュメントのサンプルコードを全部コピペして動かしてみましたが、結果は変わりませんでした。

さらに、docker logsでログを確認しても何も表示されず、コンテナ自体は起動しているのにサーバーが応答しない状態に陥っていました。

このエラー発生時点でのmain.goファイルは次のようになっています(チュートリアルのサンプルコード)。

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/cenkalti/backoff/v4"
	"github.com/cockroachdb/cockroach-go/v2/crdb"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	db, err := initStore()
	if err != nil {
		log.Fatalf("failed to initialize the store: %s", err)
	}
	defer db.Close()

	e.GET("/", func(c echo.Context) error {
		return rootHandler(db, c)
	})

	e.GET("/ping", func(c echo.Context) error {
		return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
	})

	e.POST("/send", func(c echo.Context) error {
		return sendHandler(db, c)
	})

	httpPort := os.Getenv("HTTP_PORT")
	if httpPort == "" {
		httpPort = "8080"
	}

	e.Logger.Fatal(e.Start(":" + httpPort))
}

type Message struct {
	Value string `json:"value"`
}

func initStore() (*sql.DB, error) {

	pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
		os.Getenv("PGHOST"),
		os.Getenv("PGPORT"),
		os.Getenv("PGDATABASE"),
		os.Getenv("PGUSER"),
		os.Getenv("PGPASSWORD"),
	)

	var (
		db  *sql.DB
		err error
	)
	openDB := func() error {
		db, err = sql.Open("postgres", pgConnString)
		return err
	}

	err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
	if err != nil {
		return nil, err
	}

	if _, err := db.Exec(
		"CREATE TABLE IF NOT EXISTS message (value TEXT PRIMARY KEY)"); err != nil {
		return nil, err
	}

	return db, nil
}

func rootHandler(db *sql.DB, c echo.Context) error {
	r, err := countRecords(db)
	if err != nil {
		return c.HTML(http.StatusInternalServerError, err.Error())
	}
	return c.HTML(http.StatusOK, fmt.Sprintf("Hello, Docker! (%d)\n", r))
}

func sendHandler(db *sql.DB, c echo.Context) error {

	m := &Message{}

	if err := c.Bind(m); err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	err := crdb.ExecuteTx(context.Background(), db, nil,
		func(tx *sql.Tx) error {
			_, err := tx.Exec(
				"INSERT INTO message (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = excluded.value",
				m.Value,
			)
			if err != nil {
				return c.JSON(http.StatusInternalServerError, err)
			}
			return nil
		})

	if err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	return c.JSON(http.StatusOK, m)
}

func countRecords(db *sql.DB) (int, error) {

	rows, err := db.Query("SELECT COUNT(*) FROM message")
	if err != nil {
		return 0, err
	}
	defer rows.Close()

	count := 0
	for rows.Next() {
		if err := rows.Scan(&count); err != nil {
			return 0, err
		}
		rows.Close()
	}

	return count, nil
}

解決策

PostgreSQL用のドライバがインポートされていないため、DB接続に失敗していることがエラーの原因でした。

Goの標準パッケージだけではdb, err = sql.Open("postgres", pgConnString)を使ってPostgreSQLに接続することはできません。

  1. go get github.com/lib/pqを実行
  2. importに以下の1行を追加
import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/cenkalti/backoff/v4"
	"github.com/cockroachdb/cockroach-go/v2/crdb"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
        // インポートを追加
        _ "github.com/lib/pq"
)

github.com/lib/pqがエクスポートする関数を直接呼び出すわけではなく、ドライバ自体の有効化だけを目的に読み込むため、_を付与しブランクインポートとしています。

これでチュートリアルに記載の以下のコマンドを再度実行します。

$ docker build --tag docker-gs-ping-roach .
$ docker run -it --rm -d \
  --network mynet \
  --name rest-server \
  -p 80:8080 \
  -e PGUSER=totoro \
  -e PGPASSWORD=myfriend \
  -e PGHOST=db \
  -e PGPORT=26257 \
  -e PGDATABASE=mydb \
  docker-gs-ping-roach

するとチュートリアルどおりの出力が得られました!

$ curl localhost
Hello, Docker! (0)

ログに何も表示されなかった理由

今回、コンテナ自体はdocker psで見るとUp状態にもかかわらず、docker logsで見ても何も出力がないため原因がわかりませんでした。

実は、本来エラーが起きている箇所で log.Printlog.Fatalf を出さないまま、backoff.Retryで延々と接続を試み続けていたのです。

そのためcurl localhostEmpty reply from serverとなり、ログに何も出力されないままコンテナが待機してしまったのでした。

以下のようにログ出力の処理を追加してデバッグを行いました。

openDB := func() error {
    db, err = sql.Open("postgres", pgConnString)
    if err != nil {
        // 失敗時に必ずログを出力
        log.Printf("DB connection error: %v", err)
    }
    return err
}

おわりに

このチュートリアルではdistrolessイメージを使用しています。

distrolessは余計なパッケージを一切含まないためシェルが無くコンテナ内でのデバッグが困難です。

ログ出力用のコードを追加するか、いったんAlpineイメージを使って検証するなどの工夫が求められます。

このあたりのGo言語の作法にも慣れていきたいですね。

参考資料

https://pkg.go.dev/database/sql

Discussion