Apple Silicon — Docker — dotnet — gRPC — is that compatible?

Daniel Stoyanoff
6 min readApr 23, 2021

Up until a few months, I didn’t want to even hear about Mac. This has changed with the revolution that M1 did. So, I’ve decided to jump ahead and get a MacBook Pro with 16GB RAM. So far I do not regret my choice. It’s super fast and pleasant to work with.

I did not have any issues with running development apps so far (mainly .NET/Node JS/Frontend), until I had to work with a gRPC service inside of docker.. I will try to describe my journey and what worked for me at the end. I hope it will be useful for someone.

Let’s create an example project

Let’s start first with .NET 5. Here is the info about the SDK that I have:

~ dotnet --list-sdks
5.0.202 [/usr/local/share/dotnet/sdk]

Then create a new sln and project:

# create folder and solution
~ mkdir grpc-dotnet-apple-silicon && cd grpc-dotnet-apple-silicon
~ dotnet new sln --name grpc-dotnet
~ mkdir src && cd src
# create GRPC project and add it to the solution~ dotnet new grpc --name grpc-dotnet
~ cd ..
~ dotnet sln add src/grpc-dotnet

After running those commands, we should have a working app with the Greet GRPC service. Let’s try to run it:

~ dotnet run --project src/grpc-dotnet

FAIL!

Building...
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: The specified task executable "/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/tools/linux_x64/protoc" could not be run. System.ComponentModel.Win32Exception (8): Exec format error [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: at System.Diagnostics.Process.ForkAndExecProcess(String filename, String[] argv, String[] envp, String cwd, Boolean redirectStdin, Boolean redirectStdout, Boolean redirectStderr, Boolean setCredentials, UInt32 userId, UInt32 groupId, UInt32[] groups, Int32& stdinFd, Int32& stdoutFd, Int32& stderrFd, Boolean usesTerminal, Boolean throwOnNoExec) [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: at System.Diagnostics.Process.StartCore(ProcessStartInfo startInfo) [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: at System.Diagnostics.Process.Start() [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: at Microsoft.Build.Utilities.ToolTask.ExecuteTool(String pathToTool, String responseFileCommands, String commandLineCommands) [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
/Users/danielstoyanoff/.nuget/packages/grpc.tools/2.34.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6003: at Microsoft.Build.Utilities.ToolTask.Execute() [/Users/danielstoyanoff/dev/tutorials/grpc-dotnet-apple-silicon/src/grpc-dotnet/grpc-dotnet.csproj]
The build failed. Fix the build errors and run again.

This is the output when running from a native terminal. If we now try under Rosetta, we will get further. If you have a fresh dotnet install, you will see an error related to the HTTPS:

Unable to start Kestrel.
System.IO.IOException: Failed to bind to address https://localhost:5001.
---> System.AggregateException: One or more errors occurred. (HTTP/2 over TLS is not supported on macOS due to missing ALPN support.) (HTTP/2 over TLS is not supported on macOS due to missing ALPN support.)

This is a limitation of the Kestrel server on macOS. You can read more about it in this document. We will just disable TLS for now in Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
// Setup a HTTP/2 endpoint without TLS.
options.ListenAnyIp(5000, o => o.Protocols =
HttpProtocols.Http2);
});
webBuilder.UseStartup<Startup>();
});

If we re-run the project now, it should work well.

Let’s create a second project to use as a gRPC client to verify our server works:

~ cd src
~ dotnet new console --name client
~ cd ..
~ dotnet sln add src/client
# copy the Protos folder from the server to the client:
~ cp -r src/grpc-dotnet/Protos src/client/Protos
# install gRPC.Net.Client nuget package
~ dotnet add src/client package Grpc.Net.Client
~ dotnet add src/client package Google.Protobuf
~ dotnet add src/client package Grpc.Tools

Then include the Proto file in client.csproj.

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

Build the client in order to let it generate the gRPC client:

~ dotnet build src/client

Change src/client/Program.cs to the following:

using System;
using System.Threading.Tasks;
using grpc_dotnet;
using Grpc.Net.Client;

namespace client
{
class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("http://localhost:5000");
var client = new Greeter.GreeterClient(channel);

var reply = await client.SayHelloAsync(new HelloRequest
{
Name = "gRPC!"
});

Console.WriteLine(reply.Message);
}
}
}

If you run the client now, you should see it working (don’t forget to start the server if you stopped it):

~ dotnet run --project src/client
Hello gRPC!

Add Database

Let’s spice the things a bit by adding EntityFramework with a Postgres provider:

~ dotnet add src/grpc-dotnet package Microsoft.EntityFrameworkCore
~ dotnet add src/grpc-dotnet package Microsoft.EntityFrameworkCore.Design
~ dotnet add src/grpc-dotnet package Npgsql.EntityFrameworkCore.PostgreSQL

Create a model and Db Context:

# Hello.cs
namespace
grpc_dotnet
{
public class Hello
{
public int Id { get; set; }

public string Name { get; set; }
}
}
# AppDbContext.cs
using Microsoft.EntityFrameworkCore;

namespace grpc_dotnet
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}

public DbSet<Hello> Hellos { get; set; }
}
}

Initialise the database in Startup.cs

services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql("Server=db;Port=5432;Database=hello-db;User Id=user;Password=pass;"));

Update the GreeterService to save the name in the database:

using System.Threading.Tasks;
using Grpc.Core;

namespace grpc_dotnet
{
public class GreeterService : Greeter.GreeterBase
{
private readonly AppDbContext _dbContext;

public GreeterService(AppDbContext dbContext)
{
_dbContext = dbContext;
}

public override async Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
_dbContext.Hellos.Add(new Hello
{
Name = request.Name
});

await _dbContext.SaveChangesAsync();

return new HelloReply
{
Message = "Hello " + request.Name
};
}
}
}

That should be enough for now. And this is where the easy part ends. Let’s dive into the complex part.

Docker configuration

Time to run the server in a Docker container. For that purpose, we will create a super basic Docker file:

~ touch src/grpc-dotnet/Dockerfile
~ touch src/grpc-dotnet/.dockerignore
# Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:5.0
WORKDIR /src

COPY grpc-dotnet.csproj .

RUN dotnet restore

COPY . .

RUN dotnet build

RUN dotnet publish -o /app/publish

WORKDIR /app/publish
EXPOSE 5000

ENTRYPOINT ["dotnet", "/app/publish/grpc-dotnet.dll"]

# .dockerignore:
Properties
obj
bin
Dockerfile
.dockerignore

Let’s try to build that

~ docker build -t grpc-dotnet src/grpc-dotnet

Which for me fails with an unknown reason:

=> ERROR [6/8] RUN dotnet build                                                                                                                                                                                             2.0s
------
> [6/8] RUN dotnet build:
#10 0.963 Microsoft (R) Build Engine version 16.9.0+57a23d249 for .NET
#10 0.963 Copyright (C) Microsoft Corporation. All rights reserved.
#10 0.963
#10 1.214 Determining projects to restore...
#10 1.332 All projects are up-to-date for restore.
#10 1.665 /root/.nuget/packages/grpc.tools/2.37.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6006: "/root/.nuget/packages/grpc.tools/2.37.0/tools/linux_arm64/protoc" exited with code 139. [/src/grpc-dotnet.csproj]
#10 1.668
#10 1.668 Build FAILED.
#10 1.668
#10 1.668 /root/.nuget/packages/grpc.tools/2.37.0/build/_protobuf/Google.Protobuf.Tools.targets(280,5): error MSB6006: "/root/.nuget/packages/grpc.tools/2.37.0/tools/linux_arm64/protoc" exited with code 139. [/src/grpc-dotnet.csproj]
#10 1.668 0 Warning(s)
#10 1.668 1 Error(s)
#10 1.668
#10 1.668 Time Elapsed 00:00:00.67

If you notice, it tried to build using linux_arm64, which apparently fails. Googling shows this information for error code 139:

exit(139): It indicates Segmentation Fault which means that the program was trying to access a memory location not allocated to it. This mostly occurs while using pointers or trying to access an out-of-bounds array index

So apparently linux_arm64 won’t work here. Running the docker under Rosetta (docker has a flag — platform linux/amd64) is also not an option, as it fails with Failed to create CoreCLR, HRESULT: 0x8007FF06 during restore.

The workaround I’ve found was to install protoc manually inside of the container and tell it to use that, instead of trying to load it from the nuget. This is the resulting Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:5.0

RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install --no-install-recommends --assume-yes \
protobuf-compiler-grpc

ENV PROTOBUF_PROTOC=/usr/bin/protoc

WORKDIR /src

COPY grpc-dotnet.csproj .

RUN dotnet restore

COPY . .

RUN dotnet build --verbosity normal

RUN dotnet publish -o /app/publish

WORKDIR /app/publish
EXPOSE 5000

ENTRYPOINT ["dotnet", "/app/publish/grpc-dotnet.dll"]

Let’s spin up a quick compose file:

~ touch docker-compose.yml

And add this content:

version: "3.9"
services:
db:
image: postgres:12
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=hello-db

grpc:
build:
context: ./src/grpc-dotnet
ports:
- 5000:5000
depends_on:
- db

We will need to update our database when the container starts. We will use an entrypoint for that.

~ mkdir src/grpc-dotnet/scripts && touch ./src/grpc-dotnet/scripts/start.sh

Add the following content inside:

#/src/grpc-dotnet/scripts/start.sh#!/bin/sh

dotnet ef database update

dotnet /app/publish/grpc-dotnet.dll

We will also have to modify our Dockerfile to copy the script in the container and use it as a starting point and also install ef dotnet tool so we can execute the migrations. Here is the updated Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:5.0RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive \
apt-get install --no-install-recommends --assume-yes \
protobuf-compiler-grpc
ENV PROTOBUF_PROTOC=/usr/bin/protocRUN dotnet tool install -g dotnet-ef
ENV PATH="${PATH}:/root/.dotnet/tools"
WORKDIR /srcCOPY scripts/start.sh start.sh
COPY grpc-dotnet.csproj .
RUN dotnet restoreCOPY . .RUN dotnet build
RUN dotnet publish -o /app/publish
EXPOSE 5000ENTRYPOINT [ "/bin/sh", "start.sh" ]

Then start everything:

~ docker compose up -d

It should now start. Let’s test it.

~ dotnet run --project src/client

You should see again the “Hello gRPC!” text.

Congrats, we’ve done it!

Hope it will be useful for some of you. If you’ve experienced any issues during the process or you have a better idea about some part, please let me know in the comments below.

All the code is in this GitHub repo for a reference — DStoyanoff/grpc-dotnet-apple-silicon (github.com)

--

--

Daniel Stoyanoff

Passionate about technology and architecture. #dotnet #react #typescript #grpc #microservices