Running gRPC services inside Docker containers is a standard practice for deploying distributed systems, but it involves a few more moving parts than your typical HTTP service.

Let’s see it in action.

Imagine a simple gRPC "Greeter" service. Here’s the protobuf definition:

syntax = "proto3";

package greeter;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Now, let’s build a Python gRPC server that implements this.

# server.py
import grpc
from concurrent import futures
import time

import greeter_pb2
import greeter_pb2_grpc

class GreeterServicer(greeter_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return greeter_pb2.HelloReply(message=f"Hello, {request.name}!")

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    greeter_pb2_grpc.add_GreeterServicer_to_server(GreeterServicer(), server)
    server.add_insecure_port('[::]:50051') # Listen on all interfaces, port 50051
    server.start()
    print("Server started on port 50051...")
    try:
        while True:
            time.sleep(86400) # Keep the server running
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

And a Python gRPC client to test it:

# client.py
import grpc
import greeter_pb2
import greeter_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel: # Connect to localhost on 50051
        stub = greeter_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(greeter_pb2.HelloRequest(name='World'))
        print(f"Greeter client received: {response.message}")

if __name__ == '__main__':
    run()

To run this in Docker, we need a Dockerfile for the server.

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "server.py"]

And a requirements.txt:

grpcio
grpcio-tools

To build the protobufs and install dependencies:

  1. Generate Python code from .proto:
    python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. greeter.proto
    
  2. Create requirements.txt with grpcio and grpcio-tools.
  3. Build the Docker image:
    docker build -t my-grpc-greeter .
    

Now, to run the server in a container:

docker run -d --name grpc-server -p 50051:50051 my-grpc-greeter

The -p 50051:50051 maps the container’s port 50051 to the host’s port 50051. The client, if run on the host, can then connect to localhost:50051 as shown in client.py.

If you wanted to run the client also in a container, you’d need to adjust its configuration. A common pattern is to use Docker networking.

Create a Docker network:

docker network create grpc-net

Run the server container on this network:

docker run -d --name grpc-server --network grpc-net my-grpc-greeter

And run the client container, also on the same network:

docker run --rm --network grpc-net my-grpc-greeter python client.py

In this client container, localhost:50051 won’t work because localhost refers to the client’s own loopback interface. Instead, the client needs to connect to the service name of the gRPC server within the Docker network. By default, Docker uses the container name as the hostname. So, the client would need to be modified to connect to grpc-server:50051.

# client_docker.py
import grpc
import greeter_pb2
import greeter_pb2_grpc

def run():
    # Connect to the service name 'grpc-server' on port 50051 within the Docker network
    with grpc.insecure_channel('grpc-server:50051') as channel:
        stub = greeter_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(greeter_pb2.HelloRequest(name='Docker'))
        print(f"Greeter client received: {response.message}")

if __name__ == '__main__':
    run()

The most surprising true thing about running gRPC services in Docker is that the standard localhost resolution within a container doesn’t magically translate to other containers on the same host; you must rely on Docker’s internal DNS resolution, typically using the container name as the hostname.

The key to this cross-container communication is Docker’s user-defined networking. When containers are attached to the same user-defined network (like grpc-net), Docker provides a DNS server that resolves container names to their IP addresses on that network. This allows services to discover and communicate with each other by their service names, abstracting away the underlying IP addresses, which can change dynamically. Without this, you’d be left with complex IP address management or relying on the host’s IP and port mapping, which breaks the self-contained nature of containerized applications.

The next challenge is managing gRPC service discovery and load balancing when you have multiple instances of your gRPC server running.

Want structured learning?

Take the full Grpc course →