Deno on Fly using Buildpacks
We really like Deno at Fly. It really is a better Node and we’d love people to build more with it. Our plan? “Let's make building and deploying Deno apps on Fly as simple as other languages”. Now, you can configure, build and deploy Deno code in just two commands, and we’d like to show you how we did it.
What’s Deno?
The deno.land website bills Deno as a secure runtime for JavaScript and TypeScript. In practice, it’s like working with a streamlined Node where TypeScript is the norm, with a solid Rust foundation. It also incorporates many of the lessons learned over Node’s development and growth.
As we write this, Deno is heading rapidly towards its first 1.0 release candidate and we are really looking forward to a post-1.0 Deno. We believe as more people discover Deno’s elegance as a platform it’ll become much more popular. So how do you build a Deno app for Fly now?
Straight to the chase
- Name your app’s entry point
server.ts
- Create your Fly app with
flyctl init --builder flyio/builder
touch .config
to create an empty ".config" file to go with your appflyctl deploy
and watch it all build and deploy
That’s it.
Behind the builder
The “chase” above was notably short. How? Let's look at what’s powering this build process. Fly’s builder support landed in early April and is based on the Cloud Native Buildpacks.io (CNB) specifications and implementations for building cloud native container images.
A Cloud Native build is made up of stacks, builders and buildpacks which come together to make a repeatable build process, for different languages and frameworks. Used together, they deliver container images that are ready to run. For the Deno builder, we created our own stack, builder and buildpack to build Deno.
Stacks
Everything in CNB is built on a “stack”, the base operating system in which the builders operate. For the Fly stack, we selected Ubuntu bionic, added in curl and unzip utilities (to support the Deno installer) and built the three images - base, build and run - which will be used in the subsequent steps.
Builders
A builder in CNB is a container loaded with all the OS level tooling needed to construct an image. It’s fundamentally an OS image of some form. It’s not tied to any specific container platform such as Docker, it’s designed to run wherever you can run a container.
That said, the Buildpacks.io developers do have the pack
commmand that uses Docker to run these containers - especially useful on Windows and macOS where there is no native Linux container support.
The builder brings up a container running the build stack and then steps through the buildpacks associated with the builder to work out which build script to run in that container.
Buildpacks
The buildpacks are smaller bundles of scripts (or Go programs) which have two jobs, detect and build.
detect
The detect script tries to work out if the directory contents it has been given are appropriate to build with its build script. For example, a detect script for Ruby would look for a Gemfile
and go “aha! this is the song of my people! run my build script”.
Now, Deno doesn't have an obvious packaging file like Ruby or well, anything else, due to it being so self-contained. So, we made a rule. If there’s a server.ts
file in the directory, we’ll assume it's a Deno application and signal to run our build script.
build
The build script runs in the Builder’s container and does the work of assembling tools and building the layers to create our image. In the case of our Deno buildpack, that includes downloading Deno into the image and running the Deno deno cache
command so all the dependencies are ready to run. Finally, it writes out a set of launch commands to start up the application.
configuration
There's a number of settings that can be passed to the Deno buildpack to control how the application is run. They are kept in the .config
file as a set of key values. You can run with an empty .config
file to get defaults, but you do currently need to have a .config
file present.
First up is the permissions
setting. Deno restricts access to resources by default. You have to specify which resources are available to any program with --allow-*
command-line flags. The permissions setting contains the command-line arguments you’d expect to be added to the Deno command-line. You are most probably always going to have to set the permissions setting. In the example app, this file contains:
permissions="--allow-net"
Which allows network access. If there’s no permissions
setting, currently we default to no permission flags, in step with the default Deno experience. You can incrementally add appropriate permissions to your app as you iterate your code.
If you want to allow all access, you can put -A
into permissions setting, allow all access and sort out permissions later (well, that’s what you’ll say but you know it’ll slip and you’ll do a production deployment with it still in place). It's not recommended but is usefule to know.
The arguments are added to the deno
command when the container and its launch file are built.
Next is the unstable
setting. There are APIs that are labelled unstable in Deno which are not available by default. By adding --unstable
to the command line, applications can access these APIS. With the configuration file, setting unstable=true
achieves the same effect for the buildpack
Finally, the deno_version
setting can force the buildpack to use a particular Deno version. For example, if you needed to use Deno v1.0.2 for an application, then adding:
deno_version=v1.0.2
To the .config
file would make the buildpack use v1.0.2. Don't forget to use the v
in the version number.
Step by Step
Creating an app
So, let’s build a Deno app. We’re going to use dinatra, a Deno module which gives Sinatra-like capabilities to Deno apps. You’ll of course want to install Deno and create a directory for your app. Then make a server.ts
file and put this in it:
import {
app,
get,
post,
redirect,
contentType,
} from "https://denopkg.com/syumai/dinatra/mod.ts";
const greeting = "<h1>Hello From Deno on Fly!</h1>";
app(
get("/", () => greeting),
get("/:id", ({ params }) => greeting + `</br>and hello to ${params.id}`),
);
And that’s an entire simple web server application. Don’t forget permissions though: create a .config
file with permissions="--allow-net"
in it; all this application wants is network access.
Extra steps
You may want to test your app before deploying it. You have two options.
The first is to just run it before packaging it into a container image.
Running the deno command with the permissions:
deno run --allow-net server.ts
will be all you need in that case
The second is to package up the container image and run that image. You’ll need Docker and Buildpacks.io's pack installed locally to do this. Use pack to create the image:
pack build test-server-app --builder flyio/builder
Then use Docker to run the test-server-app
image:
docker run -p 8080:8080 test-server-app
Deploying to Fly
Now we can create a Fly app for this code by running
flyctl init --builder flyio/builder
And we can deploy it with flyctl deploy
What Next
For Fly Buildpacks
We’ve made all the Buildpack code available in a GitHub repository for anyone who wants to improve the process. We’ll be looking to add new buildpacks to it too, and enhance existing buildpacks. It’s worth noting that you can build your own local buildpack and use it with the fly-builder stack for local/test builds - you’ll have to publish it with public access if you want to do Fly deployments with it though.
For Deno on Fly
We’re ready for your next Deno app on Fly, even if it’s your first. With a simple build and deploy process, it's easier than ever.
Update: 8/6/2020: Both the Deno buildpack and this article have been updated with a streamlined configuration mechanism.