Skip to content

Docker & Static Seed Deployment

Docker Compose Cluster

StaticDiscovery seed list · direct member connections

node-1
node-1:7654
VOLUME/wal
node-2
node-2:7655
VOLUME/wal
node-3
node-3:7656
VOLUME/wal

This guide covers the supported ways to run LoomCache and the production boundaries that apply to each. LoomCache ships three deployment shapes:

  1. Embedded in an existing JVM (single-node, in-process) via EmbeddedLoomCache.builder().
  2. Spring Boot embedded — loom-spring-boot auto-configuration wired into an application.
  3. Standalone process — Dockerfile, docker-compose.yml.

There is no orchestrator manifest set in the repository, and there is no load balancer between clients and members. Smart-client routing reads the partition table on connect and follows owner/leader redirects, so a TCP load balancer in front of the cluster would only break that fast path. Members find one another through static seed-list discovery; any provisioning tool you already use (Helm/Kustomize-as-generator, Ansible, plain shell, an internal CMDB) can render the seed list.

The production profile is intentionally conservative. These support boundaries apply to the release artifact:

  • Multi-group sharding is unsupported and must fail closed in production until every Raft group has independent WAL, Raft metadata, snapshot, install-snapshot, and restart recovery evidence. Use the default single replicated Raft group for production-readiness validation.
  • Production CP support is limited to atomic long, lock, and Java-client-managed semaphore calls. Java client lock/semaphore calls create and heartbeat the Raft-managed CP session internally; sessionless semaphore mutations fail closed. Latch and atomic-reference handles are not production-supported; production profiles reject their direct requests.
  • Registry-backed or local JCache is not durable production storage. JCache is production-durable only when operations are routed through the cluster client and Raft-backed map path with the same TLS/auth and persistence gates as normal LoomMap traffic.
  • Packaged generic MapStore declarations are disabled/fail-closed in production profiles. The Spring default-map JPA write-through bridge is not installed in production; unsafe/local optional datasources fail preflight when that bean is present.
  • QueueStore snapshot/restart parity is not production-supported and must fail closed until restart, rollback, and duplicate/lost item failure windows are release-validated.
  • CREATE INDEX and declarative SQL indexes are unsupported/rejected in this release and are not Hazelcast index parity claims.
  • LoomMap<K,V> does not imply Hazelcast-style implicit object support. Registered POJO map keys/values require identical Kryo registrations on clients and members; unregistered POJOs fail closed, and server-side SQL/index paths do not introspect object envelopes.
  • Server-side LRU, LFU, finite max-entry/max-memory eviction, and max-idle semantics are not production-supported until eviction decisions are Raft-applied and proven through WAL/snapshot/restart tests.

EmbeddedLoomCache runs a full LoomCache node in the calling JVM without any Spring Boot dependency. This shape is suitable for library embeddings, integration tests, and single-node development environments.

Build and start a standalone node through the builder:

System.setProperty("loomcache.profile", "development");
EmbeddedLoomCache cache = EmbeddedLoomCache.builder()
.nodeId("node-1")
.host("127.0.0.1")
.port(5701) // default; omit to use 5701
.standalone() // single-voter bootstrap, no external seeds
.enablePersistence(true)
.dataDir(Path.of("/var/lib/loomcache"))
.build();
cache.start();
// Direct access (standalone mode only — bypasses auth and Raft consensus):
var map = cache.getMap("my-map");
cache.shutdown();

The shutdown sequence drains in-flight requests before closing the TCP listener. Operators must account for the full drain budget when configuring orchestrator stop timeouts:

PhaseBudget
TCP listener drain (loomcache.network.shutdown-drain-ms, LOOMCACHE_NETWORK_SHUTDOWN_DRAIN_MS)up to 60000 ms (Docker default)
Executor command drain + executor terminationup to 60000 ms (loom.executor.stopDrainTimeoutMs defaults to 30000 ms and can be waited twice)
Spring Boot lifecyclea few seconds

The Docker/Spring Boot sample worst-case path is approximately 125 seconds before any operator-specific snapshot, persistence, or external drain margin. Set your orchestrator’s terminationGracePeriodSeconds or Docker stop_grace_period to at least 150s to avoid SIGKILL cutting off an in-progress Raft commit or WAL fsync.

The shipped Dockerfile builds the Spring Boot runnable jar on digest-pinned maven:3.9.16-eclipse-temurin-25@sha256:01ef98a139ed64622c086bac54d1e167453d0f2ff68b69d00978f26d8736215c and eclipse-temurin:25.0.3_9-jre-noble@sha256:f9bd8815e73632c22985ebb133ec49b9fc4ad5ffe0657594ac02748ad0431ab7 images with --enable-preview.

The command below is a per-member template for loomcache-1; run one container for each static Raft voter with matching LOOMCACHE_NODE_ID, LOOMCACHE_NODE_HOST, volume, and host port mappings. The fixed -p 7654:7654 -p 8080:8080 mapping assumes one LoomCache container per host; use distinct host ports if multiple members share one host. A single production container with a three-voter bootstrap list is not a valid production cluster.

Terminal window
TLS_DIR="${LOOMCACHE_TLS_DIR:-$HOME/.config/loomcache/tls}"
mkdir -p "$TLS_DIR"
IMAGE=ghcr.io/loom-cache/loom-cache@sha256:<release-image-digest>
docker run -d \
--name loomcache-1 \
--hostname loomcache-1 \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
--pids-limit 512 \
--stop-timeout 150 \
-p 7654:7654 -p 8080:8080 \
--env-file "$TLS_DIR/loomcache.env" \
-v "$TLS_DIR:/etc/loomcache/tls:ro" \
-e LOOMCACHE_PROFILE=production \
-e LOOMCACHE_CLUSTER_CLUSTER_ID=loomcache-prod-cluster \
-e LOOMCACHE_CLUSTER_SEEDS=loomcache-1:7654,loomcache-2:7654,loomcache-3:7654 \
-e LOOMCACHE_SERVER_RAFT_BOOTSTRAP_SERVERS=loomcache-1=loomcache-1:7654,loomcache-2=loomcache-2:7654,loomcache-3=loomcache-3:7654 \
-e LOOMCACHE_NODE_ID=loomcache-1 \
-e LOOMCACHE_NODE_HOST=loomcache-1 \
-e LOOMCACHE_NODE_PORT=7654 \
-e LOOMCACHE_SERVER_ENABLED=true \
-e LOOMCACHE_SERVER_PORT=7654 \
-e LOOMCACHE_SERVER_BIND_ADDRESS=0.0.0.0 \
-e SERVER_PORT=8080 \
-e LOOMCACHE_NETWORK_SHUTDOWN_DRAIN_MS=60000 \
-e LOOMCACHE_SERVER_PERSISTENCE_ENABLED=true \
-e LOOMCACHE_SERVER_PERSISTENCE_WAL_DIRECTORY=/var/lib/loomcache \
-e LOOMCACHE_SERVER_EVICTION_POLICY=NONE \
-e LOOMCACHE_SERVER_EVICTION_MAX_ENTRIES=0 \
-e LOOMCACHE_SERVER_EVICTION_MAX_MEMORY_BYTES=9223372036854775807 \
-e LOOMCACHE_TLS_ENABLED=true \
-e LOOMCACHE_TLS_REQUIRE_CLIENT_AUTH=true \
-e LOOMCACHE_TLS_KEY_STORE_PATH=/etc/loomcache/tls/keystore.p12 \
-e LOOMCACHE_TLS_TRUST_STORE_PATH=/etc/loomcache/tls/truststore.p12 \
-e LOOMCACHE_TLS_REVOCATION_CHECKING_ENABLED=true \
-e LOOMCACHE_TLS_REVOCATION_SOFT_FAIL=false \
-e LOOMCACHE_AUTH_ENABLED=true \
-e LOOMCACHE_AUTH_GATEWAY_TRUST=false \
-e LOOMCACHE_AUTH_CERT_PERMISSIONS_EU_WEST_PROD_ADMIN=ADMIN \
-e LOOMCACHE_AUTH_CERT_PERMISSIONS_EU_WEST_PROD_CLIENT=READ_WRITE \
-e LOOMCACHE_AUTH_CERT_PERMISSIONS_EU_WEST_PROD_HEALTHCHECK=READ_ONLY \
-e SERVER_SSL_ENABLED=true \
-e SERVER_SSL_KEY_STORE=/etc/loomcache/tls/keystore.p12 \
-e SERVER_SSL_TRUST_STORE=/etc/loomcache/tls/truststore.p12 \
-e SERVER_SSL_CLIENT_AUTH=need \
-e SPRING_SQL_INIT_MODE=never \
-e SPRING_AUTOCONFIGURE_EXCLUDE=org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration,org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration,org.springframework.boot.data.jpa.autoconfigure.DataJpaRepositoriesAutoConfiguration,org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration \
-e JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+FlightRecorder -Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true" \
-e LOOMCACHE_HEALTHCHECK_HOST=loomcache-1 \
-e LOOMCACHE_HEALTHCHECK_CERT=/etc/loomcache/tls/healthcheck.p12 \
-v loomcache-data:/var/lib/loomcache \
"$IMAGE"

Packaged generic MapStore/JPA configuration surfaces remain non-production validation only in this release. Production profiles keep generic JDBC MapStore and the Spring default-map JPA write-through path disabled/fail-closed, so keep SPRING_SQL_INIT_MODE=never and the datasource/JPA SPRING_AUTOCONFIGURE_EXCLUDE in production unless an application needs those optional dependencies for unrelated beans.

Run every static Raft bootstrap voter with the same seed and bootstrap lists; production rejects standalone or self-only bootstrap.

The image runs as an unprivileged loomcache user with a read-only root filesystem, no Linux capabilities, no new-privileges, a bounded /tmp tmpfs, and a PID limit. Only the mounted /var/lib/loomcache data volume and /tmp scratch tmpfs are writable; if you redirect runtime scratch or persistence paths, mount those targets as writable volumes or tmpfs entries explicitly. The Docker HEALTHCHECK probes https://${LOOMCACHE_HEALTHCHECK_HOST}:8080/actuator/health/readiness (for the example above, https://loomcache-1:8080/actuator/health/readiness) with the mounted CA and a dedicated PKCS12 healthcheck client certificate.

Client certificates must use CNs that match the configured LOOMCACHE_AUTH_CERT_PERMISSIONS_* entries. The Docker entrypoint lowercases the env suffix and converts _ to -, so LOOMCACHE_AUTH_CERT_PERMISSIONS_EU_WEST_PROD_ADMIN=ADMIN binds as loomcache.auth.cert-permissions.eu-west-prod-admin=ADMIN and authorizes client certificate CN eu-west-prod-admin. The certificate SAN must include the LOOMCACHE_HEALTHCHECK_HOST DNS name because the healthcheck validates that hostname. Use a separate healthcheck client certificate, such as CN eu-west-prod-healthcheck, instead of reusing the server keystore as a client identity. Production deployments must use static Raft bootstrap servers; single-voter standalone production mode is rejected at startup.

GraalVM native-image support is not part of the release artifact. See Native Image Feasibility for the current AOT probe and remaining support gates.

The checked-in docker-compose.yml is a local development profile, not a production deployment. It sets LOOMCACHE_PROFILE=development, publishes host ports dynamically on loopback, and enables bounded server-side eviction so developers can run a small local cluster. It does not configure Spring datasource/JPA or schema-DDL environment variables. It also overrides static Raft bootstrap voters to match the compose node IDs (node-1, node-2, node-3).

The bare-jar local launcher, scripts/start-cluster.sh, loads the node1, node2, and node3 Spring profiles from config/local-cluster; those profile files are local-development only and are not packaged into the Spring Boot application resources.

For production with the Spring Boot image, set the Spring Boot loomcache.server.*, loomcache.cluster.*, loomcache.node.*, TLS/auth, and Actuator properties through environment variables as shown above. config/loomcache-docker.yml is a direct ConfigLoader template for non-Boot direct-node tooling; do not bind-mount it as Spring Boot application.yml for the image unless you first translate its keys to LoomProperties.

Terminal window
docker compose up -d
docker compose ps
docker compose logs -f loomcache-node1
docker compose port loomcache-node1 7654
docker compose port loomcache-node1 8080

The compose services do not set container_name, so docker compose -p <project> can run isolated clusters side by side. Host ports are assigned dynamically on loopback; discover them with docker compose port.

ServiceCluster → hostREST/Actuator/Prometheus → host
loomcache-node1docker compose port loomcache-node1 7654docker compose port loomcache-node1 8080
loomcache-node2docker compose port loomcache-node2 7654docker compose port loomcache-node2 8080
loomcache-node3docker compose port loomcache-node3 7654docker compose port loomcache-node3 8080

Environment variables consumed by LoomProperties through Spring’s relaxed binding:

  • LOOMCACHE_NODE_PORT, LOOMCACHE_SERVER_PORT, LOOMCACHE_SERVER_BIND_ADDRESS, LOOMCACHE_SERVER_ENABLED
  • LOOMCACHE_CLUSTER_SEEDS
  • LOOMCACHE_SERVER_PERSISTENCE_ENABLED, LOOMCACHE_SERVER_PERSISTENCE_WAL_DIRECTORY, LOOMCACHE_SERVER_PERSISTENCE_SNAPSHOT_THRESHOLD
  • LOOMCACHE_SERVER_EVICTION_POLICY, LOOMCACHE_SERVER_EVICTION_MAX_ENTRIES, LOOMCACHE_SERVER_EVICTION_MAX_MEMORY_BYTES
  • LOOMCACHE_SERVER_RAFT_ELECTION_TIMEOUT_MS, LOOMCACHE_SERVER_RAFT_HEARTBEAT_INTERVAL_MS
  • LOOMCACHE_TRANSACTIONS_MAX_BUFFERED_OPS, LOOMCACHE_TRANSACTIONS_MAX_PARTICIPANT_GROUP_COUNT, LOOMCACHE_TRANSACTIONS_MAX_OPERATION_PAYLOAD_BYTES, LOOMCACHE_TRANSACTIONS_COMMIT_APPLY_RETRY_DELAY_MS, LOOMCACHE_TRANSACTIONS_COMMIT_APPLY_MAX_ATTEMPTS
  • LOOMCACHE_NETWORK_SHUTDOWN_DRAIN_MS (loomcache.network.shutdown-drain-ms)
  • SERVER_PORT for the Spring Boot HTTP listener (default 8080)
  • MANAGEMENT_SERVER_PORT for the Spring Boot management/actuator port (default 8080; set separately from SERVER_PORT to expose management endpoints on a dedicated port)

For source builds, target the builder-backed runtime stage:

Terminal window
docker build --pull --target runtime -t loomcache:local .

The runtime-signed stage is for the protected release workflow only; it expects LOOMCACHE_JAR to point at a prebuilt signed executable JAR. If MANAGEMENT_SERVER_PORT differs from SERVER_PORT, also configure MANAGEMENT_SERVER_SSL_ENABLED=true, MANAGEMENT_SERVER_SSL_KEY_STORE, MANAGEMENT_SERVER_SSL_TRUST_STORE, MANAGEMENT_SERVER_SSL_KEY_STORE_PASSWORD, MANAGEMENT_SERVER_SSL_TRUST_STORE_PASSWORD, and MANAGEMENT_SERVER_SSL_CLIENT_AUTH=need. Keep management on 8080 in production Docker unless those separate management SSL settings are provisioned and tested.

Static seed-list discovery is the only supported discovery strategy. Any provisioning tool can render the seed list — the deliverable is a stable set of nodeId=host:port entries plus a matching seed list for clients. Per-node env file:

Terminal window
LOOMCACHE_CLUSTER_CLUSTER_ID=loomcache-prod-eu-west-1
LOOMCACHE_CLUSTER_SEEDS=loomcache-1.eu-west.prod.lan:7654,loomcache-2.eu-west.prod.lan:7654,loomcache-3.eu-west.prod.lan:7654
LOOMCACHE_SERVER_RAFT_BOOTSTRAP_SERVERS=loomcache-1=loomcache-1.eu-west.prod.lan:7654,loomcache-2=loomcache-2.eu-west.prod.lan:7654,loomcache-3=loomcache-3.eu-west.prod.lan:7654
LOOMCACHE_NODE_ID=loomcache-1
LOOMCACHE_NODE_HOST=loomcache-1.eu-west.prod.lan

No DNS-discovery, no KubernetesApiDiscovery, no multicast. Helm/Kustomize/Ansible templating is fine to render this seed list, but the runtime never queries the Kubernetes API or another control plane to discover peers.

The client builder accepts the same seed list:

LoomClient client = LoomClient.builder()
.seeds(List.of("loomcache-1.eu-west.prod.lan:7654", "loomcache-2.eu-west.prod.lan:7654", "loomcache-3.eu-west.prod.lan:7654"))
.tlsConfig(tlsConfig)
.credentialsFactory(new StaticCredentialsFactory(ClientCredentials.certificate()))
.build();

Routing remains direct: clients open TCP connections to each seed, complete the protocol handshake, fetch the partition table, and route subsequent requests to the partition owner/leader without any reverse proxy in front.

Add loom-spring-boot to your application:

loomcache:
node:
host: node-1.eu-west.prod.lan
port: 5701 # Binary member port advertised in seeds; bind-address controls the listener interface.
client:
enabled: true # Required in embedded-server mode when the app needs the LoomCache client facade/CacheManager.
server:
enabled: true
bind-address: 0.0.0.0
# server.port is IGNORED for embedded-node binding — the node binds loomcache.node.port.
# Spring Boot startup logs a warning when server.port differs from loomcache.node.port.
# Remove or keep it equal to loomcache.node.port to avoid confusion.
persistence:
enabled: true
wal-directory: /var/lib/loomcache
cluster:
seeds:
- host1:5701
- host2:5701
- host3:5701

The auto-configuration wires the embedded server, health, and the REST endpoints listed in REST API. It registers a client-side LoomCache facade, not raw LoomClient/AsyncLoomClient beans; embedded-server mode disables that client by default unless loomcache.client.enabled=true, and LoomCacheManager is created only when the client exists and loomcache.spring-cache.enabled=true.

Database write-through (Oracle validation)

Section titled “Database write-through (Oracle validation)”

LoomCache’s optional JPA write-through targets Oracle for non-production validation. The production profile refuses packaged generic JDBC MapStore declarations and keeps the Spring default-map JPA bridge uninstalled, even though the optional bean still rejects unsafe/local datasources and accepts external Oracle with validate/none schema settings. Keep that bridge non-production unless a release explicitly validates leader-owned Raft wiring, snapshot/graceful-drain behavior for write-behind queues, and read-through recovery for the external-store path. Oracle validation environments should use a jdbc:oracle:thin:... URL and the Hibernate OracleDialect; keep spring.jpa.hibernate.ddl-auto=validate or none when schema validation is desired. Local tests can override this to update or create-drop while running H2 in Oracle-compatibility mode (MODE=Oracle) so the same SQL exercises the same code path without a live Oracle XE instance.

Upserts use Oracle MERGE INTO ... USING (...) ON (...) WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ... syntax for the non-production validation path. The Spring Boot module declares Spring Data JPA (org.springframework.boot:spring-boot-starter-data-jpa) and Oracle JDBC (com.oracle.database.jdbc:ojdbc11 or newer) as optional dependencies for the non-production external-store/JPA validation path, so consumers do not receive them by default. Applications that run that validation path must include Spring Data JPA and the driver explicitly. The validation path is not production cache persistence. No alternate JDBC driver is bundled for that path.

LoomCache does not support mixed-artifact server fleets. The version handshake advertises the build’s software and protocol compatibility information; peers are accepted only when the artifact version matches exactly, the advertised protocol value is this build’s supported value, and the Kryo registration digest matches this build.

A different artifact version, an unsupported protocol value, a digest mismatch, or an unknown handshake feature bit is rejected instead of negotiated down. This build has no lower-version or mixed-version rolling window.

The protocol handshake is fail-closed. Peers exchange version, serializer-digest, and fresh nonce evidence before a connection is trusted. A peer is rejected on malformed payloads, duplicate negotiation attempts, nonce mismatches, digest mismatches, version/protocol mismatches, or unexpected negotiation messages; when possible the receiver returns a protocol error response before marking the connection to close.

Server upgrades use homogeneous server-version windows: back up the cluster, stop or drain traffic according to the degradation matrix, upgrade every member to the same server build, then return the cluster to ACTIVE.

Pre-upgrade checks (before stopping any member):

  1. Confirm /api/cluster/status (ADMIN) reports all members live, Raft leader elected, and operational state ACTIVE.
  2. Confirm partition migration is idle — loomcache.partition.migration.bytes counter is not advancing and /actuator/health/readiness is UP. Direct node runs can additionally confirm subsystems.migrationQueue in rich /health/readiness is 0 over the TLS listener with a verified mTLS client; plaintext /health/readiness returns 403.
  3. Confirm no in-flight cross-group transactions through the operator transaction diagnostics on the raft-0 leader.
  4. Back up all WAL and snapshot directories before touching any member.

During and after the upgrade:

  1. Confirm /api/cluster/status reports the expected clusterVersion, member liveness, and operational state.
  2. Verify loomcache_handshake_rejected (Prometheus wire form; Micrometer name loomcache.handshake.rejected) stays at zero; any non-zero count means an incompatible client or peer is still connecting.
  3. Confirm member logs have no Kryo registration digest mismatch, invalid nonce/digest key, or nonce ... mismatch handshake errors; those indicate mixed artifacts or mismatched serializer registration.
  4. Upgrade clients and members as a homogeneous artifact set; this build has no mixed-version rolling window.
  5. Keep a rollback copy of the previous server artifact and data backup until smoke tests pass.
  6. Run the SLO benchmark and persistence validation from the performance and persistence guides after the upgrade.

Public LoomCache releases follow the SemVer and release-note contract in the root release docs. Public APIs, REST endpoints, and configuration keys require the appropriate version boundary for breaking changes. Wire and storage compatibility is maintained for supported same-artifact upgrade and recovery paths, but internal transport frames, local WAL records, and snapshot layouts are not application-facing extension APIs.

Before upgrading, review the release notes for removed or deprecated surfaces and update any configuration, REST clients, or startup scripts that rely on them. Deprecated public surfaces should document the replacement or migration path when one exists; unsupported and development-only surfaces can change or disappear outside the public compatibility contract.

See Default Ports for the full allocation and the 5701 vs. 7654 split.

PortRoleTypical scope
5701Default JVM/Spring Boot binary member TCPLocal JVM, bare metal, direct config
5702Direct-node admin HTTP (/health, /ready)Non-production or private plaintext mode; production direct admin must disable plaintext
5703Direct-node admin HTTPS/TLS listenerRequired with mTLS when production direct admin is enabled; Docker/Spring Boot production templates disable direct admin and use Actuator
7654Docker sample binary member TCPContainer samples
9090Direct-node Prometheus metrics HTTP (/metrics)Direct node/embedded runtime only; not served by the Spring Boot jar
8080Spring Boot HTTPS / REST / ActuatorOptional Boot app, management JSON, and /actuator/prometheus scrape

See Security & mTLS. Minimum viable production configuration: tlsConfig.enabled = true, keystore and truststore paths pointing at PKCS12 files, requireClientAuth = true, revocationCheckingEnabled = true, and revocationSoftFail = false; also enable JSSE CRLDP or OCSP with JVM flags so hard-fail revocation has a source.

LoomCache is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Hazelcast, Inc. or by any other company whose products are named in this documentation. “Hazelcast” is a trademark of Hazelcast, Inc.; references to it are nominative and describe only migration and comparison. All other product and company names are trademarks of their respective owners and are used for identification purposes only.