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
- Prerequisites
- Project Setup
- Defining the Protobuf Service
- Generating Go Code from Protobuf
- Implementing the gRPC Server
- Setting Up gRPC-Gateway
- Implementing the REST Gateway
- Dockerizing the Application
- Running the Services
- Testing the API
- 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
Initialize a new Go module:
go mod init github.com/yourusername/go-grpc-docker
Defining the Protobuf Service
Create a directory for your protocol buffer definitions:
mkdir proto
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;
}
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
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
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
Implementing the gRPC Server
Create a directory for your server code:
mkdir server
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)
}
}
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
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
}
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)
}
}
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"]
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
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
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
Test the gRPC server:
grpcurl -plaintext localhost:50051 pb.Greeter/SayHello -d '{"name": "Docker"}'
You should receive a response like:
{
"message": "Hello, Docker!"
}
Testing the REST Gateway
Test the RESTful API:
curl http://localhost:8080/v1/hello/Docker
You should see:
{"message":"Hello, Docker!"}
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)