15DEPLOYING

DEPLOYING

At some point, you gotta deploy your app. It's the most important step, yet also the one most fraught with options, uncertainty, and complexity.

How you plan to deploy affects the way you write the code. For example, we'll be deploying the tic-tac-toe game we made earlier. As it's written, it is only fit as a text-based app. That's why we'll be deploying it as a CLI application.

On the other hand, if you are writing a web application, that entails a whole swarm of other complications: in addition to the web-specific coding that you'll need to do, there's also the complicated step of choosing how to deploy it. Do you use some niche web hosting service that provides native support for Common Lisp? Do you use a VPS or cloud server? If you do, do you run your application in a container, or do you create an executable? What's your CI look like? And on and on it goes.

In this book, we'll keep things simple and give you one way of doing things as a starting point, but I'm sure you'll have lots of fun learning new and better deployment environments and workflows once you're done here.

15.1DEPLOYING

EXECUTABLE CLI APPS

Common Lisp can make executables. There are several ways to make executables. Which method you use will depend on the nature of your project.

15.1.1DEPLOYING

Tic-tac-toe

Our tic-tac-toe game is the simplest example. We will use SBCL's sb-ext:save-lisp-and-die function.

(sb-ext:save-lisp-and-die #P"path/to/the/binary"                    
                          :toplevel #'ttt:play-game-with-computer" 
                          :executable t)                          

The first argument is the path to the binary you want to create.

The second argument is the package-qualified name of the "main" function you want to call when running the executable.

The third argument tells SBCL to return an executable, not a Lisp image.

In order to call sb-ext:save-lisp-and-die, you of course need to have some code compiled and loaded; however, you can't run it in a Sly REPL. Instead, you need to run it in the command line. That would look like this:

sbcl --load tic-tac-toe.lisp --eval "(sb-ext:save-lisp-and-die #p\"ttt\" :toplevel #'ttt:play-game-with-computer :executable t)"

First, load the lisp file, then evaluate the call to sb-ext:save-lisp-and-die.

You can make the process more accessible to users by creating a Makefile to run the above:

build:
        sbcl --load tic-tac-toe.lisp --eval "(sb-ext:save-lisp-and-die #p\"ttt\"	\
        :toplevel #'ttt:play-game-with-computer :executable t)"

But, if you try to run this, you'll have a problem: the package ttt doesn't exist because we hadn't learned about packages before making the tic-tac-toe game. So let's go add a package definition to the tic-tac-toe.lisp file.

(defpackage #:ttt
  (:use :cl)
  (:export #:play-game-with-computer))
(in-package #:ttt)

With that, you can run make build. Once the executable is created, you can type ./ttt in your command line and enjoy a spirited game of tic-tac-toe against the computer.

15.1.2DEPLOYING

almighty-accounting

For very simple programs, the above is good enough. However, if your project has an .asd file, like the almighty-accounting system from the previous chapter, then you have the option of making an executable from a whole system.

The simple version of this process looks like this:

  • Configure the system to build an executable.
  • Load the system with asdf:load-system.
  • Run asdf:make on the loaded system.

This process uses ASDF, rather than calling sb-ext:save-lisp-and-die directly.

15.1.2.1DEPLOYING

Configure System

First, we need to configure the system. We already did that earlier in our almighty-accounting system definition.

(defsystem "almighty-accounting"
  :author "Micah Killian"
  :version "0.0.1"
  :description "A rudimentary accounting system controlled via CLI."
  :depends-on (#:almighty-money
               #:almighty-kaikei
               #:local-time
               #:mito
               #:sxql
               #:dbi
               #:dbd-sqlite3
               #:clingon)
  :serial t
  :components ((:module "src"
                :serial t
                :components
                ((:file "db")
                 (:file "transactions")
                 (:file "views")
                 (:file "commands")
                 (:file "main"))))
  :build-operation "program-op"
  :build-pathname "almighty-accounting"
  :entry-point "almighty-accounting:main")

The last three lines are the parts we need to build the executable.

We pass "program-op" to :build-operation. ASDF has a number of predefined operations (https://asdf.common-lisp.dev/asdf.html#Predefined-operations-of-ASDF-1) for compiling and loading systems. The default op is load-op. If you don't set it to program-op, you will simply load the system and get dropped into a REPL. program-op tells ASDF to make an executable out of the system.

:build-pathname is the name of the executable file we want to create.

:entry-point is the package-qualified name of the function we want to call when running the executable.

15.1.2.2DEPLOYING

Defining the Entry Point

In the previous chapter, we didn't define the main.lisp file, so let's do that now.

Open a buffer to almighty-accounting/src/main.lisp and add the following:

(defpackage #:almighty-accounting
  (:use #:cl)
  (:nicknames #:almighty-accounting/main)
  (:local-nicknames (#:db #:almighty-accounting/db)
                    (#:cli #:clingon)
                    (#:commands #:almighty-accounting/commands))
  (:export
   #:main))

(in-package #:almighty-accounting)

(defun main ()
  (let ((app (commands:top-level/command)))
    (db:with-chart-of-accounts #p"data/data.db"
      (cli:run app))))

That's all we'll need.

15.1.2.3DEPLOYING

Load & Make System

Now we just need to load and make the system. We're going to do things a little differently this time, though. What we'll do is make a build.lisp file and simply load that file. This will give us more flexibility for orchestrating the next steps. To load build.lisp, we'll use a Makefile:

almighty-accounting: build.lisp almighty-accounting.asd src/*.lisp
        sbcl --load build.lisp 

The filenames after almighty-accounting tell make to run the code that follows if any of the files listed are updated.

Now for the build.lisp file:

(require :asdf)

;;; Force ASDF to only look here for systems.
(asdf:initialize-source-registry `(:source-registry (:tree ,(uiop:getcwd))
 :ignore-inherited-configuration))

(progn
  (format t "-- LOADING SYSTEM --~%")
  (asdf:load-system :almighty-accounting)
  (format t "-- SYSTEM LOADED --~%"))

(progn
  (asdf:make "almighty-accounting")
  (format t "-- DONE --~%")
  (quit))

We require ASDF in order to use it here. require loads a package if it isn't already loaded. Usually ASDF is built into modern implementations of Common Lisp, but this ensures that ASDF is loaded in cases where your implementation doesn't load it (or have it built in).

The call to asdf:initialize-source-registry is to ensure that when ASDF loads the system that it resolves dependences using only the systems within our project directory. This is important because we used vend for our dependencies. We want asdf to use only the versions of those dependencies, not go hunting down dependencies in other directories (which it will).

asdf:load-system is the same operation performed with the REPL shortcut , load-system.

asdf:make runs the :build-operation we configured earlier.

We run quit because if for some reason you are running make and there are no changes, then no executable will be made and you'll be thrown into the REPL. quit is there to conveniently kick us out immediately.

15.1.2.4DEPLOYING

It's Time To Build

Now, all you need to do is run make in the almighty-accounting project root directory to create the binary.

DEPLOY! (that means run make)

15.1.2.5DEPLOYING

Run the Executable

Once the build runs successfully, you should have a new file called almighty-accounting in your almighty-accounting project root directory. You can give it a spin…

But huh…no…no no no no…

We have a bug!

15.1.2.6DEPLOYING

Fixing bug

When you tried using the application to make a cash-sale, etc. you probably have something like this happen:

Returns
micah@almighty almighty-accounting % ./almighty-accounting cash-sale
huh
Name of good: Unknown good: huh. Must be one of:
beef
chicken
rice
cheese
bread
beef
Name of good:
beef
Quantity: Invalid quantity: beef. Quantity must be a positive integer.
2
More? (y or n) y
Quantity:
rice
Name of good:
n
Quantity: Invalid quantity: n. Quantity must be a positive integer.
2

More? (y or n) n
Quantity: Transaction completed. Total: $140.00

Right from the beginning, you'll notice that my "huh" comes before "Name of good:". That's because when I first started the application, "Name of good:" didn't print to the terminal. The rest of the output is similarly odd.

But it worked fine in the REPL! What's going on?

The problem is read-line: format t writes to *standard-output*, but Lisp streams are "batched" or buffered and nothing forces that buffer out to the terminal before read-line blocks waiting for input. We didn't have any trouble testing in the REPL because it does the flushing of the buffer for us, but when we use the application in the terminal we need to force it to output. Conveniently, there is a function called force-output that does just that.

Under every format call before a read-line, add force-output.

(defun ask-good-quantity ()
  (format t "~&Quantity: ")
  (force-output)                        ; <- Add this
  (let* ((user-input (read-line))
         ...)
    ...))
(defun ask-good-name ()
  (format t "~&Name of good: ")
  (force-output)                        ; <- Add this
  (let* ((user-input (read-line))
         ...)
    ...))
(defun ask-number-of-nights ()
  (format t "~&Number of nights: ")
  (force-output)                        ; <- Add this
  (let* ((user-input (read-line))
         ...)
    ...))

Make sure all files in your project are saved and then rerun make again.

15.1.2.7DEPLOYING

Run the Executable (successfully this time)

Now that the executable is updated, just run the application. Try this:

./almighty-accounting cash-sale

If it worked, congratulations, you have now deployed your accounting application. You are officially ready to ship real CLI applications!

15.2DEPLOYING

EXECUTABLE WEB APP

Web application deploys are significantly more complicated that CLI ones, so I'll guide you through doing such a deploy now.

We are going to deploy a simple hello world web app to a Ubuntu 24.04 LTS box. We'll make it using the Fuka Stack: clack, lack, woo, and myway. We'll use vend for our dependencies. After we setup a small hello world example of using the Fuka Stack, we'll setup the server, upload the code, build an executable, and then run that executable as a service via systemd.

If you use Ubuntu 24.04 LTS on an x86-64 processor–the same OS and hardware as the server–you might even be able to skip the step of uploading the code to the server, building the executable on your machine and then uploading the executable to the server.

The purpose is not to get into the weeds of how to make a web framework. When we get to the part about the code, we'll focus on the parts relevant to deploying, not to setting up the framework.

15.2.1DEPLOYING

Requirements

  • An SSH Key.
  • A server with Ubuntu 24.04 LTS.
  • vend installed on your computer.
15.2.2DEPLOYING

Server Setup

First, let's start with setting up the server. I used a Linode VPS for this example, but you can use any Ubuntu 24.04 LTS machine.

15.2.2.1DEPLOYING

Add non-root user

First, SSH into your server. Once you're in, run this command, replacing yourusername with one of your choosing.

adduser yourusername

Then, give that username sudo permissions.

usermod -aG sudo yourusername

Then you need to set up SSH for the new username. Exit the SSH session. On your local machine, run this command in the terminal:

ssh-copy-id yourusername@your-server-ip
15.2.2.2DEPLOYING

Install Roswell dependencies, libev4, Caddy

In the terminal, run this command:

sudo apt install git build-essential automake autoconf libcurl4-openssl-dev zlib1g-dev libev4 -y
15.2.2.3DEPLOYING

Install Roswell

We use Roswell so that we can get the latest version of SBCL or use some other implementation later. While Common Lisp isn't getting updates, implementations update their compilers, adding optimizations, etc.

In the terminal, run these commands:

git clone -b release https://github.com/roswell/roswell.git
cd roswell
sh bootstrap
./configure
make
sudo make install
cd ..
rm -rf roswell 
15.2.2.4DEPLOYING

Install Caddy

Caddy is a reverse proxy that will let us run our Woo server on port 5000 but be accessible in the browser from https://the-ip-address-or-domain-name rather than https://the-ip-address-or-domain-name:5000. I like it because it takes care of SSL certificates automatically, so when you use it, https:// just works.

Run the following commands:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Open up /etc/caddy/Caddyfile with your favorite editor (such as nano). There should already be some default stuff there. Create a block like this:

your-server-ip-address-or-domain-name {
    reverse_proxy localhost:5000
}

You need to enable and start the service after your changes.

sudo systemctl enable caddy
sudo systemctl start caddy
15.2.2.5DEPLOYING

(Optional) Change Hostname

The server terminal should show something like yourusername@localhost. You can change localhost to something of your choosing.

Run this command:

sudo hostnamectl set-hostname almightylisp

Then use our favorite editor to sudo open /etc/hosts. Add 127.0.0.1 yourpreferredhostname, save, and close. Then run exec $SHELL or just restart the SSH session.

With that, you are ready to move on to the code.

15.2.3DEPLOYING

The Code

Now that we have the server setup, we'll get our code written.

First, on your local machine, make a new project root directory called fuka-stack. Inside it, you can also make another directory called src.

15.2.3.1DEPLOYING

Build Script

The script for building the executable is going to look similar to our previous build.lisp file. However, this one is a .ros file, used by Roswell to build the executable.

Open a buffer in fuka-stack/fuka-stack-app.ros, add the following code, and then save:

Buffer

../code/deploying/fuka-stack/fuka-stack-app.ros

Package

fuka-stack-script

#!/bin/sh
#|-*- mode:lisp -*-|#
exec ros -Q -- $0 "$@"
|#

;;; Roswell script header (above) makes this executable

(defpackage #:fuka-stack-script
  (:use #:cl)
  (:export #:main))
(in-package #:fuka-stack-script)

;;; Load your system
(asdf:initialize-source-registry
 `(:source-registry
   (:tree ,(uiop:getcwd))
   :ignore-inherited-configuration))

(asdf:load-system "fuka-stack")

;;; Environment variable utilities.
(defun get-env-int (env default)
  (let ((env (uiop:getenv env)))
    (if env
        (parse-integer env :junk-allowed t)
        default)))

(defun get-env (env default)
  (or (uiop:getenv env) default))

(defun envp (env)
  (when (string-equal (uiop:getenvp env) "true")
        t))

(defun main ()
  (let ((host (get-env "HOST" "127.0.0.1"))
        (port (get-env-int "PORT" 5000))
        (debugp (envp "DEBUGP")))
    (handler-case
        (progn
          (clack:clackup (lack:builder fuka-stack:*component*) :server :woo
                                                               :address host
                                                               :port port
                                                               :debug debugp)
          (sleep most-positive-fixnum))
      (error (c)
        (format *error-output* "Aborting. ~a ~&" c)
        (force-output *error-output*)
        (uiop:quit 1)))))

The .ros file needs to have its own main function. For the purposes of deploying a web app, all you need to understand is this main function. That function uses uiop:getenv via get-env and get-env-int helpers to–you guessed it–get the environment variables HOST and PORT, defaulting to 127.0.0.1 and 5000. If the DEBUGP environment variable is set to true, then the clack:clackup :debug setting will be set to t, otherwise nil. Later we'll change the variables on the production environment, but this allows you to try this code out on your computer.

sleep is a function we haven't seen before. Usually, after loading the system and calling clack:clackup to start the server, it will remain on (usually you save it to a parameter so you can turn it off with clack:stop).

When you run the executable built with this code, if you don't include the call to (sleep most-positive-fixnum), systemd will assume your executable is done working and stop the service, turning off your server.

15.2.3.2DEPLOYING

Service & Env files

We will need to run our executable as a service. If we don't, the server will block our terminal session, and if there is an error, it will simply crash the server. Systemd can be configured to restart any crashed services, and the service won't block our terminal session, either.

Open a buffer in fuka-stack/fuka-stack-app.service, add the following code, and then save.

[Unit]
Description=Fuka Base App
After=network.target

[Service]
Type=simple
# Replace with the username you create when setting up the server
User=micah
# ...same here
WorkingDirectory=~/.local/bin/
EnvironmentFile=/etc/systemd/system/fuka-stack-app.env

# ...and here
ExecStart=/home/micah/.local/bin/fuka-stack

TimeoutStartSec=120
TimeoutStopSec=10
KillMode=process
KillSignal=SIGTERM

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

We also need environment variables set up, which we can do in a separate file (notice EnvironmentFile above).

Open a buffer in fuka-stack/fuka-stack-app.env, add the following code, and then save.

HOST=0.0.0.0
PORT=5000
DEBUGP=false
15.2.3.3DEPLOYING

Makefile

The makefile includes both make and make install commands. Open a buffer in fuka-stack/makefile and save this code inside:

fuka-stack: *.ros *.asd *.lisp src/*.lisp
        ros build fuka-stack-app.ros
install:
        mv fuka-stack-app ~/.local/bin/fuka-stack-app
        sudo cp fuka-stack-app.service /etc/systemd/system/fuka-stack-app.service
        sudo cp fuka-stack-app.env /etc/systemd/system/fuka-stack-app.env
        sudo systemctl daemon-reload
        sudo systemctl enable fuka-stack-app.service
run:
        sudo systemctl start fuka-stack-app.service

make will run Roswell's version of SBCL to build the executable. If you want to use/test this script locally you need to install Roswell.

make install will move the fuka-stack-app executable and setup the systemd service and environment variables. make run will turn the service on.

15.2.3.4DEPLOYING

Hello World Code

Now that we have our server set up, and we have all we need to build an executable and start it as a service, we can make our hello-world web app.

In the project root directory, make a file called fuka-stack.asd and place the below code into it.

(defsystem "fuka-stack"
  :author "Micah Killian <[email protected]>"
  :maintainer "Micah Killian <[email protected]>"
  :description "Fuka Stack hello world deploy example"
  :license "MIT"
  :version "0.1"
  :depends-on (:clack
               :woo 
               :myway
               :lack
               :lack-component
               :lack-request
               :lack-response)
  :components ((:module "src"
                :components
                ((:file "main")))))

Notice that we don't have any of the build options like we had for the previous executable CLI app. That's because we'll be using Roswell to build the executable.

Save this file and then be sure to run vend get to download the dependencies into your project.

Finally, we have the main body of the app. I won't be explaining the mechanics of the stack–that is the subject of a future book.

Open a buffer in fuka-stack/src/main.lisp, add this code, and save it.

(defpackage #:fuka-stack
  (:use #:cl)
  (:nicknames #:fuka-stack/main))
(in-package #:fuka-stack/main)

;;; Lack setup
(defclass fuka-component (lack/component::lack-component)
  ((routes :initarg :routes :accessor component-routes :initform (myway:make-mapper))))

(defparameter *component*
  (make-instance 'fuka-component))

(defparameter *request* nil)
(defparameter *response* nil)

(defmethod lack/component:call ((component fuka-component) env)
  (let ((*request* (lack/request:make-request env)))
    (multiple-value-bind (response foundp)
        (myway:dispatch (slot-value component 'routes)
                        (lack/request:request-path-info *request*)
                        :method (lack/request:request-method *request*))
      (if foundp
          (if (functionp response)
              response
              (destructuring-bind (status headers body) response
                (lack/response:finalize-response (lack/response:make-response status headers body))))
          (lack/response:finalize-response (lack/response:make-response 404 '(:content-type "text/html") '("Not found")))))))

;;; Clack response utility
(defun http-response (body &key (code 200) (headers nil))
  (let ((headers (append `(:content-type "text/html; charset=utf-8"
                           :content-length ,(length body))
                         headers)))
    `(,code ,headers (,body))))


;;; Myway routing utilities
(defun make-endpoint (fn)
  (lambda (params)
    (funcall (symbol-function fn) params)))

(defun route (method routing-rule endpoint &optional (mapper (component-routes *component*)))
  (myway:connect mapper
                 routing-rule
                 (make-endpoint endpoint)
                 :method method))


;;; Index controller and route
(defun index (params)
  (declare (ignore params))
  (http-response (format nil
                         "Hello, world! This is the path: ~a. This is the HTTP request method: ~a."
                         (lack/request:request-path-info *request*)
                         (lack/request:request-method *request*))))

(route :GET "/" 'index)
15.2.4DEPLOYING

The Deploy

You are on the precipice of a new beginning, my friend. Your Common Lisp web development life arc begins with the next two steps. It's time to upload your code to the server. If you are comfortable using git, you could use it now. For the sake of simplicity, we'll just upload with the rsync command.

From your local machine, run this command:

rsync -avz --progress ~/path/to/your/local/version/of/fuka-stack yourusername@domain-name-or-ip-address-to-your-server:/home/yourusername/fuka-stack

Now all you need to do is ssh back into your server, navigate to the fuka-stack directory, and run the make, make install, and make run commands in order. If all goes well (and we know things always go well with deploys), you should now be able to access your hello-world example in your browser with https://your-domain-name-or-server-ip-address.

Congratulations, you have successfully deployed a Common Lisp web app!

15.2.5DEPLOYING

Summary

There are a number of things we could have done differently. We could have skipped Roswell and just used whatever version of SBCL was available on the Ubuntu 24.04 LTS package repository. We could have compiled the latest one from source. We could have built the executable in about three other ways. We could have used nginx instead of Caddy. We could have used hunchentoot instead of clack and friends. We could have used Docker to set up the entire environment on the server and simply run the code from that environment.

The principles to keep in mind when deploying a Common Lisp web app are these:

  1. You need to have some entry point–the call to start the server.
  2. You need to run that call someway as a service to enable automatic restarts.
  3. You will need to call sleep in the entry point to keep the server thread open in the background.
  4. You want to turn off the debugger in prod.