Stop Getting FileNotFound in Containers: How Java Developers Can Load Resources Correctly with Quarkus
Master classpath resource access in containerized JVM and native builds. No more brittle file paths, just production-proof practices.
Quarkus is built for fast startup and lean runtime behavior; perfect for cloud-native environments. But once you move from development to containers, certain assumptions can break. One common issue is accessing resource files like templates, text files, or default configs packaged inside your application. In this hands-on tutorial, you’ll learn how to correctly read static resource files from within a Quarkus app. Regardless of whether you’re running on the JVM or as a native image inside a container.
Let’s walk through it step-by-step.
Understanding the Problem: File Paths Don’t Work the Way You Think
When you're working in your IDE, loading a resource from src/main/resources/my-data.txt
with a relative file path might appear to work. But once your Quarkus application is packaged, especially as a JAR or native executable, that directory structure vanishes. Files in src/main/resources
are no longer separate files. Instead, they’re embedded into the application and must be accessed through the classpath.
The fix? Use Java’s ClassLoader
mechanism — it gives you a portable and consistent way to access embedded resources across all environments.
Prerequisites
Before you start, make sure you have the following installed and ready:
Java (JDK 17 or newer): Compatible with your Quarkus version.
Apache Maven: For building your Quarkus application.
Podman: For containerizing and running the app.
Basic knowledge of Java and Quarkus: You should be familiar with JAX-RS and project structure.
Step-by-Step Guide
Create a Simple Quarkus Project
We’ll start by scaffolding a fresh Quarkus project with REST capabilities. This gives us a clean slate to demonstrate file reading.
mvn io.quarkus.platform:quarkus-maven-plugin:LATEST:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=resource-reader \
-Dextensions="rest-jackson"
cd resource-reader
Now let’s add a simple text file to act as our embedded resource:
echo "Hello from Resource File!" > src/main/resources/my-data.txt
This file will be embedded in the final application, not copied over as a separate file in the container.
Read the Resource Using Java ClassLoader
The key to reliable resource access in Java (and Quarkus) is the ClassLoader
. This gives you a stream to the embedded file, no matter where your code is running.
Create a file src/main/java/org/acme/ResourceReaderResource.java
and add the following:
package org.acme;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
@Path("/resource")
public class ResourceReaderResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response readResource() {
String resourcePath = "/my-data.txt"; // Absolute path from classpath root
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
if (inputStream == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Resource not found: " + resourcePath)
.build();
}
String content = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
return Response.ok(content).build();
} catch (Exception e) {
e.printStackTrace();
return Response.serverError()
.entity("Error reading resource: " + e.getMessage())
.build();
}
}
// Alternative for advanced use-cases
private InputStream getResourceViaContextClassLoader(String path) {
return Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(path.startsWith("/") ? path.substring(1) : path);
}
}
Why this matters:
getResourceAsStream()
looks for the resource on the classpath — not on disk.The path
/my-data.txt
is relative to the root of thesrc/main/resources
directory.It works whether the app is running in dev mode, in a JAR, or in a native binary.
Understand the Dockerfile Setup
Quarkus generates ready-to-use Dockerfiles in src/main/docker
. Let’s explore what they do and why you don’t need to tweak anything for resource file access.
Dockerfile.jvm
Uses a full JDK or JRE base image.
Copies the Quarkus-generated JARs.
Exposes port 8080.
Runs the app via the
java
command.
Dockerfile.native
Uses a tiny base image.
Copies in the native executable.
Runs it directly (no JVM required).
Important: Files in src/main/resources
are automatically included in the native executable. However, if you dynamically generate files or use non-standard paths, you may need to set this in application.properties
:
quarkus.native.resources.includes=my-data.txt
Build the Application and Docker Images
Time to compile and package everything.
For JVM mode:
./mvnw clean package -Dquarkus.package.jar.type=fast-jar
podman build -f src/main/docker/Dockerfile.jvm -t quarkus/res-read-jvm .
For Native mode (You will need a LOT of memory for this!!):
mvn clean package -Dnative -Dquarkus.native.container-build=true
# Optional manual Docker image build:
# podman build -f src/main/docker/Dockerfile.native -t quarkus/resource-reader-native .
This step produces either a runnable JAR or a native binary, plus the corresponding container image.
Run the Container
Time to fire it up and test our setup.
JVM mode:
podman run --rm -p 8080:8080 quarkus/res-read-jvm
Native mode:
podman run --rm -p 8080:8080 quarkus/resource-reader-native
Both commands expose port 8080 and remove the container once it exits.
Verify Everything Works
Open your browser or terminal and run:
curl http://localhost:8080/resource
Expected output:
Hello from Resource File!
This proves the app is reading the embedded resource file, from inside a container, without relying on the file system.
Key Takeaways and Troubleshooting
Understanding how Java resource loading works can save you hours of frustration in containerized environments. Here are the key points to remember:
Classpath over File Paths: Don’t hardcode
src/main/resources/...
. Use the classpath instead.Leading Slash:
/my-data.txt
means root of classpath. Without it, Java will search relative to the class’s package.Always Test in a Container: Your dev machine may mask these issues — always verify behavior in the container.
Native Resource Inclusion: Quarkus usually auto-includes standard resources. If you're missing one, check build logs and
quarkus.native.resources.includes
.
Debug Tip: Check Your JAR
Want to be 100% sure your file made it into the app?
jar tf target/quarkus-app/app/resource-reader-1.0.0-SNAPSHOT.jar | grep my-data.txt
If it's listed — it’s embedded.
Alternatives to Embedded Resource Files
While embedding static files inside your Quarkus application works well for default configurations, templates, or read-only reference data, it’s not always the best choice. In many real-world scenarios, you’ll need to externalize or dynamically manage files. Here are some common alternatives:
Mounting Files via Volume in Containers
In containerized environments like Kubernetes or Podman, you can mount files or directories into your application container at runtime using volumes:
podman run -v /host/path/config:/app/config -p 8080:8080 my-app
Then, your Quarkus app can access the mounted files using standard Java file I/O:
Path configPath = Paths.get("/app/config/custom-config.yaml");
This is ideal for external configuration, user uploads, or frequently updated files.
Serving Files from Object Storage (S3, MinIO, etc.)
For cloud-native apps, storing files in an object store is often preferred. Services like Amazon S3 or MinIO allow your app to:
Offload large or dynamic content
Enable distributed access
Avoid bloating the application image
You can use the AWS SDK or REST clients to download files at runtime:
S3Object object = s3Client.getObject("my-bucket", "path/to/my-data.txt");
This approach is ideal for large files, logs, images, or any content that changes independently of the application.
External Configuration Services
Quarkus supports integration with external configuration sources like:
Kubernetes ConfigMaps or Secrets
HashiCorp Vault
Consul or Etcd
For dynamic configs, consider using Quarkus extensions like:
quarkus-config-yaml
quarkus-vault
quarkus-consul-config
These allow you to update configuration values without rebuilding the application or image.
When to Use What
Choosing the right method for handling resource files in your Quarkus application depends on the use case. If you're dealing with static files like templates, default configuration, or read-only data that never changes at runtime, embedding them directly in src/main/resources
is both convenient and reliable. For files that are mutable or need to be changed without rebuilding the application, such as user uploads, runtime configuration, or external datasets, it's better to mount them into the container using persistent volumes or shared storage. When operating in a cloud-native environment and scalability or distributed access is a concern, storing files in an object storage service like Amazon S3 or MinIO allows your application to fetch content on demand without inflating the image size. Finally, for secure and dynamic configuration such as credentials, API keys, or feature flags, Quarkus integrates well with external configuration services like HashiCorp Vault, Kubernetes ConfigMaps, or Consul, enabling safe and runtime-aware configuration without hardcoding values into your application.
Conclusion
Static resource files are part of nearly every application — and in Java, the classpath is the key to accessing them reliably. This becomes even more important in containerized environments where the source layout no longer exists.
By using getResourceAsStream()
and building with Quarkus conventions in mind, you can ensure your app works the same in dev, test, production — JVM or native — and avoids the classic "it works on my machine" trap.
And if embedded resources aren’t flexible enough? You’ve now seen how to scale up with volume mounts, cloud storage, or external config tools as your application evolves.