I like docker and use it, but it's not for everything everywhere and is overly-hyped IMO.
This is the key. The right JVM, the right classpaths, the right configuration, the right permissions, the right native libraries.
The setup process for every Java app we've used (thankfully just ActiveMQ and Kafka lately) have been incredibly complicated. JAR files in paths, long, convoluted shell scripts to set up all possible variables for every possible JVM, wrappers that wrap launchers, etc.
And then all of those steps are prone to breakage and are difficult to debug.
Shipping a Docker container lets you say "Here is a working environment that needs no configuration and won't suddenly fail until reconfigured when another app you have needs Java 9 and not Java 8".
A VM is also super heavy: all those libraries, the package system, init/systemd and you need some Ansible/Chef/Puppet scripts to build all that. A container still has a lot of those libraries (most are build on a base distro image: ubuntu, debian, alpine, etc), but no init/systemd. Your process runs as 1 with cgroups used for isolation.
You also have this nice contained artifact. No packaging it as an RPM/DEB and having to install it. It comes with its JVM and dependencies in there. It will always run the same way where ever you put it. If you have five containers that all package the same way, when you deploy, you don't get 5x the space usage. Docker uses overlay filesystems so the base can be shared.
Also once it's in a container, you can use something fancy like Marathon, K8s, nomad or swarm to schedule it to run somewhere out on a cluster. These ecosystems have tools for monitoring the number of services you have running in parallel and the ability to scale them up and down.
You don't get the isolation/security you get in a VM (if the kernel has an exploit, it can be exploited from within the container. It shares the host's kernel), but they are as light as a process with some security guarantees via cgroups and with all your artifacts and dependencies packed into an image you can easily save off, transfer and deploy.
So you can see it as a convenient way to distribute the JVM. Say that the JVM makes your application portable wrt the OS, Docker makes the JVM itself more portable.
And, as other people have pointed out, a docker container isn't a VM. It's a chroot process with a bunch of tooling around provisioning, networking, security, etc around it.
One of the big wins of docker is that it allows you to ship file-systems around and run them as if they were binaries.
1. A compatible JVM is installed somewhere.
2. When you run your application, it uses that JVM and not some other JVM.
3. You have the right flags passed to the JVM. For example, max heap size matching your application's actual usage, garbage collector tuning, thread stack size, and any properties that are set via -D.
4. Your jar file is there.
5. There is a place to write log files, and your application's logging system knows to write them there.
6. If you are using any native libraries through JNI, they need to be installed, and the JVM must be able to find them and use them.
7. If you listen on port, an available port number is found, your application knows what it is, and the rest of the world knows where to find you.
I believe Docker addresses all these points.