Mon 27 March 2023
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):
Then change to the root of the gridref-web
repository and execute the following to configure the app for deployment via fly.io:
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:
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:
- Creating a CNAME to point
gridref.longwayaround.org.uk
at gridref-web.fly.dev.
- 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.
comments.