Up(sun) and running with Lisp
Lisp, created by John McCarthy in 1958, is one of the oldest programming languages, renowned for its unique syntax where code and data share the same structure. It’s commonly used in fields like artificial intelligence and functional programming, with its main strengths being its flexibility and suitability for recursive data structures.
In this step-by-step guide, you will learn how to quickly deploy a Lisp web application on both Platform.sh and Upsun. So, without further ado, let’s jump right in:
Write your Lisp program
For this guide we will be using a basic example of a Hunchentoot-based web app
(you can find the corresponding .asd
and .yaml
files further down):
(defpackage #:example
(:use :hunchentoot :cl-who :cl)
(:export main))
(in-package #:example)
(define-easy-handler (greet :uri "/hello") (name)
(with-html-output-to-string (s) (htm (:body (:h1 "hello, " (str name))))))
(defun main ()
(let ((acceptor (make-instance
'easy-acceptor
:port (parse-integer (uiop:getenv "PORT")))))
(start acceptor)
(sleep most-positive-fixnum)))
Notice how the app gets the PORT
from the environment and sleeps at the end as (start acceptor)
yields and the apps run in the foreground as required.
This code is surprisingly legible. For example, use
means “import all the symbols so there’s no need to prefix them with the package name” and cl-who
allows the actual HTML to be created.
Specify the language in your app
Next, make sure that you specify lisp
as your app’s type in your config.yaml:
type: 'lisp:2.1'
applications:
# The app's name, which must be unique within the project.
<APP_NAME>:
type: 'lisp:2.1'
Handle dependencies
The recommended way to handle Lisp dependencies with Upsun and Platform.sh is through using ASDF.
ASDF is a description of dependencies between files of source code in such a way that they can be compiled and loaded in the right order.
Create an .asd
file. When an .asd
file is found in your repository, the system will automatically download the dependencies using QuickLisp.
Below is an example.asd
file:
(defsystem example
:name "example"
:description "Example of a simple web application on Platform.sh"
:author "Lisp Coder <user@example.com>"
:components ((:file "example"))
:build-operation "asdf:program-op"
:build-pathname "example"
:entry-point "example:main"
:depends-on (:hunchentoot :cl-who))
Assumptions
Before we continue, the following assumptions have been made about your application to provide a more streamlined experience. These assumptions are the following:
- Your
.asd
file is named like your system name. For example, our application above is namedexample.asd
and has(defsystem example ...)
in the.asd
file.
Therefore (asdf:make :example)
will run on our system to build a binary.
If you don’t want these assumptions, you can disable this behavior by specifying in your config.yaml
:
build:
flavor: none
applications:
myapp:
type: 'lisp:2.1'
build:
flavor: none
Okay, let’s proceed…
Specify QuickLisp distrubutions
If you wish to change the distributions that QuickLisp is using, you can specify those as follows, specifying a distribution name, its URL and, an optional version:
runtime:
quicklisp:
DISTRIBUTION_NAME:
url: "..."
version: "..."
applications:
# The app's name, which must be unique within the project.
<APP_NAME>:
type: 'lisp:2.1'
runtime:
quicklisp:
DISTRIBUTION_NAME:
url: "..."
version: "..."
For example:
runtime:
quicklisp:
quicklisp:
url: 'http://beta.quicklisp.org/dist/quicklisp.txt'
version: '2025-03-17'
applications:
# The app's name, which must be unique within the project.
myapp:
type: 'lisp:2.1'
runtime:
quicklisp:
quicklisp:
url: 'http://beta.quicklisp.org/dist/quicklisp.txt'
version: '2025-03-17'
Get built-in variables
Upsun and Platform.sh expose relationships and other configurations as environment variables. To get the PORT
environment variable (the port on which your web application is supposed to listen):
(parse-integer (uiop:getenv "PORT"))
Build and run your application
Assuming example.lisp
and example.asd
are present in your repository, the app is automatically built on push.
You can then start it from the web.commands.start
directive.
Note that the start command must run in the foreground. Should the program terminate for any reason it’s automatically restarted. In the example below the app sleeps for a very long time. You could also choose to join the thread of your web server, or use other methods to make sure the program doesn’t terminate.
Create a .yaml
containing something like what is shown below:
name: myapp
type: lisp:2.1
web:
commands:
start: ./example
locations:
/:
allow: false
passthru: true
disk: 512
applications:
myapp:
type: 'lisp:2.1'
web:
commands:
start: ./example
locations:
/:
allow: false
passthru: true
Note that a proxy server is still in front of your app. If desired, certain paths may be served directly by the router without hitting your app (for static files, primarily) or you may route all requests to the Lisp application unconditionally, as in the example above.
Access services
The services configuration is available in the environment variable PLATFORM_RELATIONSHIPS
.
To parse them, add the dependencies to your .asd
file:
:depends-on (:jsown :babel :s-base64)
The following is an example of accessing a PostgreSQL instance:
(defun relationships ()
(jsown:parse
(babel:octets-to-string
(with-input-from-string (in (uiop:getenv "PLATFORM_RELATIONSHIPS"))
(s-base64:decode-base64-bytes in)))))
Given a relationship defined in your .yaml
config file:
relationships:
postgresql:
applications:
myapp:
type: 'lisp:2.1'
relationships:
postgresql:
The following would access that relationship, and provide your Lisp program the credentials to connect to a PostgreSQL instance.
Add this to your .asd
file:
:depends-on (:postmodern)
Now, in your program you can access the PostgreSQL instance as follows:
(defvar *pg-spec* nil)
(defun setup-postgresql ()
(let* ((pg-relationship (first (jsown:val (relationships) "postgresql")))
(database (jsown:val pg-relationship "path"))
(username (jsown:val pg-relationship "username"))
(password (jsown:val pg-relationship "password"))
(host (jsown:val pg-relationship "host")))
(setf *pg-spec*
(list database username password host)))
(postmodern:with-connection *pg-spec*
(unless (member "example_table" (postmodern:list-tables t) :test #'string=)
(postmodern:execute "create table example_table (
a_field TEXT NOT NULL UNIQUE,
another_field TEXT NOT NULL UNIQUE
")))))
Conclusion
In conclusion, deploying a Lisp-based web application on Platform.sh and Upsun is a straightforward process once you understand the key configurations and dependencies involved. From setting up your Lisp program to managing environment variables and service relationships, you can easily get your application up(sun) and running.
composable image
in the future, don’t hesitate to reach out via our Discord for assistance.