Docker & Static Seed Deployment
Docker Compose Cluster
StaticDiscovery seed list · direct member connections
This guide covers the supported ways to run LoomCache and the production boundaries that apply to each. LoomCache ships three deployment shapes:
- Embedded in an existing JVM (single-node, in-process) via
EmbeddedLoomCache.builder(). - Spring Boot embedded —
loom-spring-bootauto-configuration wired into an application. - 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.
Production support boundaries
Section titled “Production support boundaries”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
LoomMaptraffic. - 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 INDEXand 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.
Standalone single-node (non-Spring)
Section titled “Standalone single-node (non-Spring)”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();Graceful-shutdown drain budget
Section titled “Graceful-shutdown drain budget”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:
| Phase | Budget |
|---|---|
TCP listener drain (loomcache.network.shutdown-drain-ms, LOOMCACHE_NETWORK_SHUTDOWN_DRAIN_MS) | up to 60000 ms (Docker default) |
| Executor command drain + executor termination | up to 60000 ms (loom.executor.stopDrainTimeoutMs defaults to 30000 ms and can be waited twice) |
| Spring Boot lifecycle | a 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.
Production Docker member
Section titled “Production Docker member”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.
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.
Local development Docker Compose
Section titled “Local development Docker Compose”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.
docker compose up -ddocker compose psdocker compose logs -f loomcache-node1docker compose port loomcache-node1 7654docker compose port loomcache-node1 8080The 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.
| Service | Cluster → host | REST/Actuator/Prometheus → host |
|---|---|---|
loomcache-node1 | docker compose port loomcache-node1 7654 | docker compose port loomcache-node1 8080 |
loomcache-node2 | docker compose port loomcache-node2 7654 | docker compose port loomcache-node2 8080 |
loomcache-node3 | docker compose port loomcache-node3 7654 | docker 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_ENABLEDLOOMCACHE_CLUSTER_SEEDSLOOMCACHE_SERVER_PERSISTENCE_ENABLED,LOOMCACHE_SERVER_PERSISTENCE_WAL_DIRECTORY,LOOMCACHE_SERVER_PERSISTENCE_SNAPSHOT_THRESHOLDLOOMCACHE_SERVER_EVICTION_POLICY,LOOMCACHE_SERVER_EVICTION_MAX_ENTRIES,LOOMCACHE_SERVER_EVICTION_MAX_MEMORY_BYTESLOOMCACHE_SERVER_RAFT_ELECTION_TIMEOUT_MS,LOOMCACHE_SERVER_RAFT_HEARTBEAT_INTERVAL_MSLOOMCACHE_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_ATTEMPTSLOOMCACHE_NETWORK_SHUTDOWN_DRAIN_MS(loomcache.network.shutdown-drain-ms)SERVER_PORTfor the Spring Boot HTTP listener (default8080)MANAGEMENT_SERVER_PORTfor the Spring Boot management/actuator port (default8080; set separately fromSERVER_PORTto expose management endpoints on a dedicated port)
For source builds, target the builder-backed runtime stage:
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 cluster
Section titled “Static seed-list cluster”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:
LOOMCACHE_CLUSTER_CLUSTER_ID=loomcache-prod-eu-west-1LOOMCACHE_CLUSTER_SEEDS=loomcache-1.eu-west.prod.lan:7654,loomcache-2.eu-west.prod.lan:7654,loomcache-3.eu-west.prod.lan:7654LOOMCACHE_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:7654LOOMCACHE_NODE_ID=loomcache-1LOOMCACHE_NODE_HOST=loomcache-1.eu-west.prod.lanNo 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.
Spring Boot embedded
Section titled “Spring Boot embedded”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:5701The 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.
Version and protocol policy
Section titled “Version and protocol policy”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):
- Confirm
/api/cluster/status(ADMIN) reports all members live, Raft leader elected, and operational stateACTIVE. - Confirm partition migration is idle —
loomcache.partition.migration.bytescounter is not advancing and/actuator/health/readinessis UP. Direct node runs can additionally confirmsubsystems.migrationQueuein rich/health/readinessis0over the TLS listener with a verified mTLS client; plaintext/health/readinessreturns403. - Confirm no in-flight cross-group transactions through the operator transaction diagnostics on the raft-0 leader.
- Back up all WAL and snapshot directories before touching any member.
During and after the upgrade:
- Confirm
/api/cluster/statusreports the expectedclusterVersion, member liveness, and operational state. - Verify
loomcache_handshake_rejected(Prometheus wire form; Micrometer nameloomcache.handshake.rejected) stays at zero; any non-zero count means an incompatible client or peer is still connecting. - Confirm member logs have no
Kryo registration digest mismatch,invalid nonce/digest key, ornonce ... mismatchhandshake errors; those indicate mixed artifacts or mismatched serializer registration. - Upgrade clients and members as a homogeneous artifact set; this build has no mixed-version rolling window.
- Keep a rollback copy of the previous server artifact and data backup until smoke tests pass.
- Run the SLO benchmark and persistence validation from the performance and persistence guides after the upgrade.
Change and removal policy
Section titled “Change and removal policy”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.
| Port | Role | Typical scope |
|---|---|---|
| 5701 | Default JVM/Spring Boot binary member TCP | Local JVM, bare metal, direct config |
| 5702 | Direct-node admin HTTP (/health, /ready) | Non-production or private plaintext mode; production direct admin must disable plaintext |
| 5703 | Direct-node admin HTTPS/TLS listener | Required with mTLS when production direct admin is enabled; Docker/Spring Boot production templates disable direct admin and use Actuator |
| 7654 | Docker sample binary member TCP | Container samples |
| 9090 | Direct-node Prometheus metrics HTTP (/metrics) | Direct node/embedded runtime only; not served by the Spring Boot jar |
| 8080 | Spring Boot HTTPS / REST / Actuator | Optional Boot app, management JSON, and /actuator/prometheus scrape |
TLS / mTLS
Section titled “TLS / mTLS”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.