Building serverless apps requires quite a change in thinking; all these functions and their cold-starts can feel like a whole new world to people who are used to their servers. Fly.io tries to ease these things with a service that allows us to deploy serverless containers instead of functions.
Based on AWS Firecracker, which also powers AWS Lambda, they offer a way to run containers PaaS-style, but with a serverless-y on-demand pricing.
I tried to build a small WebSocket based chat application with it, so you can look at what Fly.io does for you!
Serverless Container Chat
TL;DR You can find the code to the example app on GitHub.
We will build a simple chat app based on Node.js, Express, and Socket.IO. All deployed into a Fly.io container!
Prerequisites
To follow this tutorial, you need the following:
Setup a Node.js Project
We can use NPM to create a new Node.js project:
$ mkdir flyio-websockets
$ cd flyio-websockets
$ npm init -y
Install NPM Packages
Since we will be using Express and Socket.IO, we have to install them first:
$ npm i express socket.io
Implement the Backend
To implement the backend we have to create an index.js
file that contains the following code:
const http = require("http");
const express = require("express");
const socketIo = require("socket.io");
const app = express();
const httpServer = http.createServer(app);
const wsSever = socketIo(httpServer);
app.get("/", (request, response) =>
response.sendFile(__dirname + "/index.html")
);
let userNumber = 1;
wsSever.on("connection", (socket) => {
const userId = "User-" + userNumber++;
socket.on("msg", (msg) =>
wsSever.emit("msg", userId + ": " + msg)
);
});
httpServer.listen(8080);
Less than 20 lines of code are needed to create the backend. First, we set up the HTTP and WebSocket server and then tell them what to do.
The HTTP server will deliver the Socket.IO client library and an index.html
file that will act as our client later.
The WebSocket server will wait for WebSocket connections and pass on every msg
event it gets to all clients. It will also add a user ID to the messages, so the clients can see who wrote it.
Then we listen on port 8080
.
Implement the Client
For the client, which will be delivered by the HTTP server, we have to create an index.html
file that should contain this code:
<!DOCTYPE html>
<title>Serverless Container Chat</title>
<form>Say Something: <input /></form>
<hr>
<div></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const form = document.querySelector("form")
const input = document.querySelector("input")
const div = document.querySelector("div");
form.addEventListener("submit", (event) => {
event.preventDefault();
socket.emit("msg", input.value);
input.value = "";
return false;
});
socket.on("msg", (msg) => {
div.innerHTML += `${msg}<hr>`;
});
</script>
As you can see, the HTTP server also hosts the Socket.IO client library to include it right away with no external dependencies.
Adding a Start Script
It is not strictly needed to try our current setup, but later the Fly.io service will use the NPM start script, so let's create one. This way, we can check locally if everything works as intended.
The package.json
should contain the following code:
{
"name": "flyio-websockets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"socket.io": "^2.3.0"
}
}
Test the App Locally
To run the app, we can now use our NPM script!
$ npm start
We can access the app in a browser under http://localhost:8080/ and see that everything is working.
Add Fly.io Support
Now that we have a self-contained Node.js app, we can add Fly.io support by creating a fly.toml
configuration file that contains information for our deployment.
app = "flyio-websockets"
[build]
builtin = "node"
[[services]]
internal_port = 8080
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
Since Fly.io will deploy your app on <APP_NAME>.fly.dev
, you have to replace the flyio-websockets
with the name of your choice.
The build
part will tell Fly.io that we run a Node.js application. The fly CLI will bundle your project up and upload it to the Fly.io service, which will try to install our dependencies and put everything into a Docker image that will become the base for our deployment.
The services
part defines the container-specific port we used in our Node.js app. With services.ports
we tell Fly.io how we want to access this port from the outside.
Then there are some additional configurations for scaling and health checks.
That's all there is to it—just a small TOML file.
Deploy the App
Now, we're finally ready to deploy our application to Fly.io.
You can deploy with the following command:
$ fly deploy
The command will look into the fly.toml
, upload our files, and tell the Fly.io service to build and run everything.
After a few seconds, the command should finish, and we can run the status command.
$ fly status
The output of the status command should include the URL our app is hosted at.
Serverless Container as an Alternative
The serverless paradigm helps us to minimize operational overhead in the long run. But since everyone and their dog "has a problem that doesn't fit serverless," containers won't go away soon.
Fly.io seems to be a good offering in that space, and when it gets support in IaC tools, I think it can save quite some time for folks out there.
What do you think? Do you want more Heroku-like offerings or is serverless going in the right direction? Tell me in the comments!