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 version
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'
kamal deploy
or 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 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
.
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
andREGISTRY_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.yml
file.- We duplicated database credentials to set up database container and use
DATABASE_URL
in the app.
.env
to git repository!
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.
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
.
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
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]"
Install mise-en-place (or asdf), then to install system deps run:
mise install
Run frontend in watch mode (js and css):
task ui
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
.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
Copyright © 2024 Andrey Bogoyavlenskiy
Distributed under the MIT License.