Skip to content

abogoyavlensky/clojure-kamal-example

Repository files navigation

Full-stack Clojure/Script app with deployment using Kamal

This project demonstrates the setup of a Clojure/Script web application that uses PostgreSQL as a database. It also includes configuration for deploying the app with Kamal on a single server.

Key backend libs:

  • Integrant
  • Reitit
  • Malli
  • next.jdbc
  • HoneySQL
  • Automigrate

Key frontend libs:

  • re-frame
  • Reitit
  • Shadow CLJS
  • TailwindCSS

Other tools:

  • Kamal
  • GitHub Actions
  • mise-en-place
  • Taskfile
  • Testcontainers

The app

This setup provides a Clojure/Script web application with an example API route for demonstration purposes with fetching a list of movies and displaying them on the main page.

App main page

Deploy: summary

Pre-requisites

  • Docker installed on local machine
  • Server with public IP
  • Domain pointed out to server
  • SSH connection from local machine to the server with SSH-keys
  • Open 443 and 80 ports on server
  • (optional) Configure firewall

Install Kamal locally

Install mise-en-place (or asdf), and run:

brew install libyaml  # or on Ubuntu: `sudo apt-get install libyaml-dev` 
mise install ruby
gem install kamal -v 1.5.2
kamal version

First deployment

kamal envify --skip-push  # :warning: then fill all variables in the newly created `.env` file
kamal server bootstrap
ssh [email protected] 'docker network create traefik'
ssh [email protected] 'mkdir -p /root/letsencrypt && touch /root/letsencrypt/acme.json && chmod 600 /root/letsencrypt/acme.json'
kamal setup
kamal app exec 'java -jar standalone.jar migrations'

Regular deployment

kamal deploy

or push to the master branch.

Deploy: step-by-step

Assume that you have Kamal installed and other requirements from the "Pre-requisites" section above.


ℹ️ Note: Alternatively you can use dockerized version of Kamal and use the ./kamal.sh predefined command instead of Ruby gem version:

./kamal.sh version

It mostly works for initial server setup, but some management commands don't work properly. For instance, ./kamal.sh app logs -f or ./kamal.sh build push.


Initial server setup

Setup environment variables

Run command envify to create a .env with all required empty variables:

kamal envify --skip-push

The --skip-push parameter prevents the .env file from being pushed to the server.

Now, you can fill all environment variables in the .env file with actual values for deployment on the server. Here’s an example:

# Generated by kamal envify
# DEPLOY
SERVER_IP=192.168.0.1
REGISTRY_USERNAME=your-username
REGISTRY_PASSWORD=secret-registry-password
[email protected]
APP_DOMAIN=app.domain.com

# App
DATABASE_URL="jdbc:postgresql://clojure-kamal-example-db:5432/demo?user=demoadmin&password=secret-db-password"

# DB accessory
POSTGRES_DB=demo
POSTGRES_USER=demoadmin
POSTGRES_PASSWORD=secret-db-password

Notes:

  • SERVER_IP - the IP of the server you want to deploy your app, you should be able to connect to it using ssh-keys.
  • REGISTRY_USERNAME and REGISTRY_PASSWORD - credentials for docker registry, in our case we are using ghcr.io, but it can be any registry.
  • TRAEFIK_ACME_EMAIL - email for register TLS-certificate with Let's Encrypt and Traefik.
  • APP_DOMAIN - domain of your app, should be configured to point to SERVER_IP.
  • clojure-kamal-example-db - this is the name of the database container from accessories section of deploy/config.yml file.
  • We duplicated database credentials to set up database container and use DATABASE_URL in the app.

⚠️ Do not include file .env to git repository!

Bootstrap server and deploy app

Install Docker on a server:

kamal server bootstrap

Create a Docker network for access to the database container from the app by container name and a directory for Let’s Encrypt certificates:

ssh [email protected] 'docker network create traefik'
ssh [email protected] 'mkdir -p /root/letsencrypt && touch /root/letsencrypt/acme.json && chmod 600 /root/letsencrypt/acme.json'

Set up Traefik, the database, environment variables and run app on a server:

kamal setup

The app is deployed on the server, but it is not fully functional yet. You need to run database migrations:

kamal app exec 'java -jar standalone.jar migrations'

Now, the application is fully deployed on the server.

Regular deploy

For subsequent deployments from the local machine, run:

kamal deploy

Or just push to the master branch, there is a GitHub Actions pipeline that does the deployment automatically .github/workflows/deploy.yaml.

Setup CI for deployment

For CI setup you need to add following environment variables as secrets for Actions. In GitHub UI of the repository navigate to Settings -> Secrets and variables -> Actions. Then add variables with the same values you added to local .env file:

APP_DOMAIN
DATABASE_URL
POSTGRES_DB
POSTGRES_PASSWORD
POSTGRES_USER
SERVER_IP
SSH_PRIVATE_KEY
TRAEFIK_ACME_EMAIL

Notes

  • SSH_PRIVATE_KEY - a new SSH private key without password that you created and added public part of it to servers's ~/.ssh/authorized_keys to authorize from CI-worker.

To generate SSH keys, run:

ssh-keygen -t ed25519 -C "[email protected]"

Development

System deps

Install mise-en-place (or asdf), then to install system deps run:

mise install

Frontend

Run frontend in watch mode (js and css):

task ui

Backend

Create file .env.local with local database credentials, for example:

POSTGRES_DB=demo
POSTGRES_USER=demo
POSTGRES_PASSWORD=demo
DATABASE_URL=jdbc:postgresql://localhost:5432/demo?user=demo&password=demo

⚠️ Do not include file .env.local to git repository!

Run database for local development:

task up

Start REPL:

task repl

Run backend in the REPL:

(reset)

Run tests:

task test

Manage database migrations:

task migrations -- list
task migrations -- make
task migrations -- migrate
task migrations -- explain :number 1

Print all available commands:

task -l

task: Available tasks for this project:
* build:                Build uberjar
* check:                Run lint, fmt and tests. Intended to use locally
* css-prod:             Build css in prod mode
* deps:                 Install all dev deps
* fmt:                  Fix code formatting
* fmt-check:            Check code formatting
* lint:                 Linting project's code
* lint-init:            Linting project's classpath
* migrations:           Manage db migrations
* outdated:             Upgrade outdated Clojure deps versions
* outdated-check:       Check outdated deps versions
* repl:                 Run built-in Clojure repl
* test:                 Run tests
* ui:                   Build js and css in watch mode for local development
* up:                   Run docker services for local development

Resources

License

Copyright © 2024 Andrey Bogoyavlenskiy

Distributed under the MIT License.