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
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
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
- 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
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
- 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 ourtrivyal_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 intrivyal_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
orCommand+K
) to open the Railway command palette - Choose to deploy a 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
orCommand+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
- 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
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
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.