Deploy ASP.NET Core Web API to Fly via Docker

In my last post I deployed the standard Blazor template over to vercel static site hosting.

In the standard template, the FetchData component gets its data from a local sample-data/weather.json file via an HttpClient.

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");

I wanted to upgrade this by replacing that call to the local json file with a call to an ASP.NET Core Web API backend.

Unfortunately unlike in the version 1 days of zeit where you could deploy Docker based apps to them vercel now offer serverless functions instead but do not support .NET.

So, as an alternative, I looked at fly.io.

I first used them in 2017 before GitHub supported HTTPS/SSL for custom domains by using them as middleware to provide this service.

Since then they now support deploying Docker based app servers which works in pretty much the same way as zeit used to.

Perfect!

Backend

So, the plan was to create a backend to replace the weather.json file, deploy and host it via Docker on fly.io and point my vercel hosted blazor website to that!

First up I created a backend web API using the dotnet new template and added that to my solution.

Fortunately, the .NET Core Web API template comes out of the box with a /weatherforecast endpoint that returns the same shape data as the sample_data/weather.json file in the frontend.

dotnet new webapi -n backend
dotnet sln add backend/backend.csproj

Next, I needed to tell my web API backend that another domain (my vercel hosted blazor app) would be connecting to it. This would fix any CORS related error messages.

So in backend/Program.cs

private readonly string _myAllowSpecificOrigins = "_myAllowSpecificOrigins";

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy(name: _myAllowSpecificOrigins,
                        builder =>
                        {
                            builder.WithOrigins("https://blazor.now.sh",
                                                "https://blazor.solrevdev.now.sh",
                                                "https://localhost:5001",
                                                "http://localhost:5000");
                        });
    });

    services.AddControllers();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseCors(policy =>
                policy
                    .WithOrigins("https://blazor.now.sh",
                                    "https://blazor.solrevdev.now.sh",
                                    "https://localhost:5001",
                                    "http://localhost:5000")
                    .AllowAnyMethod()
                    .WithHeaders(HeaderNames.ContentType));

    app.UseAuthorization();

    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

Docker

Now that the backend project is ready it was time to deploy it to https://fly.io/.

From a previous project, I already had a handy dandy working Dockerfile I could re-use so making sure I replaced the name of dotnet dll and ensured I was pulling a recent version of .NET Core SDK

Dockerfile

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
# update the debian based system
RUN apt-get update && apt-get upgrade -y
# install my dev dependacies inc sqllite and curl and unzip
RUN apt-get install -y sqlite3
RUN apt-get install -y libsqlite3-dev
RUN apt-get install -y curl
RUN apt-get install -y unzip
# not sure why im deleting these
RUN rm -rf /var/lib/apt/lists/*

# add debugging in a docker tooling - install the dependencies for Visual Studio Remote Debugger
RUN apt-get update && apt-get install -y --no-install-recommends unzip procps
# install Visual Studio Remote Debugger
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
WORKDIR /app/web

# layer and build
COPY . .
WORKDIR /app/web
RUN dotnet restore

# layer adding linker then publish after tree shaking
FROM build AS publish
WORKDIR /app/web
RUN dotnet publish -c Release -o out

# final layer using smallest runtime available
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
WORKDIR /app/web
COPY --from=publish app/web/out ./

# expose port and execute aspnetcore app
EXPOSE 5000
ENV ASPNETCORE_URLS=http://+:5000
ENTRYPOINT ["dotnet", "backend.dll"]

The lines of code in that Dockerfile that were really important for fly.io to work were

EXPOSE 5000
ENV ASPNETCORE_URLS=http://+:5000

I also created a .dockerignorefile

bin/
obj/

I had already installed and authenticated the flyctl command-line tool, head over to https://fly.io/docs/speedrun/ for a simple tutorial on how to get started.

After some trial and error and some fantastic help from support, I worked out that I needed to override the port that fly.io used so that it matched my .NET Core Web API project.

I created an app using port 5000 by first navigating into the backend project so that I was in the same location as the csproj file.

cd backend
flyctl apps create -p 5000

You should find a new fly.toml file has been added to your project folder

app = "blue-dust-2805"

[[services]]
  internal_port = 5000
  protocol = "tcp"

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20

  [[services.ports]]
    handlers = ["http"]
    port = "80"

  [[services.ports]]
    handlers = ["tls", "http"]
    port = "443"

  [[services.tcp_checks]]
    interval = 10000
    timeout = 2000

Make a mental note of the app name you will see it again in the final hostname, also note the port number that we overrode in the previous step.

Now to deploy the app…

flyctl deploy

And get the deployed endpoint URL back to use in the front end…

flyctl info

The flyctl info command will return a deployed endpoint along with a random hostname such as

flyctl info
App
  Name = blue-dust-2805
  Owner = your fly username
  Version = 10
  Status = running
  Hostname = blue-dust-2805.fly.dev

Services
  PROTOCOL PORTS
  TCP 80 => 5000 [HTTP]
             443 => 5000 [TLS, HTTP]

IP Addresses
  TYPE ADDRESS CREATED AT
  v4 77.83.141.66 2020-05-17T20:49:30Z
  v6 2a09:8280:1:c3b:5352:d1d5:9afd:fb65 2020-05-17T20:49:31Z

Now that the app is deployed you can view it by taking the hostname blue-dust-2805.fly.dev and appending the weather forecast endpoint at the end.

For example https://blue-dust-2805.fly.dev/weatherforecast

If all has gone well you should see some random weather!

Login to you fly.io control panel to see some stats

Frontend

Next up it was just a case of replacing the frontend’s call to the local json file with the backend endpoint.

builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri("https://blue-dust-2805.fly.dev") });

A small change to the FetchData.razor page.

protected override async Task OnInitializedAsync()
{
    _forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("weatherforecast");
}

Re-deploy that to vercel by navigating to the root of our solution and running the deploy.sh script or manually via

cd ../../
dotnet publish -c Release
now --prod frontend/bin/Release/netstandard2.1/publish/wwwroot/

Test that everything has worked by navigating to the FetchData endpoint of our frontend. In my case https://blazor.now.sh/fetchdata

GitHub Actions

As a final nice to have fly.io have GitHub action we can use to automatically build and deploy our Dockerfile based .NET Core Web API on each push or pull request to GitHub.

Create an auth token in your project

cd backend
flyctl auth token

Go to your repository on GitHub and select Setting then to Secrets and create a secret called FLY_API_TOKEN with the value of the token we just created.

Next, create the file .github/workflows/fly.yml

name: Fly Deploy
on:
    push:
        branches:
            - master
            - release/*
    pull_request:
        branches:
            - master
            - release/*
env:
  FLY_API_TOKEN: $
  FLY_PROJECT_PATH: backend
jobs:
  deploy:
      name: Deploy app
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v2
        - uses: superfly/flyctl-actions@1.0
          with:
            args: "deploy"

Notice that in that file we have told the GitHub action to use the FLY_API_TOKEN we just setup.

Also because my fly.toml is not in the solution root but in the backend folder I can tell fly to look for it by setting the environment variable FLY_PROJECT_PATH

FLY_API_TOKEN: $
FLY_PROJECT_PATH: backend

Also, make sure the fly.toml is not in your .gitignore file.

And so with that, every time I accept a pull request or I push to master my backend will get deployed to fly.io!

The new code is up on GitHub at https://github.com/solrevdev/blazor-on-vercel

Success 🎉