OpenJDK Java Development Kit
Overview
The SecureAnyCloud OpenJDK 21 Dev Kit is a Java development workstation on Ubuntu 24.04 on Azure. You connect to it over SSH and use it to write, build, test and package Java applications. This guide covers deploying and using the image from the Azure Marketplace.
The image is a development workstation, not a server. It runs no network service of any kind. After first boot, the only listening socket reachable from outside the machine is SSH on port 22. There is no web console, no dashboard and no login page, so no credential of any kind ships in the image. There is nothing to log in to and nothing to rotate.
What you get:
- OpenJDK 21 LTS as the primary toolchain, installed from the Ubuntu 24.04
mainpocket. The Ubuntu OpenJDK build is GPL-2.0 with the Classpath Exception, which makes it redistributable inside a commercial machine image. Oracle JDK is not and is never installed here. - Automatic security fixes. Because the JDK comes from
main, it is covered bynoble-security. Theunattended-upgradesservice, which this image leaves enabled, upgrades it automatically, so your JVM keeps receiving CVE fixes for the life of the machine. - Nothing is held back or version-pinned. There is no
apt-mark holdand noPin: versionanywhere. Freezing a package is a common way to pin a toolchain, but it silently blocks every future security update. This image deliberately does not do that. - Pinning by verification and assertion. At build time, every installed OpenJDK
.debhad its SHA-256 compared against the digest Ubuntu publishes in its GPG-signedPackagesindex. The resolved version was recorded, andjava -versionwas asserted to report that exact build. The recorded pin lives in/usr/share/doc/sac-openjdk/BUILD-PINon the machine. - A dedicated 32 GiB data volume at
/srv/javadata, carrying the Maven local repository,GRADLE_USER_HOMEand a workspace directory
Where your work lives. Dependency caches grow to gigabytes on a real project. Keeping them off the operating system disk lets you size, snapshot and grow that storage independently.
The mount point is /srv/javadata and not /mnt. On Azure, /mnt is where the ephemeral resource disk is mounted, and anything cached there is lost when the VM is deallocated.
What is included
- OpenJDK 21 LTS (the default
javaandjavac) from the Ubuntu 24.04mainpocket, GPL-2.0 with the Classpath Exception, checksum verified at build time - OpenJDK 17 LTS as a secondary toolchain, from the same pocket under the same licence, for projects that still target 17
- Apache Maven 3.9.16 from the official Apache archive, verified against the SHA-512 Apache publishes
- Gradle 9.6.1 from the official gradle.org archive, verified against the SHA-256 Gradle publishes
- A dedicated 32 GiB data volume at
/srv/javadataholding the Maven local repository,GRADLE_USER_HOMEand a workspace directory, mounted by UUID so it survives reboots and device renames sac-java-selftest, a single command that resolves a real dependency from Maven Central, runs a JUnit 5 suite, packages a runnable jar, executes it and checks the values it computed- A working example project at
/usr/local/share/sac-java/selftest: a Maven and Gradle project with a real dependency and a JUnit 5 suite, ready to copy into your workspace as a starting point - A global Maven toolchains file registering both JDKs, so a project can select either one
- The complete third-party licence bundle for both JDKs, Maven and Gradle, under
/usr/share/doc/sac-openjdk/licenses/. It includes the whole of each JDK'slegal/tree and the GPL-2 body the Ubuntu copyright file refers to - Ubuntu 24.04 LTS, fully patched at build time with
unattended-upgradesleft enabled
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair (
ssh-keygen -t ed25519if you do not already have one) - A VM size with at least 2 vCPUs and 4 GiB of memory.
Standard_B2sis the recommended size and is what this image is tested on. Step up toStandard_D2s_v3or larger if you build big multi-module projects - Outbound HTTPS access, so Maven and Gradle can reach Maven Central
Deploying the image
Create the VM from the Marketplace offer. Choose an administrator username and supply your SSH public key.
The image declares its own 32 GiB data disk, so you do not need to attach one. Azure provisions it, formats it and mounts it at /srv/javadata automatically.
Inbound rules need only SSH on port 22, ideally restricted to your own address. This product exposes nothing else, so there is no other port to open.
Connecting
Connect over SSH using the administrator account you chose:
ssh azureuser@<vm-ip>First boot runs a one-shot setup service. It resolves the administrator account you created, grants it access to the data volume, points ~/.m2/repository and ~/.gradle at that volume, and writes a plain toolchain manifest. It finishes in a couple of seconds. Confirm that it ran:
sudo systemctl is-active openjdk-firstboot.service cat /var/lib/sac/data/openjdk-toolchain.envThe manifest is not a secrets file, because this product has no secrets. It records what is installed and where:
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 JAVA_HOME_17=/usr/lib/jvm/java-17-openjdk-amd64 JDK_PRIMARY_VERSION=21.0.11+10-1~24.04.2 JDK_SECONDARY_VERSION=17.0.19+10-1~24.04.2 MAVEN_VERSION=3.9.16 GRADLE_VERSION=9.6.1 MAVEN_LOCAL_REPOSITORY=/srv/javadata/m2/repository GRADLE_USER_HOME=/srv/javadata/gradle WORKSPACE=/srv/javadata/workspace WORKSPACE_OWNER=azureuserVerifying the machine in one command
Before you trust the machine with your own code, run the self test. It is more than a version check. It writes a real multi-class Maven project, resolves a third-party dependency from Maven Central, runs a JUnit 5 suite and packages a runnable jar. It executes the jar and checks the values the program computed, builds the same sources again with Gradle, and proves cross-target compilation by reading the class file version back out of the emitted bytecode.
sac-java-selftestThe first run takes about ninety seconds on a Standard_B2s, because it downloads dependencies. Every line should read PASS, and the last line should be SELFTEST_OK.
Look at the last section of the output. The listener check uses a positive control. It first proves the probe can detect a live service by reading the real SSH-2.0- banner from port 22. Only then does it report that the same probe finds ports 8080, 8443 and 9000 refused. A refusal on its own would prove nothing, because a broken probe also fails to connect.
The toolchain
Check the installed versions:
java -version mvn -v gradle -vmvn and gradle are installed as wrappers in /usr/local/bin, which is on the default PATH. This means ssh yourvm 'mvn -v' works, and so does a systemd unit or a cron job, not only an interactive login shell.
The exact build that shipped in your image is recorded on the machine:
cat /usr/share/doc/sac-openjdk/BUILD-PINIf a security update has since raised the JDK version, that is working as intended. The recorded pin tells you what shipped, and apt tells you what you are running now.
Your first project
A complete, working example project ships inside the image at /usr/local/share/sac-java/selftest. It is the same project the self test builds: a small multi-class Maven and Gradle project with a real third-party dependency and a JUnit 5 suite. Copy it into your workspace and you have a project that already builds:
cd /srv/javadata/workspace rm -rf example cp -a /usr/local/share/sac-java/selftest example
cd example mvn -q package java -jar target/sac-java-selftest-1.0.0.jarExpected output. These are values the program computes, not strings it prints:
SELFTEST-COMPUTE PRIMES=17984 SUM=1709600813 SELFTEST-DEP SHA256=e05cd5dad95983cfe3d1ecbd2ef2b608a6af56c7f8cca140aadf9921c6cfbf80 SELFTEST-RUNTIME OpenJDK 64-Bit Server VM 21.0.11 classfile-target=uk.co.sac.selftest.MainThe second line is produced by Apache Commons Codec, a dependency Maven resolved from Maven Central into the cache on your data volume. Seeing it proves that dependency resolution, compilation, packaging and execution all worked end to end.
Targeting Java 17
Both LTS releases are installed, so you can compile for 17 while running the build on 21. The simplest way is the compiler release property. To confirm it worked, read the class file version out of the compiled bytecode rather than trusting the build log:
cd /srv/javadata/workspace/example mvn -q -Dmaven.compiler.release=17 clean package -DskipTests javap -v -cp target/classes uk.co.sac.selftest.Primes | grep 'major version'Expected output: 61 is the class file major version for Java 17, where Java 21 emits 65.
major version: 61Some multi-module builds need a whole separate JDK rather than a different bytecode target. Both JDKs are registered in a global Maven toolchains file at /etc/sac/maven-toolchains.xml, which the mvn wrapper passes to every build.
Declare maven-toolchains-plugin in your POM and select <version>17</version>. Maven then runs the compiler from the OpenJDK 17 installation itself.
Starting a project from scratch
To start your own project instead of copying the example, use this minimal project. It builds and runs against a real dependency:
sudo install -d -o "$(id -un)" -g "$(id -gn)" /srv/javadata/workspace/hello/src/main/java/com/example cd /srv/javadata/workspace/hello cat > pom.xml <<'XML' <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>hello</artifactId> <version>1.0.0</version> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.17.0</version> </dependency> </dependencies> </project> XML cat > src/main/java/com/example/Hello.java <<'JAVA' package com.example; import org.apache.commons.lang3.StringUtils; public class Hello { public static void main(String[] args) { String name = args.length > 0 ? args[0] : "world"; System.out.println(StringUtils.capitalize(name) + " from Java " + Runtime.version().feature()); } } JAVABuild and run the project. Maven fetches the dependency from Maven Central on the first build and caches it on the data volume:
cd /srv/javadata/workspace/hello mvn -q package LANG3="$(find /srv/javadata/m2/repository -name 'commons-lang3-3.17.0.jar' | head -1)" java -cp "target/classes:${LANG3}" com.example.Hello sacExpected output:
Sac from Java 21Building with Gradle
Gradle 9.6.1 is installed and uses the same JDK. GRADLE_USER_HOME already points at the data volume. First boot also symlinks the administrator's ~/.gradle there, which matters when a project ships its own ./gradlew wrapper, because that wrapper never sees the /usr/local/bin wrapper script.
gradle --versionWhere your work lives
Confirm the data volume mount, its size and the Maven local repository setting:
findmnt -no SOURCE,TARGET,FSTYPE,SIZE /srv/javadata df -h /srv/javadata grep '<localRepository>' /opt/apache-maven-3.9.16/conf/settings.xmlThe image redirects Maven in Maven's global settings.xml rather than through an environment variable. This is deliberate. The setting applies to every user, to non-interactive SSH commands and to a project's own ./mvnw wrapper, none of which reliably inherit a shell profile.
To grow the volume, resize the managed disk in Azure, then extend the filesystem:
sudo resize2fs /dev/disk/azure/scsi1/lun0Remote development with VS Code
This image works with VS Code Remote-SSH on first connect. Add the machine to your SSH config, connect, and install the Extension Pack for Java in the remote window.
The extension pack detects JAVA_HOME and finds mvn and gradle on the default PATH. It also downloads and manages its own language server inside ~/.vscode-server.
That is why no system-wide Java language server is installed here. The editor extension ships and updates its own copy, so a second system copy would never be used. The one commonly packaged for this purpose is licensed under the EPL, which would attach a source-availability obligation to the image for no functional benefit.
The extension needs only a JDK 21 or newer with JAVA_HOME set and build tools on the PATH. This image provides exactly that.
Licences and attack surface
The image contains a verbatim copy of every upstream licence, notice and legal file shipped by both JDKs, by Maven and by Gradle. This includes the whole of each JDK's legal/ tree and the GPL-2 body that the Ubuntu copyright file refers to. Inspect them, and the listening sockets, with these commands:
sed -n '1,20p' /usr/share/doc/sac-openjdk/licenses/NOTICE find /usr/share/doc/sac-openjdk/licenses -type f | wc -l grep -m1 'License:.*Classpath' /usr/share/doc/openjdk-21-jre-headless/copyright ss -HltnThe ss output is the whole attack surface: SSH on port 22, plus systemd-resolved's stub resolver bound to loopback addresses that nothing outside the machine can reach.
Dependency caches ship empty. The self test resolves around a hundred third-party jars when it runs. None of them are in the image you received, because they were purged before capture. This keeps SecureAnyCloud from redistributing artifacts it never licensed for redistribution. The first build on a fresh machine downloads its own dependencies, which is ordinary Java behaviour.
Keeping the machine patched
unattended-upgrades is enabled, so Ubuntu security updates, including OpenJDK updates, are applied automatically. To check for updates by hand:
sudo apt-get update apt list --upgradableNothing in this image is held or version-pinned, so a JDK security update installs normally. After a JDK upgrade, the build reported by java -version moves ahead of the value recorded in BUILD-PIN. That is the expected and desirable outcome.
Troubleshooting
mvn or gradle cannot download dependencies. The machine needs outbound HTTPS to repo.maven.apache.org and, for Gradle, to services.gradle.org. Check the network security group and any firewall or proxy on the path.
A build is slow or runs out of memory on Standard_B2s. Two vCPUs and 4 GiB are enough for the self test and for small to medium projects, but a large multi-module build needs more. Resize the VM rather than adding swap. This image deliberately ships with no swap, because Azure certification rejects an image with swap baked into the operating system disk. Azure manages swap on the ephemeral resource disk instead.
~/.m2/repository looks like a symlink. It is. First boot points it at the data volume, so tools that read the conventional path still land on the right storage.
A build wrote to the operating system disk instead of the data volume. Check findmnt -no TARGET -T "$(mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout)". It should print /srv/javadata. If a project overrides localRepository in its own settings.xml or passes -Dmaven.repo.local, that override wins.
The self test reports a broken check. Read the failing line, which names the stage that did not pass. The most common cause on a fresh machine is no outbound network, which shows up as the Maven stage not completing.
Support
SecureAnyCloud provides 24/7 support for this image.
Java is a registered trademark of Oracle and/or its affiliates, and OpenJDK is used here nominatively under Oracle's OpenJDK trademark policy. Gradle is a trademark of Gradle, Inc., and Apache and Apache Maven are trademarks of The Apache Software Foundation. SecureAnyCloud is not affiliated with, endorsed by, or sponsored by Oracle, Gradle, Inc., The Apache Software Foundation or Canonical Ltd.
Deploy on Azure
Launch SecureAnyCloud OpenJDK 21 Dev Kit on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com