How to deploy a Common Lisp web application to Platform.sh

Goal

Run a simple web application, using Common Lisp, on Platform.sh.

Assumptions

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.

0

Comments

2 comments
Date Votes
  • Due to popular demand Platform.sh now has a common lisp runtime :slight_smile:

    0
  • It was probably less effort than proofreading my magnum opus

    0

Please sign in to leave a comment.

 

Didn't find what you were looking for?

New post