Docker Compose for Clojure Development
Docker Compose provides a declarative configuration for orchestrating services locally, providing a simple way to spin up services to try them out or orchestrate a whole system from numerous dependent services.
Compose can build images for Clojure projects and conditionally run based on the health check of other supporting services, e.g. Postgres Database.
Now that Compose is part of docker, more features such as build on change are being added.
Practicalli Engineering Playbook covers Docker Compose in more detail
Compose is part of Docker
Compose is now integrated into Docker which simplifies the configuration a little, although existing configuration and commands should still work for a time.
compose.yaml
is the new configuration file for Docker Compose, replacing docker-compose.yaml
.
The docker-compose version number is no longer required at the top of the configuration file, as docker is using the built-in version of Compose. Otherwise the syntax is the same, although new features like watch
under x-develp
will be added to the new Compose specification.
docker compose
is the new command to use compose, replacing the docker-compose
command (which should still work for a time).
To start a service using the compose.yaml
configuration
docker compose up
Shutdown the services with
docker compose down
Build Clojure Service
The simplest approach to building a service is to include a build:
configuration specifying the location of a multi-stage Dockerfile
, which has a builder
stage to create an uberjar that is run in the run-time image.
The build:
option for the Clojure service with the path to the multi-stage Dockerfile for the project (typically in the same root directory of the project, although a remote Git repository can also be used)
# --- Docker Compose Configuration --- #
# - Docker Compose V2
# - https://docs.docker.com/compose/compose-file/
#
# Build the Clojure Service from source code
# and run on port 8080
name: "practicalli"
services:
# --- Clojure Service --- #
gameboard-service:
platform: linux/amd64
# Build using Dockerfile - relative path or Git repository
build:
context: ./ # Use Dockerfile in project root
environment:
- COMPOSE_PROJECT_NAME
ports: # host:container
- 8080:8080
name:
(optional) is used to set the container prefix of the service name, via theenvironment:
variableCOMPOSE_PROJECT_NAME
, helping to identify the container by name when running. The above container would bepracticalli-gameboard-service
. This option is more valuable as the number of services growsservices:
contains one or more service definitions with a unique name, e.g.gameboard-service
gameboard-service:
is a unique name for a service configurationplatform:
defines the operating system and hardware architecture that should be used for the servicebuild: context:
defines the location of theDockerfile
to use, either a local file or a Git repository URLports:
defines the host:container mapping for the service port.8080:8000
value would map the service running within the container on8000
to the host (e.g. engineers' computer) on port 8080.
Use a multi-stage
Dockerfile
to provide greater opportunity to create image overlays for the build stage which can be cached, e.g. downloading project dependencies, speeding up the build process on consecutive runs.
Compose services
Add more unique service configurations under services:
, e.g. postgres-database
. All the services that support the local testing and integration of the Clojure project can be added to the compose.yaml
file (database, cache, mock APIs, coupled services, etc.).
The Clojure service defines a dependency on a Postgres Database. The dependency has a condition so the Clojure service is only started once the Postgres service is healthy
services:
clojure-service:
platform: linux/amd64
build: ./
ports: # host:container
- 8080:8080
depends_on:
postgres-database:
condition: service_healthy
postgres-database:
image: postgres:15.2-alpine
environment:
POSTGRES_PASSWORD: "$DOCKER_POSTGRES_ROOT_PASSWORD"
healthcheck:
test: [ "CMD", "pg_isready" ]
timeout: 45s
interval: 10s
retries: 10
ports:
- 5432:5432
depends_on:
include one or more services that the service depends on, optionally adding a startcondition:
depends_on: <service-name> condition:
a condition that should be true in order for the service image to start, typically checkingservice_healthy
of another service in the configurationheathcheck:
define a command and options to determine if the specific service running is healthy, with thetest:
command specific to the type of service.
Build and run the services using the --build
flag with docker compose
docker compose up --build
The Clojure project is built in parallel with running the Postgres database.
On the initial run the Clojure project may take longer to run that starting the Postgres database, in which case the Clojure uberjar will run straight away.
On consecutive runs, at least part of the Clojure build process should be cached and images for all the services will have been downloaded and cached. The Clojure uberjar may wait for the Postgres database to start up.
Each service can define a health check that can be used as a conditional startup trigger and ensure all services start in a meaningful order.
Build on Change
Docker provides watch
as an experimental feature which can rebuild the Clojure service when a file change is detected.
The watch approach seems most useful for Clojure projects when troubleshooting issues that occur during system integration testing.
When additional tools need to run outside of Clojure code, e.g. SaSS build, data loading / ETL, then watch
can compliment the Clojure REPL workflow by providing incremental change management for non-clojur aspects of the project.
Add an x-develop
configuration with watch
under the Clojure service configuration. Define the action for each path to create a simple but comprehensive way to update the service
services:
clojure-service:
platform: linux/amd64
build: ./
ports: # host:container
- 8080:8080
depends_on:
postgres-database:
condition: service_healthy
x-develop:
watch:
- path: ./deps.edn
action: rebuild
- path: ./src
action: rebuild
Start the services and the file watch mode
docker compose up --detach && docker compose alpha watch
Save changes to files and a new image for the Clojure service will be built and deployed when ready.
Having an optimised build process in the
Dockerfile
orcompose.yaml
configuration is especially important to make build on change fast and therefore effective to use.
Summary
With Compose integrated to Docker the configuration is simplified and provides a consistent point to evolve the compose specification.
Composing services provides a simple way of trying out different tools and services in concert with Clojure projects or by themselves.
Practicalli Engineering Playbook covers Docker Compose in more detail
Thank you