Running a .NET 5 web app on Railway

I've been working on a fun little project on my stream recently. It's an app called StreamBadger that adds overlay images and plays sounds on the stream through OBS Studio. It uses the Twitch API so it needs to authenticate with Twitch, and as it's a desktop application I wanted to build that authentication flow where you log in through the browser and the app sits and waits and then magically gets connected.

This is pretty straightforward: open a browser page, passing some kind of unique key, then poll an endpoint on the server until it returns the mystic runes you seek. So this needs to be an ASP.NET Core MVC app, and it needs some transient external storage like, say, a Redis instance. And this is an open source side-project and I really don't want to be paying for hosting for an essential component of it.

A couple of weeks earlier, Khalid tweeted a link to a .NET app running on a .railway.app sub-domain, and I'd been intrigued and checked out what that was. Turns out, Railway is a cloud platform that provides really, really simple hosting and deployment, as well as plug-ins for things like Redis, MongoDB and Postgres. The good bit is that it's free for up to three projects. So I thought I'd give it a try and last night, on the stream, I went for it.

Creating a Railway project

I used the Railway web UI to create the project and added the Redis plugin to it. This sets some variables like REDISHOST and REDISPASSWORD which are available as environment variables at runtime. I also added my Twitch app details as custom variables here.

Installing the Railway CLI

Railway provides a CLI that lets you set up projects, run them locally and deploy them to the cloud with simple commands like railway run. Unfortunately it doesn't work very well on Windows, but that's no problem these days because Windows runs Linux, and it works perfectly there. I initially tried to install it using NPM, but that didn't work. That's not Railway's fault: NPM just didn't work.

Luckily for me Jake from Railway was in the stream chat and he suggested using the Curl installation, and very helpfully pasted the command into the chat. So I copied and pasted it into my WSL terminal, because running Linux commands from my Twitch chat with sudo is how I roll.

Then I cd'd into the Windows project directory from my WSL shell (cd /mnt/d/Twitch/StreamBadger). Technically there's a tiny performance overhead from working with Windows directories in WSL, but in this case it made no noticeable difference.

Logging in

First up I had to do railway login which, funnily enough, took me through the exact same process I'm trying to create for StreamBadger: it opened a browser page and, because I was already logged in, just magically authenticated. Now my WSL session was connected to my Railway account.

Linking the project

On the Railway dashboard page from my project there was yet another command I could just copy and paste into my terminal to link the current directory with the Railway project. So I did that, but not with sudo this time because that was not necessary. I linked from the project root directory where the .sln and Dockerfile files were.

Adding a Dockerfile

Railway runs a bunch of different things like Node.js, Python and Ruby, but it will also just run Docker images, which means it can handle pretty much anything. If there's a Dockerfile in your directory, it will just use it. So I created a standard Dockerfile for my web app:

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY ./StreamBadger.sln .
COPY ./src/StreamBadger/StreamBadger.csproj ./src/StreamBadger/
COPY ./src/StreamBadgerDesktop/StreamBadgerDesktop.csproj ./src/StreamBadgerDesktop/
COPY ./src/StreamBadgerLogin/StreamBadgerLogin.csproj ./src/StreamBadgerLogin/
RUN dotnet restore

# Copy everything else and build
COPY . .
RUN dotnet publish --no-restore -c Release -o out ./src/StreamBadgerLogin

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["./StreamBadgerLogin"]

I also added a .dockerignore file to ignore bin and obj directories.

**/bin/
**/obj/

This is important if you're developing on Windows because the obj directory contains a file called project.assets.json, which is like a lock file for Nuget packages and such. If you copy this into the Docker image, which is running Linux, then the dotnet restore build step will explode and kill your cat.

Running the project locally

You can test your code straight away, pointing at the actual resources in the cloud, like my Redis plugin. I just had to tweak the code to read the connection details from the environment variables mentioned earlier. Then it's just railway run and it builds the Docker image and runs your app on localhost. Slightly weird thing, though: it arbitrarily chooses a port to bind to. In my case it chose 4411. This is also exposed as an environment variable, PORT, which I believe is quite a common practice in the land of script. Jake from Railway pointed this out and I added some code to my app to use this port:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();

            // For running in Railway
            var portVar = Environment.GetEnvironmentVariable("PORT");
            if (portVar is {Length: >0} && int.TryParse(portVar, out int port))
            {
                webBuilder.ConfigureKestrel(options =>
                {
                    options.ListenAnyIP(port);
                });
            }
        });

Then I did railway run again and there was my app, happily running in Docker and connecting to the Railway Redis instance.

Deploying to the cloud

The theory here is that if railway run works on your machine, then it should work in the cloud too. To deploy it, you just use railway up. That's it. Under the covers it builds your Docker image, pushes it to Railway's registry, spins up the container and connects it to your sub-domain (in this instance streambadger-production.up.railway.app/). And it just worked. Took a couple of minutes and there it was.

Connecting an actual domain

I've bought streambadger.com because of course I have, so I wanted to get that hooked up as well. I do pretty much all my DNS through Cloudflare because they're awesome and do magic SSL and such, so I just needed to set up a CNAME pointing to the Railway sub-domain and... no wait, infinite redirect loop. I'd missed a helpful bit in the Provider Specific Instructions that tells you to use the Full SSL/TLS encryption mode with Cloudflare. This is because Railway just won't serve your site over plain old HTTP, but Cloudflare's Flexible encryption mode only proxies to HTTP.

In summary, then

I'm super impressed with Railway. They've obviously worked very hard on the user experience, and it shows. I had Jake in the stream chat to help me out so I wasn't having to refer to the docs, but they're really good. The one small hiccup was the Windows issues with the CLI, which isn't great, but I'd expect any developer in 2021 to be running WSL anyway.

You get three projects with two plugins per project on the Free plan, with a couple of environments (e.g. Staging and Production) as well. The next step up is the $20/month "Early Adopter" plan, which is just unlimited everything: projects, plugins, environments, deploys. And access to something called Teams, which I don't know what that is.

I've got a few things I run online, and I'm pretty sure if I moved them all over to Railway on that $20 plan I'd be saving money over the various cloud platforms I'm using at the moment. It won't replace Azure for more complex stuff with a wider range of platform services, but for side projects and simple sites or APIs I think Railway looks great.

Disclaimer

This post was not sponsored or requested by Railway. I used it and liked it and just wanted to share.

Twitch

If you want to watch me learn more stuff like this in real-time, stop by my Twitch channel some time. I'm usually on at 7pm UK (6pm UTC) on Tuesdays and Thursdays.

Previous
Previous

My 20 years with .NET

Next
Next

WhereNotNull in LINQ