Use CloudBees CodeShip Pro for CI and Traefik for ASP.NET Applications

Written by: Amit Saha
7 min read

Editor's Note: This post is a follow-on blog post to Setting up Traefik as a Reverse Proxy for ASP.NET Applications.

In this article, we will see how we can Traefik as a reverse proxy for ASP.NET core applications. We will be using Docker containers as our deployment mechanism and use CloudBees Codeship Pro for setting up continuous integration (CI). Let's get started!

Software setup

We will be using Dot Net Core 2.2 for our project and you can obtain the SDK from here. We will be using Linux as our deployment operating system and assume that it has the Docker engine installed, has access to the public Internet and can run Docker containers.

The demo project along with other files can be found in a Git repository. For the rest of this article, I will assume you have access to the repository both locally and the deployment instance.

Sample project

Our sample ASP.NET core project is generated using the .NET CLI as follows:

$ dotnet new webapp --no-https --name demo2
..

The --no-https flag is specified so that the Web application doesn't enforce and redirect to HTTPS as we will be using Traefik instead. Hence the secure sockets layer (SSL) termination will happen at Traefik.

The source for the project can be found in the demo2 sub-directory of the above repository. If you have installed the SDK locally, running dotnet run in the demo2 sub-directory will start kestrel on port 5000:

$ dotnet run
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
      User profile is available. Using '/Users/amit/.aspnet/DataProtection-Keys' as key repository; keys will not be encrypted at rest.
Hosting environment: Development
Content root path: /Users/amit/work/github.com/amitsaha/traefik-aspnet-demos/demo2
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Docker image

Next, we will create a Dockerfile which will create a Docker image of our application:

FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
# Build runtime image
FROM microsoft/dotnet:2.2-aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1
# Assembly name is the same as the project name
ENTRYPOINT ["dotnet", "demo2.dll"]

We make use of multi-stage builds to create the final image which only has the built artifacts. Note, how we also specify a HEALTHCHECK instruction - which will ensure that our service is healthy before any dependent services are spun up (more on this soon).

Instead of building the docker image manually, we will setup continuous integration for our project. There are various free hosted options available out there. For this article, I decided to take CodeShip Pro for a test.

Setting up continuous integration with CodeShip Pro

To setup continuous integration for our repository, we will first have to create a project on CloudBees CodeShip Pro. The free plan should be sufficient for our testing. Once done, we will create two files in our repository - codeship-services.yml and codeship-steps.yml.

CloudBees CodeShip Pro adopts a container first approach which means every step that runs - either building a deployment artifact or running your tests must happen inside a container. The codeship-services.yml file defines the Docker containers that will be used to execute various commands as part of our CI pipeline. It is confusing that these are referred to as services, since they may not be services at all. For example, a container's role could be to just run the tests.

Next, we see a valid codeship-services.yml file which builds a Docker image for our web application and also specifies another service we will use to perform a basic test on the built image.

codeship-services.yml

The following file specifies that we want the following services to be available during a build:

myapp:
  build:
    image: amitsaha/aspnetcore-demo
    context: demo2
    dockerfile: Dockerfile
curl:
  image: pstauffer/curl:latest
  depends_on: ["myapp"]

The first section of the file configures the myapp service - here we specify that we want to build a Docker image amitsaha/aspnetcore-demo using the demo2 directory as context and the file Dockerfile as the Docker file.

Next, we specify the curl service which will use the pstauffer/curl:latest Docker image from Docker hub. We also specify that this service has a dependency on the myapp service. This means that myapp service will be created first and then the curl service will be created. The HEALTHCHECK instruction defined for the myapp image means that the curl service will come up only after the myapp instance is healthy.

codeship-steps.yml

The codeship-steps.yml file is as follows:

- service: curl
  command: curl -I http://myapp

The above configuration file states that we want to execute the command curl -I http://myapp inside the curl service container. Basically this means that a command like docker run pstauffer/curl:latest curl -I http://myapp will be executed.

To summarize, via the two above files, we are testing:

  1. That our application builds successfully and hence we can build the docker image.

  2. We are able to run the image of our application and make a HTTP GET request successfully.

This is a start - and once we do a push to our Git repository or trigger a build manually, we should see that first a Docker image amitsaha/aspnetcore-demo (or another specified image name) is built, then a container is created from the image, and once it's healthy, the curl container is created and our command is executed successfully, thus indicating a successful build.

On a successful build, we want to push our docker image to a docker registry. We will add a step to do that to our codeship-steps.yml file. The complete file will be:

- service: curl
  command: curl -I http://myapp
- service: myapp
  type: push
  image_name: amitsaha/aspnetcore-demo
  encrypted_dockercfg_path: dockercfg.encrypted

By default, the registry is assumed to be Docker Hub. However a number of other registries are supported out of the box. The dockercfg.encrypted file is an encrypted dockercfg file generated using the jet CLI tool and is documented here.

Once we make the above change to our codeship-steps.yml file along with adding the encrypted docker configuration, the next build will push the built Docker image to the demo Docker hub.

Deploying our application

Now that we have our application Docker image, it's time to deploy our application. We will be working on a Linux instance with Docker installed with connectivity to the public Internet as well as allowing incoming traffic from the public internet over port 80 (for lets encrypt HTTP challenge) and 443 for accessing the web application.

We will be deploying two Docker containers, Traefik and the application itself.

First let's create the Traefik container by running the following command from the demo2 sub-directory:

# repository root
$ cd demo2
$ chmod 600 acme.json
$ sudo  docker run -d -p 80:80 -p 443:443 \
        -v $PWD/traefik.toml:/etc/traefik/traefik.toml \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v $PWD/acme.json:/acme.json \
        traefik

Next, we deploy our web application:

$ image="amitsaha/aspnetcore-demo"
$ sudo docker run \
      --label "traefik.backend=aspnetcoredemodemo" \
      --label "traefik.frontend.rule=Host:aspnetcoredemo.echorand.me"  \
      --label "traefik.port=80" \
      --label "traefik.backend.healthcheck.path=/" \
      --label "traefik.backend.healthcheck.interval=5s" \
      --label "app=$image" \
      -d $image

We use Docker labels to specify various configurations that will specify what traffic is our application interested in and where to send the traffic. The most important configuration is the traefik.frontend.rule which states that any traffic with the Host header set to aspnetcoredemo.echorand.me should be sent to this container on port 80.

Note that since we are using LetsEncrypt for SSL certificates, the DNS we provide in the Host above must be valid and publicly resolvable.

Now, from the host, if we run the following command, you will see our web application's home page:

$ curl -k --header 'Host: aspnetcoredemo.echorand.me' https://127.0.0.1/code>

Of course, we can now use a browser to visit the site, https://aspnetcoredemo.echorand.me and we will see our web application.

Conclusion

In this article, we saw how we can deploy a ASP.NET core web application as docker containers on Linux. We used Traefik as a reverse-proxy and setup automatic SSL certificates via LetsEncrypt. Using Traefik also gave us the ability to do zero-downtime deployments and straight forward traffic routing, among a host of other features.

Note: One point worth noting here is that aspnetcoredemo.echorand.me at the time of writing was setup to be valid publicly available DNS record. This is relevant because LetsEncrypt needs the DNS records to be public and resolve correctly to issue SSL certificates. On Amazon AWS, I have created a record set in my Route53 zone and pointed it to the public IP address of my EC2 instance. Since this DNS record will cease to exist at the time you are reading this article, please substitute all references to aspnetcoredemo.echorand.me in this article by a valid public DNS that you own/control.

Additional resources

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.