Immutable images when embedding QuestDB Java library and noexec /tmp
When /tmp
is mounted with noexec
(a common hardening step), the QuestDB Java library may still try to unpack its
native libraries into /tmp
and load them from there. On hardened hosts or minimal containers, this fails with
UnsatisfiedLinkError
or permission errors.
This guide shows how to run QuestDB’s Java library without writing executables to /tmp
, by pre-bundling the native
libs and pointing QuestDB at them via -Dquestdb.libs.dir
. It also documents the fallback of redirecting Java’s temp directory.
Important: If you're using official QuestDB server container images or platform-specific distributions, you do not need to do anything. They already ship the native libraries and set
-Dquestdb.libs.dir
, so nothing gets unpacked to/tmp
. The rest of this guide is only for teams building their own images from scratch or embedding the QuestDB Java library in another application.
TL;DR
On official QuestDB images/distributions? You're done. This is already handled.
- QuestDB’s Java library needs two native libs at runtime:
libquestdb.so
libquestdbr.so
- They both are already inside the JAR under:
io/questdb/bin/linux-x86-64/
(for x86_64)io/questdb/bin/linux-aarch64/
(for ARM64)
- Best practice: extract those
.so
files at build time into your image (or host) and start the JVM with:
java -Dquestdb.libs.dir=/opt/questdb/lib -jar your-app.jar
- Alternative: move Java’s temp dir off
/tmp
:
java -Djava.io.tmpdir=/run/questdb-tmp -jar your-app.jar
…but pre-bundling is preferable for immutable builds and least surprise in prod.
Who is affected?
This guide applies to teams who embed the QuestDB Java library via Maven/Gradle in a custom application, especially
when that application is deployed to a hardened container or host where /tmp
is mounted with noexec
.
Why this happens
QuestDB ships two native libraries (libquestdb.so
, libquestdbr.so
) required by the Java client and embedded mode.
When -Dquestdb.libs.dir
is not set, the library falls back to unpacking native binaries from its JAR into Java’s
temp directory (usually /tmp
) and loads them from there. On hardened systems, mapping or executing from /tmp
fails.
Recommended approach: immutable builds
Ship the native libs with your application and tell QuestDB where to find them.
1) Extract the native libs from the JAR
If you already have the dependency JAR locally, you can extract only the two files you need:
# x86_64 exampleunzip -j path/to/questdb-<version>.jar \'io/questdb/bin/linux-x86-64/libquestdb.so' \'io/questdb/bin/linux-x86-64/libquestdbr.so' \-d /opt/questdb/lib# ARM64 exampleunzip -j path/to/questdb-<version>.jar \'io/questdb/bin/linux-aarch64/libquestdb.so' \'io/questdb/bin/linux-aarch64/libquestdbr.so' \-d /opt/questdb/lib
Tip:
.so
files don’t need the filesystem executable bit, but the mount where they reside must allow executable mappings (avoidnoexec
). Keep files readable by all, but writable only by the owner.
2) Point QuestDB at your lib directory
At startup, pass:
-Dquestdb.libs.dir=/opt/questdb/lib
Examples:
Plain JVM
java \-Dquestdb.libs.dir=/opt/questdb/lib \-cp your-app.jar com.example.Main
Kubernetes (env‑style via JAVA_TOOL_OPTIONS
)
env:- name: JAVA_TOOL_OPTIONSvalue: "-Dquestdb.libs.dir=/opt/questdb/lib"
Programmatic (set very early in main
)
public static void main(String[] args) {System.setProperty("questdb.libs.dir", "/opt/questdb/lib");// start the app before any QuestDB classes trigger native loads}
Build recipes
A) Docker (multi‑stage)
# --- stage: fetch questdb jar and extract nativesFROM alpine:latest AS qdb-nativesARG QDB_VERSION=9.0.3RUN apk add --no-cache unzip ca-certificatesRUN mkdir -p /opt/questdb/libADD https://repo1.maven.org/maven2/org/questdb/questdb/${QDB_VERSION}/questdb-${QDB_VERSION}.jar /tmp/questdb.jarRUN unzip -j /tmp/questdb.jar \'io/questdb/bin/linux-x86-64/libquestdb.so' \'io/questdb/bin/linux-x86-64/libquestdbr.so' \-d /opt/questdb/lib# --- stage: your app imageFROM eclipse-temurin:21-jreWORKDIR /appCOPY --from=qdb-natives /opt/questdb/lib /opt/questdb/libCOPY build/libs/your-app-all.jar /app/app.jarENV JAVA_TOOL_OPTIONS="-Dquestdb.libs.dir=/opt/questdb/lib"CMD ["java", "-jar", "/app/app.jar"]
If you build for ARM64, swap the two
unzip
paths tolinux-aarch64
.
B) Maven: unpack at build time
Use maven-dependency-plugin
to unpack just the native libs into your build output.
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.6.1</version><executions><execution><id>unpack-questdb-natives</id><phase>process-resources</phase><goals><goal>unpack</goal></goals><configuration><artifactItems><artifactItem><groupId>org.questdb</groupId><artifactId>questdb</artifactId><version>9.0.3</version><includes>io/questdb/bin/linux-x86-64/libquestdb.so,io/questdb/bin/linux-x86-64/libquestdbr.so</includes><outputDirectory>${project.build.outputDirectory}/questdb/lib</outputDirectory></artifactItem></artifactItems><overWriteReleases>true</overWriteReleases></configuration></execution></executions></plugin>
Note: Adjust the dependency coordinates (
org.questdb:questdb:9.0.3
) to match the exact QuestDB verson in your project.
Then package those files into your image or distribution and run with -Dquestdb.libs.dir=${app.home}/questdb/lib
.
C) Gradle (Kotlin DSL)
dependencies {implementation("org.questdb:questdb:9.0.3")}val copyQuestDbNatives by tasks.registering(Copy::class) {val qdbJar = configurations.runtimeClasspath.get().files.first { it.name.startsWith("questdb-") && it.extension == "jar" }from(zipTree(qdbJar)) {include("io/questdb/bin/linux-x86-64/libquestdb.so")include("io/questdb/bin/linux-x86-64/libquestdbr.so")}into(layout.buildDirectory.dir("questdb/lib"))}tasks.named("processResources") { dependsOn(copyQuestDbNatives) }
Note: Adjust the dependency coordinates (
org.questdb:questdb:9.0.3
) to match the exact QuestDB verson in your project.
Bundle ${buildDir}/questdb/lib
with your app and set -Dquestdb.libs.dir
accordingly.
Alternative: change Java’s temp dir
If you can’t rebuild images right now, redirect the extraction location away from /tmp
to a mount that allows executable mappings:
# Example: a tmpfs with exec permitted, sized smallsudo mkdir -p /run/questdb-tmpsudo mount -t tmpfs -o size=32m,mode=0755 tmpfs /run/questdb-tmpjava -Djava.io.tmpdir=/run/questdb-tmp -jar your-app.jar
This avoids /tmp
, but it’s less ideal than pre-bundling because you’re still extracting at runtime.
Troubleshooting
UnsatisfiedLinkError: cannot map file, Operation not permitted
- The target directory is on a
noexec
mount. Move the libs to a different mount and update-Dquestdb.libs.dir
.
It still writes to /tmp
- Ensure
-Dquestdb.libs.dir
is present before any QuestDB classes initialize. - Check typos: the property name must be exactly
questdb.libs.dir
.
Wrong architecture
- Extract from
linux-aarch64
on ARM64, notlinux-x86-64
.
System Compatibility
Minimal distros (e.g., distroless/alpine)
- QuestDB requires glibc 2.17+ on x86_64 and 2.28+ on ARM64. Make sure your base image meets this. QuestDB currently does not support musl/Alpine. Please vote for this issue if you need it.
Official images & distributions: already handled (no action required)
QuestDB’s official container images and platform-specific distributions already include the native libs (e.g., under /app/lib
) and start the JVM with:
-Dquestdb.libs.dir=/app/lib
If you're using these, you do not need to change anything — no runtime extraction to /tmp
occurs.
Closing notes
- Prefer immutable builds with
-Dquestdb.libs.dir
. - Use
-Djava.io.tmpdir
only as a stopgap. - Keeping
/tmp
noexec is good practice—just don’t rely on it for native libs.