Getting Started With .NET Core Microservices Using Docker & VS Code on OSX

I’ve started working on some .NET Core microservices stuff and thought I’d outline the steps I took to get a basic instance of a .NET Core service up and running in Docker on OSX.

Install .NET Core

The full instructions are here
First, install openssl using Homebrew

brew update
brew install openssl
mkdir -p /usr/local/lib
ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/
ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/

Install the .NET Core SDK from the page above.

I had an issue after the install where the dotnet command wasn’t exposed on my path. Found the following fix on one of the github issue comments:

ln -s /usr/local/share/dotnet/dotnet /usr/local/bin

Some comments suggest it may be because I use oh-my-zsh.

Once the sdk is setup successfully you can init a project:

mkdir dotnet-microservice
cd dotnet-microservice
dotnet new
dotnet restore
dotnet run

That should produce a simple Hello World output:

helloworld.png

Install VS Code

Now that we have the basic .NET Core app let’s install VS Code to use as our IDE. The OSX download is available at code.visualstudio.com

We need to add some nuget packages to the project so the first step is adding the Nuget package manager.

Go to View → Extensions and search for Nuget, the Docker extension is also useful for adding docker commands to VS Code.

nugetextension.png

Once you have Nuget installed you can access Nuget commands in the command bar with F1:

commandbar.png
add.gif

I added Kestrel and a couple of MVC nuget packages, you can see the list in the project.json file further down.

Once you add a nuget package through the UI you will get prompted to restore (dotnet restore is the terminal command):

vscoderestore.png
Some Code

A microservice will normally expose a RESTful interface, to serve as the host we add a WebHostBuilder to the main method. Kestrel is a lightweight web server included in .NET Core. The Startup class is passed to the web host and allows you to define routing, DI and other services. The ConfigurationBuilder can be used to compile config settings from different sources; file, args, env, etc.

        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var config = new ConfigurationBuilder()
            .Build();

            var builder = new WebHostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseConfiguration(config)
                .UseStartup<Startup>()
                .UseKestrel()
                .UseUrls("http://localhost:5000");

            var host = builder.Build();
            host.Run();
        }

The Startup class here is pretty basic, I just went with the default routing provided out of the box by MVC. You can add different route patterns or you can use attribute based routing if desired.

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore();
        }
        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            app.UseMvcWithDefaultRoute();
        }
    }

The controller is a simple math function. Um, multiplication…​

    public class CalculatorController : Controller
    {
        public string Multiply(int first, int second)
        {
            return (first * second).ToString();
        }
    }

The project.json lists the dependancies. In the most recent version of .NET Core, these .json files get upgraded to .csproj files

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Mvc.Core": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.0"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

Using dotnet run you can start the server:

curl.png

The debug mode of VS Code lets you debug the application as you would expect:

debug.png

Docker

Now that we can run the service locally, lets add it to a docker container. The first step is to create a dockerfile. The Docker extension for VS Code has some commands built in:

dockercommands.png

This was the first version of my dockerfile, I’ll walk through some of the issues I had to troubleshoot further down.

FROM microsoft/aspnetcore:1.0.1
LABEL Name=dotnet-microservice Version=0.0.1
ARG source=.
WORKDIR /app
EXPOSE 5000
COPY $source .
ENTRYPOINT dotnet dotnet-microservice.dll

Building my docker file from VS Code:

docker build -f Dockerfile -t dotnet-microservice:latest .
Sending build context to Docker daemon 518.1 kB
Step 1/7 : FROM microsoft/aspnetcore:1.0.1
1.0.1: Pulling from microsoft/aspnetcore

Base image vs app version mismatch

Running the docker container:

▶ docker run -it --rm dotnet-microservice:latest
The specified framework 'Microsoft.NETCore.App', version '1.1.0' was not found.
  - Check application dependencies and target a framework version installed at:
      /usr/share/dotnet/shared/Microsoft.NETCore.App
  - The following versions are installed:
      1.0.1
  - Alternatively, install the framework version '1.1.0'.

My first issue was the incorrect version of my app compared to the aspnetcore version I pulled down in my base image.

I updated the base image in the dockerfile and rebuilt: FROM microsoft/aspnetcore:1.1.0

Copying the correct folder

▶ docker run -it --rm dotnet-microservice:latest
Did you mean to run dotnet SDK commands? Please install dotnet SDK from:
  http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409

The issue this time turned out to be the COPY command wasn’t copying my build output to the working dir of the image.

COPY $source/bin/Debug/netcoreapp1.1/ .

Publishing artifacts

docker run -it --rm dotnet-microservice:latest
Error: assembly specified in the dependencies manifest was not found -- package:
'Microsoft.DotNet.PlatformAbstractions', version: '1.1.0', path: 'lib/netstandard
1.3/Microsoft.DotNet.PlatformAbstractions.dll'

This time I realized I was copying the build output but I needed to be running a publish so that dependencies are included. I needed to run dotnet publish and change the COPY to pick up the published output.

▶ dotnet publish
Publishing dotnet-microservice for .NETCoreApp,Version=v1.1
Project dotnet-microservice (.NETCoreApp,Version=v1.1) was previously compiled. S
kipping compilation.
publish: Published to /Users/danny/dev/dotnet-microservice/bin/Debug/netcoreapp1.
1/publish
Published 1/1 projects successfully

COPY $source/bin/Debug/netcoreapp1.1/publish .

Port forwarding

The publish did the trick, now I was able to run the docker container…​ but not access the endpoint.

▶ docker run -it --rm dotnet-microservice:latest
Hello World!
Hosting environment: Production
Content root path: /app
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

I ran the docker container specifying * as the host to bind to and forwarding port 5000 on the container to post 5001 on my machine.

▶ docker run -p 5001:5000 dotnet-microservice
Hello World!
Hosting environment: Production
Content root path: /app
Now listening on: http://*:5000
Application started. Press Ctrl+C to shut down.
curl2.png

Curl confirms that there is nothing running on port 5000 and the container is exposed on port 5001.

dockerps.png

The docker ps command lists the running containers

That’s it, a simple microservice running on the .NET Core Kestrel server and packaged up in a docker image. It’s a basic structure for somethign that can be built upon in future.

I’ve included the terminal commands/output just in case others hit similar issues and hopefully the solutions above will help them.

comments powered by Disqus