How to deploy a Common Lisp web application to Platform.sh
Goal
Run a simple web application, using Common Lisp, on Platform.sh.
Assumptions
- Platform.sh CLI installed locally
- An SSH key configured on a Platform.sh account
- A basic understanding of Common Lisp
- sbcl installed
- quicklisp installed, to manage packages
- roswell installed, to launch and package the app
Verify: Installation assumptions have been met when the following commands run without error:
- Platform.sh CLI:
platform project:list- sbcl:
sbcl- quicklisp:
sbcl --load "$HOME/quicklisp/setup.lisp"- roswell:
ros help
Problems
Platform.sh doesn’t (yet) offer a dedicated Common Lisp runtime. However, lisp is small and self-hosting, so another type can be “borrowed” to install a Lisp compiler at build time.
Steps
Note: Code for this How-to can be found on Github.
1. Create a project directory
Defining a lisp system using asdf will make it easier to install. This can be done in the quicklisp local-projects directory in a project directory platform-example-lisp.
Create ~/quicklisp/local-projects/platform-example-lisp/platform-example-lisp.asd to define the project, its metadata, and its dependencies so it can be used by quicklisp and roswell:
(defsystem "platform-example-lisp"
:description "example web application"
:author "cms <cms@beatworm.co.uk>"
:depends-on ( "hunchentoot" "clack")
:components ((:file "example-web")))
2. Write the application code
The :components keyword above is a list of the project files. There will only be one file for this project, so create the ~/quicklisp/local-projects/platform-example-lisp/example-web.lisp component
(defpackage :platform-example-lisp
(:use #:cl))
(in-package :platform-example-lisp)
(defun hello-world (env)
"return a friendly HTTP response with a document"
(declare (ignore env))
'(200 (:content-type "text/html") ("<h1>It's alive!</h1>Hello, world!")))
(defun env-lookup-port ()
"get the environment variable PORT as an integer. default to 5000
when unset"
(let ((env-var (uiop:getenv "PORT")))
(cond (env-var (parse-integer env-var))
(t 5000))))
(defun web ()
"server a web handler forever returning our hello response on root"
;; run the web page
(clack:clackup #'hello-world :port (env-lookup-port))
;; run the web server thread as the main thread
(sb-thread:join-thread
(find-if
(lambda (th)
(search "clack-handler-hunchentoot" (sb-thread:thread-name th)))
(sb-thread:list-all-threads))))
3. Run a repl
In the terminal run
$ sbcl
then enter the following form at the * prompt:
* (ql:quickload "platform-example-lisp")
quicklisp will back-resolve all of the dependencies, download and install them. Verify that the platform-example-lisp package has loaded; the following should be in the output of the above command:
To load "platform-example-lisp":
Load 1 ASDF system:
platform-example-lisp
; Loading "platform-example-lisp"
......
("platform-example-lisp")
*
Test run the website by calling the web function directly as well:
* (platform-example-lisp::web)
Hunchentoot server is started.
Listening on localhost:5000.
which can be seen in the browser at “http://localhost:5000”.
Exit out of sbcl with Ctrl-C to launch the debugger, then type (quit) to fully exit the prompt.
4. Bundle the app using roswell
Move to the app directory.
$ cd ~/quicklisp/local-projects/platform-example-lisp/
Generate a skeleton roswell script using the ros tool
$ ros init example-web
Successfully generated example-web.ros
Edit example-web.ros to add package load instructions in place of the dummy quicklisp operation, and invoke the application entry point inside the provided main form, which is the function the script will run when executed:
#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
(ros:ensure-asdf)
#+quicklisp(ql:quickload "platform-example-lisp" :silent t))
(defun main (&rest argv)
(declare (ignorable argv))
(platform-example-lisp::web))
Tell roswell to set up asdf to also look in the current working directory:
$ ros -S "$PWD" example-web.ros
Hunchentoot server is started
Listening on localhost:5000
This enables the full bootstrap to happen in the .ros script with just (ql:quickload "platform-example-lisp"). No repl needed; exit with Ctrl-C.
Compile this script into an executable.
$ ros -S "$PWD" dump executable example-web.ros -o app
$ file app
app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8ada62dd26f7ef403aa6aead06d8294fe978eea1, with debug_info, not stripped
$ ./app
Hunchentoot server is started
Listening on localhost:5000
5. Add Platform.sh application configuration and deploy hooks
Create a setup-build-env.sh in the project directory that includes:
#!/usr/bin/env bash
set -e
cd "$PLATFORM_CACHE_DIR"
function build {
sh bootstrap
./configure --prefix ~/.global
make
}
# Check out roswell from upstream if it isn't there already. If it is we update it to the latest release
if [ ! -d roswell ]; then
git clone --branch release https://github.com/roswell/roswell.git
cd roswell
build
else
cd roswell
old_head=$(git rev-parse HEAD)
git pull origin release
new_head=$(git rev-parse HEAD)
if [ ! "$new_head" = "$old_head" ]; then
git clean -xdf
build
fi
fi
make install
ros setup
Tell the Platform.sh deploy chain to use it by adding it to .platform.app.yaml configuration:
name: example-lisp
# Could be any type
type: python:3.6
build:
flavor: none
hooks:
build: |
set -e
./setup-build-env.sh
ros --source-registry "$PWD" dump executable example-web.ros -o app
disk: 256
web:
commands:
start: ./app
6. Configure routes and services
Create a simple .platform/routes.yaml file to define the routes:
"https://{default}/":
type: upstream
upstream: example-lisp:http
"https://www.{default}/":
type: redirect
to: "https://{default}/"
Create an empty .platform/services.yaml, since no services will be used for the application:
7. Deploy to Platform.sh
Connect the project to Platform.sh by:
- Initializing a git repo for the local project.
- Add and commit all changes.
- Add the initialized Platform.sh project as remote with
platform project:set-remote <project id> - Push (
git push platform master)
Conclusion
Platform.sh is one of the easiest ways to deploy a web application directly from a git tree, and common-lisp and clack are one of the easiest ways to build elegant web applications using one of the oldest and most interesting programming languages around.
Of course, this sample app doesn’t do anything much of interest, but the skeleton here could be extended considerably, without any need to modify the deployment template.
Platform.sh doesn’t yet offer common lisp as a supported runtime, but by leveraging the build hook and environment variable configuration for PORT and the powerful abstractions of roswell, clack , quicklisp, and asdf it can be rolled out quite simply.
Comments
Due to popular demand Platform.sh now has a common lisp runtime
It was probably less effort than proofreading my magnum opus
Please sign in to leave a comment.