This episode composes environment-aware gRPC configuration: environment variables for port, TLS, service discovery, and retry policy, then runs the gRPC server inside Docker and docker-compose for a clean local deployment.

A server that only runs on localhost:50051 is fine for learning, but not for real environments. In production, ports change between environments, TLS is enabled, service discovery addresses differ, and the retry policy is tuned. None of that should be hardcoded.
Episode 7 covers environment-aware configuration: reading port, TLS, and policies from environment variables, then wrapping the server in a Docker container and docker-compose. The end result is a server that's identical no matter where it runs — only env differs.
Don't hardcode the port. Read it from an environment variable with a safe default for development:
port := os.Getenv("GRPC_PORT")
if port == "" {
port = "50051"
}
lis, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatal(err)
}
log.Printf("server mendengarkan di :%s", port)The pattern of os.Getenv("GRPC_PORT") then falling back to a default is universal: configuration changes easily, code doesn't. The same applies to TLS, dependency addresses, and other flags.
TLS must be enabled without changing code:
if os.Getenv("GRPC_TLS") == "true" {
creds, err := credentials.NewServerTLSFromFile(
os.Getenv("TLS_CERT"), os.Getenv("TLS_KEY"),
)
s = grpc.NewServer(grpc.Creds(creds))
} else {
s = grpc.NewServer()
}Setting GRPC_TLS=true enables encryption; in development, this variable is left empty so it uses plaintext. This flow is refined in episode 11 with full mTLS.
The client needs to know where to connect, and that address differs per environment:
target := os.Getenv("GRPC_TARGET")
if target == "" {
target = "localhost:50051"
}
conn, _ := grpc.NewClient(target, opts...)In development the target is localhost:50051; in staging it might be grpc.internal.example.com:443; in Kubernetes a service name like catalog-svc:50051 is enough. grpc.NewClient(target, opts...) accepts all of these.
The retry policy is sent as JSON together with the target name:
{
"methodConfig": [
{
"name": [
{ "service": "catalog.v1.CatalogService" }
],
"retryPolicy": {
"maxAttempts": 4,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2.0,
"retryableStatusCodes": [
"UNAVAILABLE"
]
}
}
]
}The retryPolicy configuration above tells the client to retry up to four times when the server returns UNAVAILABLE. This JSON can be read from an environment variable or a file — episode 15 covers it in more depth.
For a small and secure image, use multi-stage builds. The first stage compiles the Go binary, the second stage creates a slim runtime image:
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /server /server
EXPOSE 50051
ENTRYPOINT ["/server"]The Dockerfile above produces an image without a shell or toolchain — just the binary. CGO_ENABLED=0 go build ensures a static binary that can run in a minimal container.
docker build -t belajar-grpc-server .
docker run -p 50051:50051 \
-e GRPC_PORT=50051 \
belajar-grpc-serverThe -p 50051:50051 flag maps the container port to the host, and -e GRPC_PORT=50051 passes the environment variable through. Now a local client can connect to the server inside the container.
When your gRPC server has dependencies like a database, use docker-compose to manage everything at once:
services:
catalog:
build: .
environment:
GRPC_PORT: "50051"
DB_HOST: postgres
ports:
- "50051:50051"
depends_on:
- postgres
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: localdev
ports:
- "5432:5432"This docker-compose.yml defines two services: catalog (the gRPC server) and postgres (the database). Notice DB_HOST: postgres — inside the compose network, services can be reached by their service name.
docker compose up --buildWith one command, docker compose up --build builds the image, creates the network, and starts the database plus the server. For faster development, you can run it in detached mode and watch the logs with docker compose logs -f.
Key takeaways:
os.Getenv with defaults keeps the code the same across all environments.In episode 8 next, we cover state, data management & schema evolution — best practices for evolving protobuf schemas without breaking changes, message and field numbering versioning techniques, service migration, and integrating gRPC with databases or stateful backends. Your freshly deployed service will start storing real data.