OpenAPI Generator on Ubuntu 24.04 on Azure User Guide
Overview
OpenAPI Generator turns an OpenAPI specification into working code. Give it your API description and it returns a client SDK, a server stub or a documentation set, ready to unpack and use, across more than 150 language and framework targets. This image is the service form of it — the openapi-generator-online module: a REST API your build pipeline can call, and an interactive console in the browser for the times you just want a client for something right now.
Running it yourself matters because an internal API description is design documentation for your system. A private generator keeps it inside your own network while still giving every team the same generated clients, from the same generator version, in every pipeline.
What is included:
- OpenAPI Generator Online 7.25.0, the unmodified upstream artefact from Maven Central at
/opt/openapi-generator/, pinned by version and by the SHA-256 of the jar, run by systemd as the unprivilegedoagenservice account - nginx terminating TLS on port
443and requiring an HTTP basic credential on every request. This is load-bearing rather than decorative: the upstream service has no authentication of any kind — it is published as an open demo — so the front door is the entire access-control story, and it has no exemptions - No default credential. A unique password is generated on the first boot of your VM, stored only as a salted SHA-512 hash, and written to a file only
rootcan read - The generator bound to
127.0.0.1:8080, where nothing outside the instance can reach it. Off-box TCP is exactly22and443; there is deliberately no port 80 - Generated output written to
/var/lib/openapi-generator/work, a directory the service account owns and the service's own systemd sandbox confines it to - A bounded cleanup on an hourly timer, so a long-running instance cannot fill its disk however heavily it is used
- Confined specification fetching: generating from a URL keeps working, but the service cannot reach the Azure instance metadata service, the Azure wire server or anything on your private network while doing it
- OpenJDK 21 (headless JRE) from Ubuntu
main, so the JVM is patched by Ubuntu security like the rest of the image. No build toolchain ships:javac,mvn,gradleandjarare all absent - On-VM self tests that prove the whole posture, each one checked against a known-bad input first
- The
openapi-generatorandnginxsystemd services, enabled and active, plus theopenapi-generator-reap.timerhousekeeping timer
OpenAPI is a trademark of the Linux Foundation. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the Apache License 2.0.

Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) is the recommended size: one JVM with a 65% heap ceiling and nginx sit comfortably inside it, and generating a large SDK is a few seconds of CPU rather than a memory problem. Choose a larger size if many pipelines will generate concurrently.
Network security group inbound rules: 22/tcp from your management network, and 443/tcp from wherever your developers and build agents connect. There is deliberately no port 80 — the only credential this service has is an HTTP basic credential, and a plaintext port is a port that leaks it.
Step 1: Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for OpenAPI Generator by cloudimg and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22). Then select Review + create and Create. Open 443 afterwards, as shown in the next step.
Step 2: Deploy from the Azure CLI
Accept the image terms once per subscription, then deploy:
az vm image terms accept --urn cloudimg:openapi-generator-ubuntu-24-04:default:latest
az group create --name openapi-generator-rg --location eastus
az vm create \
--resource-group openapi-generator-rg \
--name openapi-generator-vm \
--image cloudimg:openapi-generator-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group openapi-generator-rg --name openapi-generator-vm --port 443 --priority 1001
Restrict 443 to the networks your developers and build agents actually come from rather than leaving it open to the internet. The credential gate below is what protects the service, but a smaller exposed surface is still a smaller exposed surface.
Step 3: Retrieve this instance's credential
The image ships no usable credential. On the first boot of your VM a unique password is generated for that instance alone and written to /root/openapi-generator-credentials.txt, readable only by root:
sudo cat /root/openapi-generator-credentials.txt
The values you need:
| Key | What it is |
|---|---|
OAG_URL |
the console address for this VM |
OAG_HOST |
this VM's customer-facing address, resolved at first boot |
OAG_AUTH_USER |
the front-door username (apiuser) |
OAG_AUTH_PASSWORD |
the front-door password, unique to this VM |
OAG_TLS_CERT |
the certificate this VM serves, so on-VM tooling can verify it |
The password is never stored in clear anywhere else. nginx checks it against a salted SHA-512 hash in /etc/openapi-generator/htpasswd, which is 0640 root:www-data:
sudo bash -c 'ls -l /etc/openapi-generator/htpasswd; grep -oE "^[^:]+:\\\$6\\\$" /etc/openapi-generator/htpasswd'

Step 4: Open the console
Browse to https://<OAG_HOST>/ and sign in with OAG_AUTH_USER and OAG_AUTH_PASSWORD when the browser asks. The image ships a certificate generated for your VM's own address; because it is self-signed your browser will warn once, and Step 10 shows how to install your own.
The console lists every operation the service offers, grouped into clients and servers, with the API server set to your own instance:

Expand GET /api/gen/clients and press TRY to see the full catalogue of client and documentation generators this version supports:

From the VM itself, the same call over the API:
sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/clients" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"client generators\"); print(\", \".join(d[:12]), \"...\")"'
The
--resolveflag is there because an Azure VM cannot dial its own public IP: the Standard Load Balancer does not hairpin, so a request from the VM to its own public address times out.--resolvekeeps your real address in the URL and in the TLS check while sending the connection to loopback. From your laptop you just use the URL.
Step 5: Generate a client
This is what the product is for. Post an OpenAPI specification and the target language; you get back a one-time download link.
In the console, expand POST /api/gen/clients/{language}, set language to python, paste your specification into the body as {"spec": { ... }} and press TRY:

From the API, end to end — generate, download, and look at what came back:
sudo bash -c 'set -e
CF=/root/openapi-generator-credentials.txt
CRT=/etc/ssl/openapi-generator/openapi-generator.crt
H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-)
U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-)
P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-)
T=$(mktemp -d)
/usr/local/sbin/openapi-generator-write-spec.sh $T/spec.json
python3 -c "import json;json.dump({\"spec\":json.load(open(\"$T/spec.json\")),\"options\":{\"packageName\":\"acme_client\"}},open(\"$T/body.json\",\"w\"))"
CODE=$(curl -s -m 600 --cacert $CRT --resolve "$H:443:127.0.0.1" -u "$U:$P" -X POST -H "Content-Type: application/json" --data @$T/body.json "https://$H/api/gen/clients/python" | python3 -c "import json,sys;print(json.load(sys.stdin)[\"code\"])")
echo "download id: $CODE"
curl -s -m 600 --cacert $CRT --resolve "$H:443:127.0.0.1" -u "$U:$P" -o $T/client.zip -w "download: HTTP %{http_code} %{size_download} bytes\n" "https://$H/api/gen/download/$CODE"
unzip -l $T/client.zip | tail -n +4 | head -8
rm -rf $T'
Three things about that download link are worth knowing:
- It is one-time. The archive is deleted as it is read, so a second request for the same link fails. Generate again for another copy.
- It is built from this instance's own address, taken from
GENERATOR_HOSTin/etc/openapi-generator/openapi-generator.env. It is deliberately not built from the incoming request: left to the request, any caller could choose the host their download link points at by setting a forwarded-host header. - It expires. Upstream holds generated archives for 24 hours; the housekeeping in Step 11 clears what is left after that.

To see what a particular generator can be tuned with, ask it. Every generator has its own option set — package names, library choices, serialisation, and so on — and the console shows them under GET /api/gen/clients/{language}:

sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/clients/typescript-axios" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"options:\"); print(\", \".join(sorted(d)[:10]), \"...\")"'
Pass any of them in the options object alongside spec.
Step 6: Generate a server stub
The same call, against /api/gen/servers/{framework}. Ask the instance which frameworks it supports:
sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/servers" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"server generators\"); print(\", \".join(d[:12]), \"...\")"'
Step 7: Generating from a specification URL, and what this image will fetch
Instead of spec, you can send openAPIUrl and the service will fetch the specification itself. That is genuinely useful — generating a client for a published API is a one-liner — but it means the service makes outbound requests to addresses a caller chooses.
This image therefore confines where those requests can go. The service's systemd unit carries an IP egress filter that denies the Azure instance metadata service (169.254.0.0/16), the Azure wire server (168.63.129.16) and every RFC1918 private range, while leaving public addresses and DNS alone. Public specification URLs keep working; a caller cannot turn the generator into a probe for your instance's identity tokens or your private network.
You can confirm it, with its own control:
sudo /usr/local/sbin/openapi-generator-egress-check.sh
That check reports what happens inside the filter and, as a control, what happens without it — because a metadata service that is unreachable for some unrelated reason would otherwise make the filter look like it was working when it was not.
If your specifications live on an internal host you do want the generator to reach, add that host's address to IPAddressAllow= in a drop-in rather than removing the deny list:
sudo bash -c 'systemctl cat openapi-generator.service | grep -E "^IPAddress"'
Step 8: Verify the security posture yourself
Everything this image claims about its front door is executable. The credential gate:
sudo /usr/local/sbin/openapi-generator-verify-auth.sh
It asserts that an unauthenticated request is refused 401 with a WWW-Authenticate challenge on the API, the console, the API document, the download endpoint and the static assets; that a wrong password, a blank password, a wrong username, well-known weak guesses and a malformed authorisation header are all refused; that this VM's own credential is accepted on a browser-shaped request and really returns a populated generator catalogue; that the generator's own port does not answer on the VM's external address and nothing answers on port 80; that TLS verification refuses an address the certificate does not name; and that a hostile forwarded-host header cannot move a download link.
Every one of those checks is itself tested against a known-bad input, because a gate that cannot fail proves nothing:
sudo /usr/local/sbin/openapi-generator-gate-selftest.sh
And the exact off-box listening set:
sudo /usr/local/sbin/openapi-generator-port-check.sh

The whole appliance in one command, including a real generate-and-download round trip whose archive is checked file by file:
sudo /usr/local/sbin/openapi-generator-selftest.sh
Step 9: Rotate the front-door password
Use the supplied helper. It reads the new password from standard input rather than from the command line, so it never lands in the process table, the journal or your shell history, and it refuses empty, short, all-digit, colon-bearing and well-known weak passwords. It also proves the rotation took before it returns: the new password must work and the old one must not.
printf '%s' '<new-password>' | sudo /usr/local/sbin/openapi-generator-set-password.sh
It rewrites both the hash nginx checks and the credentials file, so the two cannot drift apart. nginx re-reads the file per request — no restart, no dropped connections.
Step 10: Use your own certificate and hostname
Put your certificate and key at /etc/ssl/openapi-generator/openapi-generator.crt and .key (key 0600 root:root), then tell the instance the address it should advertise in download links, and restart:
sudo bash -c 'echo "GENERATOR_HOST=https://<your-domain>" > /etc/openapi-generator/openapi-generator.env; systemctl restart openapi-generator nginx'
GENERATOR_HOST is what every download link is built from, so change it in the same breath as the certificate. If you leave it pointing at the old address your clients will still generate but their download links will name the wrong host.
Step 11: Generated output and disk housekeeping
Generated archives live under /var/lib/openapi-generator/work and are removed as they are downloaded. Two things then keep the disk bounded:
- Upstream expires an archive's download link after 24 hours and sweeps the expired ones hourly.
- This image adds its own hourly reaper for what that sweep structurally cannot see: a generation that failed leaves a working directory behind with no link to expire. The reaper removes anything older than 25 hours — deliberately past upstream's own window, so it can never destroy a link that is still valid — and enforces a 2 GB ceiling, pruning oldest-first when it is exceeded.
sudo bash -c 'systemctl list-timers openapi-generator-reap.timer --no-pager | head -3; du -sh /var/lib/openapi-generator/work'
Run it by hand at any time, and check the instance is holding nothing:
sudo /usr/local/sbin/openapi-generator-reap.sh
sudo /usr/local/sbin/openapi-generator-workdir-empty.sh
To change the ceiling, edit ExecStart= in /etc/systemd/system/openapi-generator-reap.service and add --max-mb <N>.
Step 12: Upgrading
The generator is a single pinned jar, so an upgrade is a download, a checksum and a restart. Check what you are running:
sudo bash -c 'ls -1 /opt/openapi-generator/*.jar; unzip -p /opt/openapi-generator/openapi-generator-online-7.25.0.jar META-INF/MANIFEST.MF | grep -i "^implementation-version"'
New releases are published to Maven Central under org.openapitools:openapi-generator-online; https://repo1.maven.org/maven2/org/openapitools/openapi-generator-online/maven-metadata.xml is upstream's own statement of the current line. Note that the project's GitHub releases attach only the command-line jar, so the online service's artefact is on Maven Central and nowhere else.
To upgrade, download the new jar, verify its checksum against the .sha1 published beside it, place it in /opt/openapi-generator/, point ExecStart= at it in /etc/systemd/system/openapi-generator.service, then systemctl daemon-reload && systemctl restart openapi-generator. Re-run openapi-generator-selftest.sh afterwards.
One upgrade note specific to this product. The published
openapi-generator-onlinejar bundlescommons-lang33.12.0, because its build inherits Spring Boot 2.5's dependency management, while the generator core calls a class that only exists from 3.17.0. Without a correction, every Python-family generator fails with HTTP 500. This image launches the unmodified jar through Spring Boot's ownPropertiesLauncherwith-Dloader.path=/opt/openapi-generator/ext, which puts the correctcommons-lang3ahead of the bundled one. Keep that flag when you upgrade, and check whether the new release still needs it:
sudo bash -c 'unzip -l /opt/openapi-generator/openapi-generator-online-7.25.0.jar | grep -o "commons-lang3-[0-9.]*jar"; ls -1 /opt/openapi-generator/ext/'
Troubleshooting
The console loads but shows nothing. The console is driven entirely by the API document at /api-docs, which becomes available a few seconds after the generation API does. The service's own readiness gate waits for both before nginx is allowed to start, so this should not happen; if it does, check the service came up cleanly:
sudo bash -c 'systemctl is-active openapi-generator nginx; journalctl -u openapi-generator -n 20 --no-pager | tail -5'
A generation returns HTTP 400. The specification was not accepted. The most common cause is an anonymous inline schema where a generator expects a named one — move the schema into components/schemas and reference it. A missing spec and a missing openAPIUrl also give 400, as does an unknown generator name.
A download returns HTTP 500 on the second try. That is expected: archives are one-time. A link that was never issued returns 404.
Everything returns 401. Check the credential you are sending against /root/openapi-generator-credentials.txt. Remember there are no exempt paths — the console, the API document and the static assets are all behind the same credential.
The service will not start. It refuses to start if /etc/openapi-generator/openapi-generator.env is missing; that is deliberate, so it can never run with download links built from whatever a caller asks for. Restore the file with a GENERATOR_HOST=https://<address> line.
sudo bash -c 'systemctl is-enabled openapi-generator openapi-generator-firstboot nginx openapi-generator-reap.timer | tr "\n" " "; echo'
Support
This image is a repackaged open source product with 24/7 cloudimg support for deployment, upgrades, TLS certificates and custom domains, pipeline and CI integration, generator configuration and templates, and scaling. Contact support through the Azure Marketplace listing or at cloudimg.co.uk.