Racket on Digital Ocean App Platform
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
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
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_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
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:
- Declare dependencies and other package metadata.
- Script to be ran at the end of the
- To Dockerize the app.
(require your-package)will load
(See Git repository for full code listings.)
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 is the entrypoint. Declaring the correct
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"))
Execute the installed package’s entrypoint. This executes
referenced in the
#!/bin/sh exec racket -l rpaste -- -p 8080
Note the Dockerfile copies in only
info.rkt then installs dependencies.
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
Condensed down for essence. Declares a
fun procedure as an main entry
(module+ main ...) is the program entry point that itself calls
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))
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.
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).
Should out to some fellows who helped accelerate this little project along: