Setting up gRPC in existing ASP.NET Core project using Docker and Nginx
I have had dotnet API running on a Digital Ocean server for quite some time. The API is running in a Docker container along with a bunch of other containers. Nginx is used as a reverse proxy to route requests to the various containers.
Recently, I had an idea for a project where I wanted to use gRPC. The plan was to add gRPC endpoints to the existing API project without having to change too much of the setup.
It sounded like an easy task, but took more time than expected. This is how I did it.
Step 1 - Add gPRC support to API
My first task was to add gRPC endpoints to the existing api and get it working locally.
I added the gRPC for .NET NuGet packages from the gRPC authors.
Next I configured gRPC in the ConfigureServices
method (Startup.cs) by calling AddGrpc
:
1 | public void ConfigureServices(IServiceCollection services) |
I set EnableDetailedErrors
since I expected to be doing a bunch of debugging later on. For an application running in production, consider keeping this disabled.
1.1 Add a gRPC service to the API
I started out with a default “Greeter” service. I wanted to make sure everything was working before implementing the actual API I wanted.
I added the greet.proto
specification file to the API
1 | syntax = "proto3"; |
Since I want to use the streaming functionality of gRPC I added an endpoint that would stream responses to the client when invoked as well.
The file has to be referenced from the .csproj
file. I added a reference to it right above my NuGet references:
1 | <ItemGroup> |
After building the project a few times to trigger the automatic generation of Greeter.GreeterBase
, I added the actual service logic to the project. The API logic is in the two methods. SayHello
that just returns a single HelloReply
and SayHellos
which continuously sends out messages with a 1 second delay. The code should be farily self-explanatory.
1 | public class GreeterService : Greeter.GreeterBase |
When all this is done, we need to register the endpoint. Back in the Startup.cs
we call MapGrpcService
in the Configure
method to register the endpoints:
1 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
1.2 Create a gRPC client
At the same time I created a seperate project acting as a client for the API so I could test it. Nothing fancy, just a simple Console appliation using the same .proto
specifications, with some code to call the endpoints:
1 | var httpClientHandler = new HttpClientHandler(); |
1.3 Try it out
At this point I tried running the API and the client.. Aaaaaand.. It didn’t work. I was still able to call all my REST endpoints, but got errors when calling the gRPC endpoints. I did manage to call the gPRC successfully.. if I removed the REST part.
Eventually, I found a StackOverflow post mentioning splitting the ports and setting one to HTTP1 and the other to HTTP2 (in Program.cs
).
1 | webBuilder.ConfigureKestrel(options => |
With this I was able to call the REST endpoints using port 5000 and gRPC using port 5001. Not entirely sure what logic makes this work though..
Step 2 - Add Docker Support
With Visual Studio we can autogenerate a docker file to the project. I slightly modified the autogenerated one to expose the right ports and include my DTOs project. This will first build the project and then create a Docker image with a release build of the API.
1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base |
2.1 - Run Image on Server
You can deploy a Docker image in many ways. Personally, I’m using Gitlab CI to deploy my project. I won’t go into details about that here.
After deployment I started looking at the Nginx setup. It took me a while to figure it out. At one point I was doubting that the docker container was actually working as I expected. To test it, I used grpc_cli.
> ./grpc_cli call 172.17.0.2:5001 greet.Greeter/SayHellos "name: 'world'" --protofiles=greet.proto
> connecting to 172.17.0.2:5001
> Received initial metadata from server:
> date : Mon, 10 Aug 2020 00:04:05 GMT
> server : Kestrel
> message: "How are you world? 1"
> message: "How are you world? 2"
> message: "How are you world? 3"
...
It was working! :)
Step 3 - Nginx
Nginx has support for gRPC using the grpc_pass
directive. It is relatively simple to use. The tricky part was figuring out what exactly the location
should be in the Nginx config. In the end it turns out it should be set to the name of the service like this:
1 | upstream restapi { |
Unfortunately, this means I have to add every new service to the Nginx configuration before it works. :( I don’t think there is much I can do about that though.
All that is needed from here is to update the url in the client and I am able to call both the REST and gRPC endpoints from anywhere.
1 | var channel = GrpcChannel.ForAddress("https://mydomain.com", new GrpcChannelOptions |
1 | > Greeting: Hello GreeterClient |
It works! Now I just need to implement the API I want.