Racket on Digital Ocean App Platform

Published: Sunday, May 15, 2022
Last modified: Wednesday, Jun 8, 2022 (0fbd635)

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 load main.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: