Mon 27 March 2023

Moving gridref to fly.io

A few years ago I made an effort to learn Clojure, during which time I built a small Clojure library and command-line app GridRef to convert between an Ordnance Survey GB grid reference and British National Grid coordinates. While I was at it I built a web API GridRef Web which exposes the conversion functions via a Ring web app. For several years gridref-web was deployed via Heroku on it's free-tier, following the free-tier coming to an end it needed a new home.

I saw mention of fly.io a little while ago and saw they had a genorious free tier including support for custom domains, https and deployment via Docker so I thought I'd give it a go.

Creating a Dockerfile

The first step was to create a Docker container which turned out to be straight forward. This is the Dockerfile which is based on the official clojure Docker image:

FROM clojure
ENV PORT=8080
ENV JVM_OPTS="-Xmx250m"
COPY . /usr/src/app
WORKDIR /usr/src/app
EXPOSE ${PORT}
CMD lein run "${PORT}"

First we define a PORT environment variable which determines which port the embedded Jetty HTTP server will listen on. I'm defaulting to 8080 which is the default http port that fly.io will forward http requests to.

The JVM_OPTS environment variable is used to limit the amount of memory that our app can use, I've chosen 250m which is slightly lower than the memory available to a fly.io instance on the free tier.

We then copy the contents of the repo to /usr/src/app, set the current directory (WORKDIR) to the same and EXPOSE the PORT.

Finally we start the app via lein run passing the value of the PORT environment variable which is handled by the main method of GridRef Web.

Build and run locally

The Dockerfile can be built and run locally via:

docker build -t gridref-web .
docker run -it --rm --memory-swap 250m --memory 250m --env PORT=8181 -p 8181:8181 --name gridref-web gridref-web

Specifying the same value for --memory-swap and --memory effectively limits the container to that much memory without swap which mimics the fly.io free-tier.

We're setting the PORT environment variable used in the Dockerfile to determine which port the Jetty listens on; the -p option exposes the container port to localhost for testing.

Visiting http://localhost:8181 should load the home page.

Deploy to fly.io

First download and install flyctl before running the following to authenticate (and create an account):

flyctl auth login

Then change to the root of the gridref-web repository and execute the following to configure the app for deployment via fly.io:

flyctl launch

Running flyctl launch prompts for various options (shown below) and creates a fly.toml file which contains the apps configuration.

# ? Choose an app name (leave blank to generate one): gridref-web
# ? Choose a region for deployment: London, United Kingdom (lhr)
# Created app 'gridref-web' in organization 'personal'
# ? Would you like to set up a Postgresql database now? No
# ? Would you like to set up an Upstash Redis database now? No
# ? Create .dockerignore from 3 .gitignore files? Yes
# ? Would you like to deploy now? No

These choices resulted in the following fly.toml:

app = "gridref-web"
kill_signal = "SIGINT"
kill_timeout = 5
primary_region = "lhr"
processes = []

[env]

[experimental]
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

I couldn't see any obvious changes to fly.toml so I moved onto deploying via:

flyctl deploy

At this point the gridref-web application is available at https://gridref-web.fly.dev/.

The application is available by default via https on fly.io (with a redirect from http), I had to make a small change to the code to use the x-forwarded-proto header to determine the client protocol to ensure the links shown on the home page use the correct protocol as fly.io makes requests to my application running in Docker via plain http.

Custom domain and certificate

Setting up a custom domain and associated certificate for http traffic involved:

  1. Creating a CNAME to point gridref.longwayaround.org.uk at gridref-web.fly.dev.
  2. Once the CNAME had propagated, requesting a certificate via the flyctl:
flyctl certs create gridref.longwayaround.org.uk

The web application is now available at https://gridref.longwayaround.org.uk/ 🎉

The application has been running for a few weeks without any issues, it's now running via https and is more responsive due to the instance always being available.

Posts