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
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.
- 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 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 versionkamal 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'kamal deployor push to the master branch.
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 versionIt 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.
Run command envify to create a .env with all required empty variables:
kamal envify --skip-pushThe --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-passwordNotes:
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_USERNAMEandREGISTRY_PASSWORD- credentials for docker registry, in our case we are usingghcr.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 toSERVER_IP.clojure-kamal-example-db- this is the name of the database container from accessories section ofdeploy/config.ymlfile.- We duplicated database credentials to set up database container and use
DATABASE_URLin the app.
.env to git repository!
Install Docker on a server:
kamal server bootstrapCreate 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 setupThe 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.
For subsequent deployments from the local machine, run:
kamal deployOr just push to the master branch, there is a GitHub Actions pipeline that does
the deployment automatically .github/workflows/deploy.yaml.
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_EMAILSSH_PRIVATE_KEY- a new SSH private key without password that you created and added public part of it to servers's~/.ssh/authorized_keysto authorize from CI-worker.
To generate SSH keys, run:
ssh-keygen -t ed25519 -C "[email protected]"Install mise-en-place (or asdf), then to install system deps run:
mise installRun frontend in watch mode (js and css):
task uiCreate 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.env.local to git repository!
Run database for local development:
task upStart REPL:
task replRun backend in the REPL:
(reset)Run tests:
task testManage database migrations:
task migrations -- list
task migrations -- make
task migrations -- migrate
task migrations -- explain :number 1Print 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
Copyright © 2024 Andrey Bogoyavlenskiy
Distributed under the MIT License.
