前言

開發環境裡,我們常用 Docker Compose 來啟動資料庫。像是 PostgreSQL 這種服務,我們通常會這樣做:

1ports:
2  - "5432:5432"

這樣就能用 localhost:5432 直接連資料庫,簡單又方便。

最近在思考,如果我們不想把資料庫連接埠對外暴露,想完全在 Docker network 裡面連線。
但容器的 IP 每次重啟都不一樣,我們該怎麼辦?

答案是:別手動 docker inspect,直接在 Go 裡用 Docker API 查 IP 就好。
查完 IP,再用它去連 PostgreSQL,完全自動化,安全又乾淨。

Docker Compose 範例

最小化的 docker-compose.yaml 長這樣:

 1services:
 2  db:
 3    image: postgres:16
 4    restart: always
 5    environment:
 6      POSTGRES_USER: user
 7      POSTGRES_PASSWORD: password
 8      POSTGRES_DB: demo
 9    volumes:
10      - db-data:/var/lib/postgresql/data
11
12volumes:
13  db-data:

注意:這裡沒有 ports:,資料庫只在 Docker network 裡對外提供服務。

Go 程式實作

我們會用三個套件:

  • github.com/docker/docker/client → 查容器資訊
  • github.com/jackc/pgx/v5 → 連 PostgreSQL
  • github.com/compose-spec/compose-go/v2/cli → 解析 docker-compose.yml

程式流程:

  1. 用 compose-go 解析 docker-compose.yml,自動獲取專案名稱
  2. 根據專案名稱和服務名稱,組合出容器名稱
  3. 用 Docker API ContainerInspect 拿容器資訊
  4. NetworkSettings.Networks 讀 IP
  5. 拼成連線字串,丟給 pgx 連資料庫

程式碼如下:

  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"time"
  8
  9	"github.com/compose-spec/compose-go/v2/cli"
 10	"github.com/docker/docker/client"
 11	"github.com/jackc/pgx/v5"
 12)
 13
 14// 從 docker-compose.yml 獲取專案資訊
 15func getProjectInfo(composePath string) (projectName string, serviceName string, err error) {
 16	// 使用 compose-go 載入專案設定
 17	options, err := cli.NewProjectOptions(
 18		[]string{composePath},
 19		cli.WithOsEnv,
 20		cli.WithDotEnv,
 21	)
 22	if err != nil {
 23		return "", "", fmt.Errorf("無法建立專案選項: %w", err)
 24	}
 25
 26	project, err := options.LoadProject(context.Background())
 27	if err != nil {
 28		return "", "", fmt.Errorf("無法載入專案: %w", err)
 29	}
 30
 31	// 取得專案名稱
 32	projectName = project.Name
 33	
 34	// 尋找資料庫服務(這裡假設名稱是 "db")
 35	for name := range project.Services {
 36		if name == "db" {
 37			serviceName = name
 38			break
 39		}
 40	}
 41
 42	if serviceName == "" {
 43		return "", "", fmt.Errorf("找不到名為 'db' 的服務")
 44	}
 45
 46	return projectName, serviceName, nil
 47}
 48
 49// 拿容器 IP
 50func getContainerIP(ctx context.Context, containerName string) (string, error) {
 51	cli, err := client.NewClientWithOpts(client.FromEnv)
 52	if err != nil {
 53		return "", fmt.Errorf("無法建立 Docker client: %w", err)
 54	}
 55	defer cli.Close()
 56
 57	containerJSON, err := cli.ContainerInspect(ctx, containerName)
 58	if err != nil {
 59		return "", fmt.Errorf("無法 inspect 容器: %w", err)
 60	}
 61
 62	for _, network := range containerJSON.NetworkSettings.Networks {
 63		return network.IPAddress, nil
 64	}
 65
 66	return "", fmt.Errorf("找不到容器的 IP")
 67}
 68
 69func main() {
 70	ctx := context.Background()
 71
 72	// 自動從 compose.yml 獲取專案資訊
 73	composePath := "compose.yml"
 74	projectName, serviceName, err := getProjectInfo(composePath)
 75	if err != nil {
 76		log.Fatalf("無法獲取專案資訊: %v", err)
 77	}
 78
 79	// Docker Compose 的容器命名規則: projectName-serviceName-1
 80	containerName := fmt.Sprintf("%s-%s-1", projectName, serviceName)
 81	fmt.Printf("專案名稱: %s, 服務名稱: %s, 容器名稱: %s\n", 
 82		projectName, serviceName, containerName)
 83
 84	ip, err := getContainerIP(ctx, containerName)
 85	if err != nil {
 86		log.Fatalf("查 IP 失敗: %v", err)
 87	}
 88
 89	fmt.Println("找到容器 IP:", ip)
 90
 91	connStr := fmt.Sprintf("postgres://user:password@%s:5432/demo", ip)
 92
 93	conn, err := pgx.Connect(ctx, connStr)
 94	if err != nil {
 95		log.Fatalf("無法連線資料庫: %v", err)
 96	}
 97	defer conn.Close(ctx)
 98
 99	var now time.Time
100	err = conn.QueryRow(ctx, "SELECT now()").Scan(&now)
101	if err != nil {
102		log.Fatalf("查詢失敗: %v", err)
103	}
104
105	fmt.Println("成功連上資料庫,現在時間:", now)
106}
⚠️ 常見問題與解決方案

如果你遇到類似以下的錯誤:

1github.com/docker/docker/client: github.com/docker/docker/client@v0.1.0-alpha.0: parsing go.mod:
2module declares its path as: github.com/moby/moby/client
3        but was required as: github.com/docker/docker/client
  1. 初始化 go.mod(如果還沒有的話):
1go mod init your-project-name
  1. go.mod 修改依賴:
1replace github.com/docker/docker => github.com/moby/moby latest
  1. 下載依賴套件:
1go mod tidy

執行測試

  1. 先啟動 PostgreSQL 容器:
1docker compose up -d db
  1. 執行 Go 程式:
1go run main.go
  1. 會看到類似輸出:
1專案名稱: blog, 服務名稱: db, 容器名稱: blog-db-1
2找到容器 IP: 172.19.0.2
3成功連上資料庫,現在時間: 2025-08-31 23:05:00.389548 +0800 CST

說明:

  • blog 是自動從目錄名稱推導出的專案名稱
  • blog-db-1 是 Docker Compose 的標準容器命名格式
  • 程式會自動解析 docker-compose.yml 來取得這些資訊

調整使用的 Docker Socket 路徑

環境變數

 1import "os"
 2
 3func main() {
 4    // 設定自定義 Docker socket 路徑
 5    os.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock")
 6    
 7    // 其他程式碼保持不變
 8    cli, err := client.NewClientWithOpts(client.FromEnv)
 9    // ...
10}

在程式中指定

1cli, err := client.NewClientWithOpts(
2    client.WithHost("unix:///tmp/docker.sock"),
3)

遠端 Docker Daemon

 1// TCP 連接
 2cli, err := client.NewClientWithOpts(
 3    client.WithHost("tcp://remote-docker-host:2376"),
 4    client.WithTLSClientConfig("/path/to/ca.pem", "/path/to/cert.pem", "/path/to/key.pem"),
 5)
 6
 7// 或使用環境變數
 8os.Setenv("DOCKER_HOST", "tcp://remote-docker-host:2376")
 9os.Setenv("DOCKER_TLS_VERIFY", "1")
10os.Setenv("DOCKER_CERT_PATH", "/path/to/certs")

安全考量

 1# 只給 read-only 權限(如果支援)
 2volumes:
 3  - /var/run/docker.sock:/var/run/docker.sock:ro
 4  
 5# 或使用 Docker-in-Docker (DinD)
 6services:
 7  app:
 8    image: your-app
 9    depends_on:
10      - docker
11  docker:
12    image: docker:dind
13    privileged: true
14    environment:
15      DOCKER_TLS_CERTDIR: ""
  • 專案名稱推導規則

    • 如果沒有在 docker-compose.yml 中指定 name:,compose-spec/compose-go 會使用目錄名稱當專案名稱
    • 也可以在 compose 檔案中明確指定:
    1name: my-custom-project-name
    2services:
    3  db:
    4    # ...
    
  • 容器命名格式:Docker Compose 使用 {projectName}-{serviceName}-{instance} 格式

    • 舊版本使用底線:{projectName}_{serviceName}_{instance}
    • 新版本使用破折號:{projectName}-{serviceName}-{instance}
  • 指定不同的 compose 檔案

    1composePath := "path/to/your/docker-compose.yml"
    2projectName, serviceName, err := getProjectInfo(composePath)
    
  • 多個 network 時,程式會拿第一個 network 的 IP。實務上可以指定 network 名稱。

  • 容器 IP 只在 Docker network 裡有效,外部機器無法直接用。

小結

透過結合 compose-spec/compose-go 套件,我們實現了完全自動化的容器 IP 查詢:

  1. 自動解析 docker-compose.yml 檔案
  2. 智能推導 專案名稱和容器名稱
  3. 動態查詢 容器 IP 地址
  4. 無需 hardcode 任何容器相關資訊

這樣做比直接開 ports 麻煩一點,但更安全更靈活。 對內網環境或安全要求高的專案,這種方式非常適合。當你的專案有多個環境或經常變更容器配置時,這種自動化方法能大幅減少維護成本。