Cloud Spanner

gRPC · port 9010 · orchestrated
Orchestrated service. Spanner runs as a Docker container managed by localgcp. Requires Docker (Docker Desktop, OrbStack, or Colima). The container starts lazily on first connection, not at startup.

Quick start

$ localgcp up --services=spanner

The first time your code connects to port 9010, localgcp pulls the official Google Spanner emulator image and starts it. Subsequent startups reuse the cached image (~3s).

Go SDK example

Connect to the emulator, create an instance, create a database, and run a query:

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    spanner "cloud.google.com/go/spanner"
    database "cloud.google.com/go/spanner/admin/database/apiv1"
    databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
    instance "cloud.google.com/go/spanner/admin/instance/apiv1"
    instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
    "google.golang.org/api/iterator"
)

func main() {
    // Point the SDK at the localgcp emulator
    os.Setenv("SPANNER_EMULATOR_HOST", "localhost:9010")

    ctx := context.Background()
    project := "my-project"

    // Create an instance
    instanceAdmin, err := instance.NewInstanceAdminClient(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer instanceAdmin.Close()

    op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{
        Parent:     fmt.Sprintf("projects/%s", project),
        InstanceId: "my-instance",
        Instance: &instancepb.Instance{
            DisplayName: "My Instance",
            Config:      fmt.Sprintf("projects/%s/instanceConfigs/emulator-config", project),
            NodeCount:   1,
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    if _, err = op.Wait(ctx); err != nil {
        log.Fatal(err)
    }

    // Create a database with a table
    databaseAdmin, err := database.NewDatabaseAdminClient(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer databaseAdmin.Close()

    dbOp, err := databaseAdmin.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
        Parent:          fmt.Sprintf("projects/%s/instances/my-instance", project),
        CreateStatement: "CREATE DATABASE `my-db`",
        ExtraStatements: []string{
            `CREATE TABLE Users (
    UserId   INT64 NOT NULL,
    Name     STRING(1024),
    Email    STRING(1024),
) PRIMARY KEY (UserId)`,
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    if _, err = dbOp.Wait(ctx); err != nil {
        log.Fatal(err)
    }

    // Insert a row and query it
    db := fmt.Sprintf("projects/%s/instances/my-instance/databases/my-db", project)
    client, err := spanner.NewClient(ctx, db)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    _, err = client.Apply(ctx, []*spanner.Mutation{
        spanner.InsertOrUpdate("Users",
            []string{"UserId", "Name", "Email"},
            []interface{}{1, "Alice", "[email protected]"},
        ),
    })
    if err != nil {
        log.Fatal(err)
    }

    iter := client.Single().Query(ctx, spanner.Statement{
        SQL: "SELECT UserId, Name, Email FROM Users",
    })
    defer iter.Stop()

    for {
        row, err := iter.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        var userId int64
        var name, email string
        if err := row.Columns(&userId, &name, &email); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("User: %d %s %s\n", userId, name, email)
    }
}

Environment variable

The Spanner client libraries check SPANNER_EMULATOR_HOST automatically. Set it before running your code:

$ export SPANNER_EMULATOR_HOST=localhost:9010

Or let localgcp set all environment variables at once:

$ eval $(localgcp env)

How it works

localgcp uses a lazy TCP proxy to manage the Spanner emulator container:

  1. Port binds instantly -- localgcp listens on port 9010 as soon as it starts, so your code never gets "connection refused."
  2. Container starts on first request -- the first TCP connection triggers docker run with the official Spanner emulator image. localgcp waits for the container to become healthy before forwarding.
  3. io.Copy proxies all traffic -- once the container is up, localgcp splices the connection through to the emulator with zero overhead for subsequent requests.
  4. Ctrl+C stops the container -- when localgcp shuts down, it stops and removes the Docker container automatically.

Pre-fetching images

The first cold start pulls the emulator image, which can take 30-60s on a slow connection. To avoid that delay, pre-fetch the image:

# Coming soon
$ localgcp pull

Or pull manually:

$ docker pull gcr.io/cloud-spanner-emulator/emulator:1.5.23

Not yet supported