One container, five engines. Drop in JARs or APKs, get back a source tree per decompiler.
No decompiler gets everything right. Obfuscated code, unusual compilers (Kotlin, Scala, Groovy), or hand-crafted bytecode break each engine in different places, so the fastest way to a readable result is to run several and compare. Installing five decompilers with their Java and Rust toolchains is tedious; pulling one image is not.
This also makes the image a convenient front-end for AI-assisted reverse engineering: language models work on source code, not bytecode.
Point the image at a JAR, then let your assistant read ./outfiles.
See Using with AI assistants.
Create three directories next to each other:
./infiles: the.jar,.war,.ear,.class,.apkor.dexfiles to decompile./libfiles: optional JARs the inputs depend on (helps CFR and Vineflower resolve types)./outfiles: empty, receives one directory per input and engine
Then run the image.
docker run --rm -ti \
--user "$(id -u):$(id -g)" \
-v "$PWD/infiles:/infiles:ro" \
-v "$PWD/libfiles:/libfiles:ro" \
-v "$PWD/outfiles:/outfiles" \
ghcr.io/eikendev/java-decompiler:latestWith Podman use --userns=keep-id instead of --user, and add :Z to the volume flags on SELinux systems.
make run does exactly that.
The image is published for linux/amd64 and linux/arm64, so it runs natively on Apple Silicon and ARM servers.
For an input app.jar you get:
outfiles/
├── app.jar-cfr/ # CFR
├── app.jar-cfr.log # engine log
├── app.jar-vineflower/ # Vineflower
├── app.jar-procyon/ # Procyon
├── app.jar-jadx/ # jadx
└── app.jar-krakatau/ # Krakatau disassembly (.j files)
For an APK, jadx decompiles the APK directly (app.apk-jadx/), dex2jar converts it to app.apk-dex2jar/app.apk.jar, and that JAR is then run through the JVM engines (app.apk.jar-cfr/, …).
Existing output directories are skipped, so re-running after adding files only processes what is new.
Engines run in parallel and a crashing engine never stops the others.
The exit code is 2 if any engine failed, with the reason in its .log file.
All options are environment variables (docker run -e NAME=value …).
| Variable | Default | Purpose |
|---|---|---|
DECOMPILERS |
cfr,vineflower,procyon,jadx,krakatau |
Engines to run |
JOBS |
number of CPUs, at most 4 | Engines running concurrently |
JAVA_OPTS |
-XX:MaxRAMPercentage=25 |
JVM flags for every Java based engine (JAVA_XMX=4G is still honoured) |
FORMAT |
1 |
Reformat all .java output with astyle for easier diffing between engines; 0 disables |
CFR_ARGS, VINEFLOWER_ARGS, PROCYON_ARGS, JADX_ARGS, KRAKATAU_ARGS |
empty | Extra arguments passed to the engine |
JADX_ANDROID_ARGS |
--deobf |
Extra jadx arguments for APK/DEX inputs only |
Example: only the two strongest engines, with 8 GB of heap each.
docker run --rm --user "$(id -u):$(id -g)" -e DECOMPILERS=cfr,vineflower -e JAVA_XMX=8G \
-v "$PWD/infiles:/infiles:ro" -v "$PWD/libfiles:/libfiles:ro" -v "$PWD/outfiles:/outfiles" \
ghcr.io/eikendev/java-decompiler:latestDecompilers are deterministic and fast; language models are good at the part decompilers are bad at, namely naming things and explaining intent. A workflow that works well:
- Run the image on the artifact, keeping at least two engines so the assistant can cross-check where one produced nonsense.
- Open
./outfilesin your coding assistant (for example Claude Code or Cursor) and ask it to summarise the code, rename obfuscated identifiers, or look for a specific behaviour, mentioning that the-cfrand-vineflowerdirectories contain the same classes from different decompilers. - For stubborn methods, point it at the
-krakataudirectory, whose disassembly is exactly what the JVM sees and does not depend on any decompiler heuristics.
Because every input is processed in one shot and written to plain files, the image slots into any agent pipeline that can run a container and read a directory.
| Tool | Version | Role |
|---|---|---|
| CFR | 0.152 | Decompiler, strong on modern Java language features |
| Vineflower | 1.12.0 | Decompiler, actively maintained fork of JetBrains' Fernflower with Kotlin support |
| Procyon | 0.6.0 | Decompiler, from the Debian package |
| jadx | 1.5.6 | Decompiler for APK/DEX and JARs, with deobfuscation |
| Krakatau | v2 (git) | Robust disassembler for obfuscated or malformed class files |
| dex2jar | 2.4.38 | DEX to JAR conversion so the JVM engines can read APKs |
| astyle | Debian | Uniform formatting of all output |
For background on these tools, see the related blog post.
Decompilers parse untrusted input, so the image is built to limit what a malicious file can reach:
-
Runs as an unprivileged user, and the input volumes can be mounted read-only.
-
Every tool is pinned to a specific release and fetched from its upstream GitHub release page.
-
Images are built for both architectures on GitHub Actions with an SBOM and signed build provenance that you can verify:
gh attestation verify oci://ghcr.io/eikendev/java-decompiler:latest --owner eikendev
-
The image is rebuilt weekly so Debian security updates land without a code change.
Releases are cut by Release Please from the commit history, and each release is also published as ghcr.io/eikendev/java-decompiler:<version>.
make build # build the image locally with Podman (RUNTIME=docker for Docker)
make test # build and run the smoke test
make check # static checks (shellcheck)Decompiler versions are build arguments at the top of the Dockerfile and are bumped by hand.
Dependabot keeps the base images and GitHub Actions current.