DEV Community

Md Imran
Md Imran

Posted on

Mastering Go gRPC Services with Docker: A One-Stop Guide

Hello, fellow developers! Ready to jump into the exciting world of Go, gRPC, and Docker? Whether you're new to backend development or a seasoned pro with containers, this guide will help you easily navigate the realm of microservices. Your friendly Docker Captain is here to help you master this tech - let's dive in together!

Introduction

In the era of microservices, building scalable and efficient APIs is crucial. gRPC, with its high performance and efficient communication, has become a popular choice for inter-service communication. Go (Golang), known for its simplicity and performance, pairs beautifully with gRPC. And when it comes to deploying these services reliably and consistently, Docker is the go-to solution.

This comprehensive guide will walk you through building a gRPC API using Go, exposing it via gRPC-Gateway for RESTful access, and containerizing the application using Docker. We'll cover everything from setting up your development environment to running your services in Docker containers. By the end of this journey, you'll have a fully functional, containerized Go gRPC service ready for deployment.

Table of Contents

  1. Prerequisites
  2. Project Setup
  3. Defining the Protobuf Service
  4. Generating Go Code from Protobuf
  5. Implementing the gRPC Server
  6. Setting Up gRPC-Gateway
  7. Implementing the REST Gateway
  8. Dockerizing the Application
  9. Running the Services
  10. Testing the API
  11. Conclusion

Prerequisites

Before we set sail, make sure you have the following installed:

  • Go (version 1.16 or higher)
  • Docker (latest version)
  • Docker Compose (latest version)
  • Protobuf Compiler (protoc)
  • Git (optional but recommended)

Project Setup

Create a new directory for your project and navigate into it:

mkdir go-grpc-docker
cd go-grpc-docker
Enter fullscreen mode Exit fullscreen mode

Initialize a new Go module:

go mod init github.com/yourusername/go-grpc-docker
Enter fullscreen mode Exit fullscreen mode

Defining the Protobuf Service

Create a directory for your protocol buffer definitions:

mkdir proto
Enter fullscreen mode Exit fullscreen mode

Inside the proto directory, create a file named service.proto:

syntax = "proto3";

package pb;

option go_package = "github.com/yourusername/go-grpc-docker/proto";

import "google/api/annotations.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {
    option (google.api.http) = {
      get: "/v1/hello/{name}"
    };
  }
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}
Enter fullscreen mode Exit fullscreen mode

This protobuf file defines a Greeter service with a SayHello method and includes HTTP annotations for gRPC-Gateway.

Generating Go Code from Protobuf

First, install the necessary plugins:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
Enter fullscreen mode Exit fullscreen mode

Ensure the binaries are in your PATH. Then, generate the Go code:

protoc -I proto \
  -I third_party/googleapis \
  --go_out proto --go_opt paths=source_relative \
  --go-grpc_out proto --go-grpc_opt paths=source_relative \
  --grpc-gateway_out proto --grpc-gateway_opt paths=source_relative \
  proto/service.proto
Enter fullscreen mode Exit fullscreen mode

Note: You'll need to clone the googleapis repository to have access to annotations.proto:

git clone https://github.com/googleapis/googleapis.git third_party/googleapis
Enter fullscreen mode Exit fullscreen mode

Implementing the gRPC Server

Create a directory for your server code:

mkdir server
Enter fullscreen mode Exit fullscreen mode

Inside server, create main.go:

package main

import (
    "context"
    "log"
    "net"

    pb "github.com/yourusername/go-grpc-docker/proto"
    "google.golang.org/grpc"
)

type greeterServer struct {
    pb.UnimplementedGreeterServer
}

func (s *greeterServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    log.Printf("Received request for name: %s", req.Name)
    return &pb.HelloResponse{Message: "Hello, " + req.Name + "!"}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Failed to listen on port 50051: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &greeterServer{})

    log.Println("gRPC server listening on port 50051...")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("Failed to serve gRPC server: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

This code sets up a simple gRPC server that implements the SayHello method.

Setting Up gRPC-Gateway

Create a directory for your gateway code:

mkdir gateway
Enter fullscreen mode Exit fullscreen mode

Inside gateway, create main.go:

package main

import (
    "context"
    "log"
    "net/http"

    pb "github.com/yourusername/go-grpc-docker/proto"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    err := pb.RegisterGreeterHandlerServer(ctx, mux, &greeterServer{})
    if err != nil {
        log.Fatalf("Failed to register handler server: %v", err)
    }

    log.Println("HTTP server listening on port 8080...")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatalf("Failed to serve HTTP server: %v", err)
    }
}

type greeterServer struct {
    pb.UnimplementedGreeterServer
}

func (s *greeterServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{Message: "Hello, " + req.Name + "!"}, nil
}
Enter fullscreen mode Exit fullscreen mode

This code sets up a RESTful HTTP server using gRPC-Gateway.

Implementing the REST Gateway

In the gateway/main.go file, we directly implemented the greeterServer to handle the requests. However, in a real-world scenario, the gateway would forward requests to the gRPC server.

Update gateway/main.go to forward requests to the gRPC server:

package main

import (
    "context"
    "flag"
    "log"
    "net/http"

    pb "github.com/yourusername/go-grpc-docker/proto"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
)

var (
    grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50051", "gRPC server endpoint")
)

func main() {
    flag.Parse()

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Register gRPC-Gateway
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}

    err := pb.RegisterGreeterHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
    if err != nil {
        log.Fatalf("Failed to register gRPC-Gateway: %v", err)
    }

    log.Println("HTTP server listening on port 8080...")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatalf("Failed to serve HTTP server: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, the gateway forwards requests to the gRPC server running on localhost:50051.

Dockerizing the Application

Writing the Dockerfile

Create a Dockerfile in the root of your project:

# Build Stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go.mod and go.sum files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy the source code
COPY . .

# Build the gRPC server
RUN go build -o bin/server ./server/main.go

# Build the gRPC-Gateway
RUN go build -o bin/gateway ./gateway/main.go

# Run Stage for gRPC Server
FROM alpine:latest AS server

WORKDIR /app

COPY --from=builder /app/bin/server .

EXPOSE 50051

ENTRYPOINT ["./server"]

# Run Stage for gRPC-Gateway
FROM alpine:latest AS gateway

WORKDIR /app

COPY --from=builder /app/bin/gateway .

EXPOSE 8080

ENTRYPOINT ["./gateway"]
Enter fullscreen mode Exit fullscreen mode

This multi-stage Dockerfile builds both the gRPC server and the gRPC-Gateway, and creates separate images for each.

Using Docker Compose

Create a docker-compose.yml file to orchestrate the services:

version: '3.8'

services:
  server:
    build:
      context: .
      target: server
    image: go-grpc-server
    ports:
      - "50051:50051"

  gateway:
    build:
      context: .
      target: gateway
    image: go-grpc-gateway
    ports:
      - "8080:8080"
    depends_on:
      - server
    environment:
      - GRPC_SERVER_ENDPOINT=server:50051
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • server: Builds the gRPC server image and exposes port 50051.
  • gateway: Builds the gRPC-Gateway image, depends on the server, and exposes port 8080. It uses an environment variable to specify the gRPC server endpoint.

Running the Services

Use Docker Compose to build and run the services:

docker-compose up --build
Enter fullscreen mode Exit fullscreen mode

This command builds the images and starts both services. You should see logs indicating that both the gRPC server and the HTTP server are running.

Testing the API

Testing the gRPC Server

Install grpcurl if you haven't already:

go install github.com/fullstorydev/grpcurl@latest
Enter fullscreen mode Exit fullscreen mode

Test the gRPC server:

grpcurl -plaintext localhost:50051 pb.Greeter/SayHello -d '{"name": "Docker"}'
Enter fullscreen mode Exit fullscreen mode

You should receive a response like:

{
  "message": "Hello, Docker!"
}
Enter fullscreen mode Exit fullscreen mode

Testing the REST Gateway

Test the RESTful API:

curl http://localhost:8080/v1/hello/Docker
Enter fullscreen mode Exit fullscreen mode

You should see:

{"message":"Hello, Docker!"}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Congratulations! You've successfully:

  • Defined a gRPC service using Protocol Buffers.
  • Implemented a gRPC server in Go.
  • Set up gRPC-Gateway to expose your gRPC service via REST.
  • Dockerized both the gRPC server and the REST gateway.
  • Used Docker Compose to run both services together.

By containerizing your services, you've made them portable and easy to deploy across different environments. Whether you're deploying to a cloud provider, a data center, or just your local machine, Docker ensures consistency and reliability.

Now, go forth and build amazing microservices with Go, gRPC, and Docker! And remember, the real treasure is the friends we made along the way (and the services we containerized).


Happy Coding!

Top comments (0)