Avatar of Andrew BekhietAndrew Bekhiet

Deploy a Dart App on Railway, Part 1

Have you ever been on a trip with friends and found yourself in a heated trivia competition?

No ….? Ok … no problem

However, if your answer was yes, then the rest is for you!

That's what happened to me recently. While waiting to reach our destination, me and my friends started a "who knows best" game. It was fun and exciting, but I quickly realized there was room for improvement.

The problem? No one was keeping track of the score, and we kept arguing over the correct answers.

As such, and as a programmer, I decided to make an app

Pro-gamer move

Pro-gamer move

The goal is simple, to make an app that everyone can use to play trivia games.

  • All players see the same question at the same time and everyone has a chance to answer.
  • After all players submit their answers, the app will show the correct answer on the host device, whether or not each player answered correctly on their devices.
  • A bonus is added to players who answer quicker than others.
  • After all questions are answered by all players, the final result is shown on the host, and the rank of each player is shown on their device.
  • Players can sign up for an account to be able to create games and host them
Overview of app flow

Overview of app flow

For this tutorial, we’ll be using Flutter for the frontend with rxdart streams for realtime communications and Riverpod for dependency injection.

And for the backend, I didn’t really know which Dart package/framework to choose. So I did a small research and decided to go with Serverpod. Why? glad you asked:

  • Typesafe Dart: Serverpod allows you to call functions from your Flutter app as if they were in another file, it doesn’t feel like you’re calling some flaky string function name with flaky json, it’s all typesafe Dart from client to server
  • Better Developer Experience: Serverpod offers many features that makes backend development very efficient. Things like automatic code generation, built-in ORM, migrations and logging

In addition, it has built in support for authentication and data streaming, which we need in this project to build realtime multiplayer experience

So, and without further ado, let’s get started and create a new Flutter + Serverpod project

  • First off, download the Serverpod CLI
dart pub global activate serverpod_cli
  • Create a new Serverpod project and open it in your preferred code editor
serverpod create trivyal
cd trivyal

If this is your first time using Serverpod, check out their documentation to understand Serverpod project structure

Let’s get our project setup in Railway!

  • Head to dev.new to create a new project on Railway
  • Select Deploy PostgreSQL from the options
Selecting PostgresQL from New Project prompt

Selecting PostgresQL from New Project prompt

  • After the database is deployed, you can go to settings and enable App Sleeping so our elephant doesn’t get sleep deprived
  • Edit the trivyal_server/config/production.yaml file to match the deployed railway db
  • In the settings tab under Networking, copy the provided TCP proxy host and port number
  • Update the values in trivyal_server/config/production.yaml

The provided host should end with .proxy.rlwy.net

Here the public networking proxy hostname is autorack.proxy.rlwy.net and the provided port is 46821

Here the public networking proxy hostname is autorack.proxy.rlwy.net and the provided port is 46821

This is how your database section should look like in production.yaml:

database:
  host: postgres.railway.internal # Or railway proxy provided hostname (something.proxy.rlwy.net)
  port: 46821
  name: railway
  user: postgres
  requireSsl: true
  • Go to trivyal_server/config/passwords.yaml file and change the database password to match the deployed database password
⚠️
Warning: Never commit your plain text passwords to git
  • Now that serverpod has all the info to connect to our database, we’re ready to run the first migration using:
dart bin/main.dart --mode production --apply-migrations

Make sure to run the previous command in the trivyal_server directory

  • Install the Railway CLI using your preferred method, for example using Homebrew:
brew install railway
  • Authenticate your CLI session:
railway login
  • Link to the project and service:
railway link
  • Finally, use Railway up to deploy the code:
railway up -d

Railway will automatically detect the Dockerfile at the root of the project and automatically build and deploy our app

The provided Dockerfile at trivyal_server/Dockerfile only builds the server directory, but doesn’t add the flutter web build to the static hosting

To fix this:

  • Move the trivyal_server/Dockerfile to the root of our monorepo
  • Edit Dockerfile so it installs Flutter SDK, makes a web build and copies the build to our trivyal_server/web directory
FROM dart:stable AS build

WORKDIR /server
COPY trivyal_server .

# Build our server
RUN dart pub get
RUN dart compile exe bin/main.dart -o bin/server

WORKDIR /web
COPY trivyal_flutter/ .
# Copy our client package code. The flutter app depends on it
COPY trivyal_client/ ../trivyal_client/

# Install Flutter SDK
RUN apt-get update && apt-get -y install curl git unzip xz-utils zip libglu1-mesa
RUN git clone https://github.com/flutter/flutter.git /usr/local/flutter
ENV PATH="${PATH}:/usr/local/flutter/bin"

# Run web build
RUN flutter pub get
RUN flutter build web

FROM alpine:latest

ENV runmode=production
ENV serverid=default
ENV logging=normal
ENV role=monolith

# Copy dart runtime and build artifacts
COPY --from=build /runtime/ /
COPY --from=build /server/bin/server server
COPY --from=build /server/config/ config/
COPY --from=build /server/web/ web/
COPY --from=build /server/migrations/ migrations/

COPY --from=build /web/build/web/ web/

EXPOSE 8080
EXPOSE 8081
EXPOSE 8082

ENTRYPOINT ./server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role

If you named your project differently, you should update the directories’ names in the previous Dockerfile

  • Remove the default Serverpod index template in trivyal_server/web and its renderer in trivyal_server/lib/src/web
  • Update trivyal_server/lib/server.dart file:
import 'package:serverpod/serverpod.dart';

import 'package:trivyal_server/src/web/routes/root.dart';

import 'src/generated/protocol.dart';
import 'src/generated/endpoints.dart';

// This is the starting point of your Serverpod server. In most cases, you will
// only need to make additions to this file if you add future calls,  are
// configuring Relic (Serverpod's web-server), or need custom setup work.

void run(List<String> args) async {
  // Initialize Serverpod and connect it with your generated code.
  final pod = Serverpod(
    args,
    Protocol(),
    Endpoints(),
  );

  // If you are using any future calls, they need to be registered here.
  // pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall');

  // Serve all files in the trivyal_server/web directory.
  // This directory will be populated by Docker with our flutter web build.
  pod.webServer.addRoute(
    RouteStaticDirectory(
      serverDirectory: '.',
      serveAsRootPath: '/index.html',
      basePath: '/',
    ),
    '/*',
  );

  // Start the server.
  await pod.start();
}
  • Go back to Railway dashboard and press (Ctrl+K or Command+K) to open the Railway command palette
  • Choose to deploy a new empty service
Choosing new empty service

Choosing new empty service

To make our server able to connect to our postgres database, we need to setup environment variables. Don’t worry no need to go back and forth between both services, as Railway provides variables references that allow us to reference variables from our postgres service to our server

  • Go to Variables tab and paste this JSON in the Raw editor:
{
  "SERVERPOD_DATABASE_USER": "${{Postgres.POSTGRES_USER}}",
  "SERVERPOD_DATABASE_PASSWORD": "${{Postgres.POSTGRES_PASSWORD}}",
  "SERVERPOD_DATABASE_HOST": "${{Postgres.RAILWAY_PRIVATE_DOMAIN}}",
  "SERVERPOD_DATABASE_PORT": "5432",
  "SERVERPOD_DATABASE_NAME": "${{Postgres.POSTGRES_DB}}",
  "SERVERPOD_DATABASE_REQUIRE_SSL": "true"
}
  • Also, go to service settings and enable App Sleeping so our server can sleep at network traffic nights
  • Finally, click Deploy changes or press Shift+Enter

Railway will detect the Dockerfile at the root of the project and automatically build and deploy our app.

Because Serverpod is modular and you may not need to use all the features at once, by default the api server listens on port 8080 and the webserver listens on 8082

Since we can use Railway’s provided domain for only one port (for now), we would need to split the http traffic somehow to port 8080 and port 8082 for the api and the static webserver respectively

Luckily, we can use this ready-to-delpoy Serverpod reverse proxy template

  • Press Ctrl+K or Command+K and choose to deploy a new service from a template
  • From the templates list, choose Serverpod reverse proxy: Unite your frontend & serverpod backend under one domain
Selecting the Serverpod reverse proxy

Selecting the Serverpod reverse proxy

  • If you kept all Serverpod’s default config, the api server should be at port 8080 and the webserver at port 8082. The domain should be the Serverpod service private domain
    So your Caddy service environment variables should look like this:
{
  "BACKEND_PORT": "8080",
  "FRONTEND_PORT": "8082"
  "BACKEND_DOMAIN": "${{trivyal.RAILWAY_PRIVATE_DOMAIN}}",
  "FRONTEND_DOMAIN": "${{trivyal.RAILWAY_PRIVATE_DOMAIN}}",
}
  • After the reverse proxy is deployed, go to Networking section and change the domain name to anything you like
How your canvas should look like at the end of Railway backend setup

How your canvas should look like at the end of Railway backend setup

To make sure everything is working, go to your caddy proxy’s domain. You should see something like this:

Testing that your Serverpod service is working

Testing that your Serverpod service is working

And if you make a get request to /api/example/hello?name=world, it should return “Hello world”

$ curl "https://trivyal.up.railway.app/api/example/hello?name=world"
"Hello world"%

If that is not the case, you might need to check the server logs in Railway

Anyways, that was just part 1 of Deploying a Dart app to Railway, if you want to continue reading, you should checkout part 2

Here are some helpful resources if you have any trouble.