Racket on Digital Ocean App Platform
Updated Tuesday, Jan 9, 2024
Digital Ocean’s App Platform works fairly well with Racket. To activate this service, create a git repository with a Dockerfile in it. Your app will be ran as a container built from the Dockerfile and hosted by Digital Ocean. It appears this platform handles load balancing and scaling. These are good value prospects when considering hosting on this platform.
If you wish to use a custom domain you can also do so, such as was the case
with p.winny.tech
(Note p.winny.tech
is presently defunct).
§rpaste - Yet another Racket pastebin
> gosh yet another pastebin!!!
Now I just need something to host on D.O. Paginating through various code-bases, I rediscovered an aging pastebin written in Racket authored by yours truly back in 2016 when I was first learning Racket. I called this pastebin “rpaste”. This web app will also be a good demonstration of all the necessary code changes to migrate a Dockerized app into Digital Ocean App Platform.
When I initially pulled this rpaste Racket app off the shelf, it looks like it
was Dockerized (4615eab
Package rpaste + Dockerize), so I was able to deploy
it right away just by telling the D.O. App Platform where the git repository is
located.
There were some snags that required code changes to deploy else where, but I’m pleased to describe these changes as very predictable.
§Racket package build times
It’s fairly easy to define your Racket package’s dependencies so that it
eventually pulls in racket-doc
. When this happens your Racket deployment
multiplies its size for the entire racket codebase, documentation, optional
dependencies, and all.
My usual work around is to either vendor the package that’s pulling in
documentation and fix its dependencies. Then I try to send off a patch to
upstream to fix this packaging issue. Needless to say the process can be a
little frustrating. As a pro tip, install pkg-dep-draw
then run racket -l- pkg-dep-draw your-pkg
. With this tool, one can visualize how exactly
documentation dependencies are being pulled in to the dependency tree.
§No Docker Volumes
This Dockerized rpaste setup uses Docker volumes. Unfortunate for me, D.O. App Platform does not support volumes. I took this as an opportunity to switch from sqlite3 (with the database path rooted in a persistent Docker volume) and migrate to postgresql, which is the database I’d choose for new work (if not sqlite3).
After setting up dotenv
support combined with support for connecting to a
postgresql database via POSTGRESQL_HOST
, POSTGRESQL_USER
,
POSTGRESQL_PASSWORD
, it turns out that Digital Ocean’s Postgresql Database
hosting uses non-standard ports. This buggy behavior manifested by the app
failing to meet the liveliness checks that are done to verify if the app
successfully started. Adding some logging around the startup helped track down
the issue.
I had been misusing the db
Racket library to connect to sqlite3 (and now
postgresql). This had caused a bit of confusion where I’d get a logline
stating the table Pastes could not be found. It occurred that prior to
changing implementation slightly, the code would spin up multiple PostgresSQL
connections per connection. The CREATE TABLE IF NOT EXISTS
that’s ran on
initial connection seemingly was not always ran, or at least not in the same
transaction. It turned out I was misusing virtual-connection
or some other
db
library constructor for connecting to databases. If you’re using
virtual-connection
be aware of the caveats and shortcomings. The solution I
found was to first create a parameter that stores either #f
or an active db
connection. Then use this parameter and a single instance of a connection pool.
§Shortest identifiers for pastes
This isn’t really a problem, but more of an interesting design decision. When initially authored URLs to Pastes contained a full SHA1 checksum. Enter Base58. It is a 8-bit safe encoding that avoids special characters and confusing ones. If one wants Base64 but don’t like those special symbols, Base58 is for you. These characteristics means it can be used virtually anywhere a simple hexadecimal encoding of a byte string could be used. A 40 character hex representation of a SHA1 Digest is only 28 characters using Base58 encoding. Like using a SHA1 hex string, using a SHA1 Base58 string also offers no easy way to guess the URL of other pastes created around the same time.
§Docker Build Registry hates me
It turns out if you have a hypen-minus at the end of your GitHub username, this breaks your usage of docker Build Registry. I opened a support ticket to ask them how to resolve the problem, but no results thus far. See this brief write-up by luxemboye on the issue.
§Brief outline of the interesting code snippets
Links to the important files that are needed to deploy any Racket codebase:
info.rkt
- Declare dependencies and other package metadata.
entrypoint.sh
- Script to be ran at the end of the
Dockerfile
. Dockerfile
- To Dockerize the app.
main.rkt
(require your-package)
will loadmain.rkt
.
(See Git repository for full code listings.)
§info.rkt
This declares the package name, version, dependencies, and in this case it also
declares that a rpaste
program in your $PATH
should launch the package’s
main.rkt
. main.rkt
is the entrypoint. Declaring the correct deps
and
build-deps
will determine if your Docker image takes tens of seconds to build
or if it takes ten minutes to build. I wonder if this unpredictable build
time might be a specter for Racket productivity?
#lang info
(define collection "rpaste")
(define version "0.1.2")
(define deps
'("base"
"db-lib"
"web-server-lib"
"mime-type-lib"
"rackunit-lib"
"rackunit-typed"
"typed-racket-lib"
"binaryio-lib"))
(define racket-launcher-names '("rpaste"))
(define racket-launcher-libraries '("main.rkt"))
§entrypoint.sh
Execute the installed package’s entrypoint. This executes main.rkt
as
referenced in the info.rkt
.
#!/bin/sh
exec racket -l rpaste -- -p 8080
§Dockerfile
Note the Dockerfile copies in only info.rkt
then installs dependencies.
Whenever info.rkt
is modified, the dependencies will be reinstalled.
Otherwise dependencies are cached. I forgot what --no-docs
does, but it does
not exactly suppress the installation of documentation. For example you may
still end up with racket-doc
installed if something (indirectly) depends on
it. I do remember it did perhaps reduce footprint.
FROM racket/racket:8.3
WORKDIR /app
COPY info.rkt .
RUN raco pkg install --auto --no-docs --name rpaste
COPY . .
RUN raco setup --no-docs rpaste
ENTRYPOINT /app/entrypoint.sh
§main.rkt
Condensed down for essence. Declares a fun
procedure as an main entry
point. The (module+ main ...)
is the program entry point that itself calls
the fun
procedure.
Note the usage of command-line
to parse command line arguments.
#lang racket
;;; Note this is edited down for brevity.
(define (fun)
;; Default values for the command line.
(define listen-port (make-parameter 8080))
(define listen-ip (make-parameter #f))
(command-line
#:once-each
[("-p" "--port") port-string "Port to listen on"
(listen-port (string->number port-string))]
[("--ip") ip-string "IP to listen on"
(listen-ip ip-string)])
(db:setup-connection) ; See GitHub for the definition
(log-rpaste-info "Connected and schema created. Visit http://~a:~a/" (or (listen-ip) "0.0.0.0") (listen-port))
(serve/servlet (log-request (headize dispatch))
#:stateless? #t
#:listen-ip (listen-ip)
#:port (listen-port)
#:servlet-path "/"
#:servlet-regexp #rx""
#:command-line? #t
#:server-root-path "."
#:file-not-found-responder (log-request (headize not-found))))
(module+ main
(with-logging-to-port (current-error-port)
fun
#:logger rpaste-logger 'debug 'rpaste))
§Pricing
Using lexilambda’s buildpack for heroku, one can get free dynamos with free database access. My setup uses Digital Ocean’s managed Postgresql service, which is $15/mo (per cluster). The App Platform costs $10/mo for entrylevel use like in this article. So hosting an app on DO is $25/mo, but could work if you have a budget. Alternatively consider Heroku, Kubernetes, or VPS hosting. It’s probably cheaper. A $5 VPS (such as the $5 option from Digital Ocean) could host multiple applications like this depending on volume and scale.
§Conclusion
If you can afford it, Digital Ocean App Platform works well for Racket app development. The ease of use was very impressive and the way you configure it is both done via the web console or via a YAML file in the git repository. I’d consider using this as a possible teaching tool for deploying web apps (in the case that Heroku or some other cheaper solution may not work).
§See also
§Thanks
Should out to some fellows who helped accelerate this little project along: