Deployment guide

PlantUML Server 1.2026.9

Overview

PlantUML turns short text descriptions into diagrams. It supports sequence, class, activity, component, state, use case, deployment and object diagrams, plus mind maps, Gantt charts, JSON and YAML views, entity relationship diagrams and C4 architecture diagrams from its standard library.

PlantUML Server is its web front end. It gives you a browser editor with a live preview, and it renders any diagram from a URL (/png/<encoded>, /svg/, /pdf/, /txt/). Wikis, documentation sites, CI pipelines and chat tools can therefore show diagrams that are kept as text in your repositories.

This guide covers launching and using the PlantUML Server 1.2026.9 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud developer and API tools listing on AWS Marketplace. You choose it as the software version when you subscribe and launch.

The SecureAnyCloud image locks PlantUML Server down so that a fresh instance is never an open diagram renderer.

What you get:

  • HTTPS from the first boot. nginx serves PlantUML on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A password gate in front of everything. PlantUML Server has no login of its own. The whole site sits behind an HTTP Basic Auth prompt whose password is generated on your instance's first boot.
  • No default login. The image contains no password. First boot writes the gate's user name and a random 24-character password to /var/lib/sac/data/plantuml-server-credentials.txt, which only root can read. nginx stores only a SHA-512 crypt hash. Common guesses are refused, and nginx removes the password from every request before PlantUML sees it.
  • Diagrams cannot read the server or reach the network. PlantUML runs in its strictest security profile, SANDBOX. A diagram cannot include a local file or a URL (!include, !includeurl, !theme ... from), load JSON (%load_json), read the service's environment (%getenv) or embed an image from a URL. In addition, the PlantUML service can reach nothing but this instance's loopback address, so even a changed profile cannot fetch from your VPC or the internet.
  • The server-side fetch endpoints are off. Upstream PlantUML Server can fetch a diagram from any URL a visitor names (/proxy, /metadata?src= and a metadata= argument on its editor pages). nginx refuses all of them, so the instance cannot be used to probe your network.
  • Other web sites cannot act with your login. A browser re-sends the gate password to any site that asks. For this reason, nginx refuses every state-changing request (POST and the like) and every WebSocket upgrade whose browser Origin is not this instance. Only the editor's own two POST calls are allowed at all.
  • A strict Content-Security-Policy. The editor may run only scripts served by this instance. It allows no inline script, no eval and no framing by other sites. The only inline code allowed is the editor's own seven button handlers, matched by exact hash.
  • The application cannot reach the instance metadata service. nginx and PlantUML are blocked at the kernel level from the EC2 instance metadata service (IPv4 and IPv6).
  • PlantUML runs locked in. Jetty listens on 127.0.0.1:8080 only, as its own unprivileged user. It has a read-only file system apart from a private temporary directory, a memory cap, a task cap and 64 worker threads. It never sees the client's forwarding headers, because nginx passes the real client address from the socket.
  • Current, pinned releases. The official PlantUML Server 1.2026.9 release (checksum pinned, its bundled engine checked against Maven Central) runs on Eclipse Jetty 12.1.14, which carries the 2026 Jetty request smuggling and denial of service fixes, and OpenJDK 21 from Ubuntu.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: PlantUML Server 1.2026.9 (/var/lib/plantuml/webapps/ROOT.war), Eclipse Jetty 12.1.14 (/opt/jetty), OpenJDK 21, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. Settings are in /etc/plantuml/.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet
  • A security group that allows TCP 22 (SSH), TCP 443 (PlantUML Server) and TCP 80 (redirects to HTTPS). Open these ports to your own networks only.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick PlantUML Server 1.2026.9 on Ubuntu 24.04 as the software version, choose your Region, then choose Launch through EC2.

Choose m5.large, your key pair, a security group as described above and a subnet.

To launch with the AWS CLI instead, replace the values in angle brackets:

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=plantuml-server}]'
2

Connect to your instance

Version SSH user

PlantUML Server 1.2026.9 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot sets the host name and certificate and creates the gate password. Then PlantUML and nginx start. Jetty unpacks PlantUML on start, which takes up to a minute on a new instance:

systemctl is-active plantuml nginx plantuml-server-firstboot
sudo test -f /var/lib/sac/data/plantuml-server-firstboot.done && echo FIRSTBOOT_DONE

All three units are active and the first boot marker is present:

active active active FIRSTBOOT_DONE

Only SSH and nginx listen on the network. PlantUML (Java, in Jetty) listens on the loopback address only:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx")) [::ffff:127.0.0.1]:8080 users:(("java"))

The built-in self-test checks the whole path through the gate. It covers HTTPS, the password prompt, the editor, PNG, SVG and text rendering, the sandbox and the request guards:

sudo plantuml-server-selftest
SELFTEST-OK https 301 + healthz + gate 401 + 6 wrong gate credentials refused + editor + PNG + SVG (Smetana) + text + SANDBOX (file include refused, %getenv empty) + fetch endpoints off + 403 cross-origin POST + /coder + 415 compressed body + 444 unknown host + preflight
4

Get your URL and password

sudo cat /var/lib/sac/data/plantuml-server-credentials.txt

Only root can read this file. It holds the URL and the gate's user name and password for this instance:

# PlantUML Server 1.2026.9 - generated on first boot by plantuml-server-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret. PLANTUML_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ PLANTUML_USERNAME=plantuml PLANTUML_PASSWORD=<PLANTUML_PASSWORD>
#
# Open PLANTUML_URL in a browser and sign in with PLANTUML_USERNAME / PLANTUML_PASSWORD (self-signed certificate minted
# on this instance's first boot; port 80 redirects to https). Render URLs: https://<host>/png/<encoded>, /svg/, /txt/. # Diagrams run in PlantUML's SANDBOX profile: no local file, URL, environment or JSON access from a diagram.
# Self-test:
sudo plantuml-server-selftest
# Change the served name:
sudo plantuml-server-set-hostname <your-domain>
# User guide: https://secureanycloud.com/stacks/plantuml-server-1-2026-9/

The URL uses the instance's public DNS name from EC2 instance metadata. Opening https://<public-ip>/ redirects you to that name.

The certificate is self-signed, so your browser asks you to accept it the first time. Step 9 shows how to use your own.

5

Draw in the browser editor

Open the URL from Step 4, accept the certificate and sign in with the user name and password. The editor opens with a small example.

Type PlantUML on the left. The preview on the right redraws a moment after you stop typing. The address bar above the editor always holds the link to the diagram as you see it.

The View as icons open the diagram as PNG, SVG, ASCII art or PDF. The settings icon (the cog) chooses the preview format, a colour theme and the editor options. Here the preview is set to SVG for a class diagram:

Component and deployment diagrams work the same way. This one shows how this instance is put together:

6

Render diagrams from the command line

Every diagram has a short encoded form. Send the source to /coder to get it, then fetch /png/<encoded>, /svg/<encoded>, /pdf/<encoded> or /txt/<encoded>. The examples below run on the instance and reach nginx through the loopback address. From your own machine, use the URL from Step 4 and -u plantuml, and curl then asks for the password.

The examples keep the password out of the command line by passing it to curl through a pipe:

H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/plantuml-server-credentials.txt) gate() {
sudo awk -F= '/^PLANTUML_USERNAME=/{u=$2} /^PLANTUML_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/sac/data/plantuml-server-credentials.txt; } cat > order.puml <<'PUML' @startuml actor Customer participant "Web shop" as Shop database Orders Customer -> Shop : place order Shop -> Orders : INSERT order Orders --> Shop : order id Shop --> Customer : confirmation @enduml PUML
ENC=$(curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" --data-binary @order.puml "https://$H/coder") echo "$ENC" curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o order.png -w '%{http_code} %{content_type}\n' "https://$H/png/$ENC" curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o order.svg -w '%{http_code} %{content_type}\n' "https://$H/svg/$ENC" file order.png
HOqn3i8m34NtdEAhUo_GWGKni814aPZT98X8D8viz_w4462ptljF9cyis6zgl4PnXSIIJuMoORZ51VhWCe0KSprS8AE5DE8i8OgwRuTnrmQOK9xa8_ZjNKDLVTPL7a_puNBjjjFntpQE57fOnU_11C_vdcGbIvnV 200 image/png 200 image/svg+xml order.png: PNG image data, 300 x 307, 8-bit colormap, non-interlaced

The same encoded string works in the browser, where https://<your-host>/uml/<encoded> opens the diagram in the editor. It also works in any page that can show an image, as long as the viewer has signed in to the gate. The text renderer is handy in terminals and code reviews:

H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/plantuml-server-credentials.txt) gate() {
sudo awk -F= '/^PLANTUML_USERNAME=/{u=$2} /^PLANTUML_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/sac/data/plantuml-server-credentials.txt; } curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" --data-binary @order.puml "https://$H/coder" | xargs -I{} curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/txt/{}"
┌────────┐ ┌────┐ ┌──────┐ │Customer│ │Shop│ │Orders│ └────┬───┘ └──┬─┘ └───┬──┘ │ place order │ │ │──────────────>│ │ │ │ │ │ │ INSERT order │ │ │──────────────>│ │ │ │ │ │ order id │ │ │< ─ ─ ─ ─ ─ ─ ─│ │ │ │ │ confirmation │ │ │< ─ ─ ─ ─ ─ ─ ─│ │ │ │ │ ┌────┴───┐ ┌──┴─┐ ┌───┴──┐ │Customer│ │Shop│ │Orders│ └────────┘ └────┘ └──────┘

PlantUML's built-in Smetana engine lays out class, component, state and activity diagrams with no extra software. See Step 8:

H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/plantuml-server-credentials.txt) gate() {
sudo awk -F= '/^PLANTUML_USERNAME=/{u=$2} /^PLANTUML_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/sac/data/plantuml-server-credentials.txt; } cat > classes.puml <<'PUML' @startuml class Customer { +id: int +email: String } class Order { +id: int +total: Decimal } Customer "1" -- "*" Order : places @enduml PUML curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" --data-binary @classes.puml "https://$H/coder" | xargs -I{} curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o classes.svg -w '%{http_code} %{content_type}\n' "https://$H/svg/{}" grep -o '>Customer<\|>Order<\|>places<' classes.svg | sort -u
200 image/svg+xml >Customer< >Order< >places<

Remove the example files when you are done:

rm -f order.puml order.png order.svg classes.puml classes.svg
7

The diagram sandbox

PlantUML's settings are in /etc/plantuml/plantuml.env, and every diagram is laid out by Smetana through /etc/plantuml/config.puml:

grep -v '^#' /etc/plantuml/plantuml.env | grep . cat /etc/plantuml/config.puml
PLANTUML_SECURITY_PROFILE=SANDBOX PLANTUML_CONFIG_FILE=/etc/plantuml/config.puml PLANTUML_LIMIT_SIZE=4096 PLANTUML_STATS=off !pragma layout smetana

SANDBOX is PlantUML's strictest profile. A diagram that tries to read a file is refused. PlantUML answers 400 and names the refused include in its X-PlantUML-Diagram-Error header. The %getenv function returns nothing:

H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/plantuml-server-credentials.txt) gate() {
sudo awk -F= '/^PLANTUML_USERNAME=/{u=$2} /^PLANTUML_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/sac/data/plantuml-server-credentials.txt; } printf '@startuml\n!include /etc/passwd\nAlice -> Bob\n@enduml\n' > probe.puml curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" --data-binary @probe.puml "https://$H/coder" | xargs -I{} curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -D - "https://$H/png/{}" | tr -d '\r' | awk -F': ' '/^HTTP/ {print} tolower($1) == "x-plantuml-diagram-error" {print "refused by the sandbox:", $2}' printf '@startuml\nAlice -> Bob : HOME=[%%getenv("HOME")]\n@enduml\n' > env.puml curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" --data-binary @env.puml "https://$H/coder" | xargs -I{} curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/txt/{}" | grep -o 'HOME=\[[^]]*\]' rm -f probe.puml env.puml
HTTP/2 400 refused by the sandbox: cannot include /etc/passwd HOME=[]

The standard library still works, because it is built into PlantUML. The !include <C4/C4_Container> and !include <awslib/...> forms, and the other <...> libraries, need no file or network access. What a diagram cannot do is pull in a file or a URL.

Put shared styles and macros into the diagram text itself, or into /etc/plantuml/config.puml, which PlantUML adds to the top of every diagram. After you change that file, restart PlantUML with sudo systemctl restart plantuml.

/usr/local/sbin/plantuml-preflight checks the settings each time PlantUML starts. It refuses to start PlantUML with a weaker profile, because INTERNET, LEGACY and UNSECURE would let every person who can open the editor read files from this instance or make it fetch URLs.

Opt-in: allow named URLs (ALLOWLIST). If your diagrams must include files from a fixed set of URLs, such as a shared style sheet on your Git server, switch to the ALLOWLIST profile. It allows only the URL prefixes you list.

Every person who can sign in to the gate can then make this instance fetch those URLs. List only addresses whose content is safe to show to all of them, never an internal admin endpoint, and keep the list short. Make three changes as root:

  • Create /etc/plantuml/allowlist.properties with one line that lists the URL prefixes separated by semicolons, for example plantuml.allowlist.url=https://git.example.com/styles/.
  • In /etc/plantuml/plantuml.env, set PLANTUML_SECURITY_PROFILE=ALLOWLIST and add PLANTUML_PROPERTY_FILE=/etc/plantuml/allowlist.properties.
  • Let PlantUML out of the loopback address while keeping the metadata block in place. Create /etc/systemd/system/plantuml.service.d/60-allowlist-egress.conf with this content:
[Service] IPAddressAllow= IPAddressDeny= IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

Then run sudo systemctl daemon-reload && sudo systemctl restart plantuml. To go back, delete the drop-in and the property file, and set SANDBOX again. The security group and any VPC firewall still apply to what PlantUML can reach.

8

Layout engine

Diagrams that need an automatic layout (class, component, use case, state, object and deployment diagrams) are laid out by Smetana, PlantUML's built-in Java port of the Graphviz dot layout. No separate layout program runs, because everything happens inside the PlantUML service. Graphviz is not installed, because Ubuntu publishes it only in its community-maintained universe archive, which does not get standard Ubuntu security updates.

Smetana handles most diagrams the same way Graphviz does. If one of your diagrams relies on a Graphviz-only feature, you can install Graphviz yourself. This means running a package from universe, and security fixes for it come with Ubuntu Pro.

To install Graphviz, run sudo apt-get install graphviz, remove the !pragma layout smetana line from /etc/plantuml/config.puml, and restart PlantUML. The image's ProtectSystem and memory limits then also apply to the dot process that PlantUML starts.

9

Publish on your own domain

Point a DNS record for your domain at the instance. An Elastic IP keeps the address stable. Then give PlantUML Server the name. The helper mints a new certificate for it, pins nginx and the cross-site guard to it, and redirects the instance's IP addresses there:

sudo plantuml-server-set-hostname <your-domain>

For a certificate browsers trust, replace /etc/ssl/plantuml-server/plantuml-server.crt and /etc/ssl/plantuml-server/plantuml-server.key with your own, for example from Let's Encrypt or your certificate authority. Then run sudo systemctl reload nginx.

Opt-in: public rendering for embedding. A wiki or documentation site that shows diagrams to readers who never sign in needs the image endpoints without the gate. This turns the instance into a public renderer. Anyone who finds it can make it render diagrams, which costs CPU and memory. The limits in Step 10 still apply, and the sandbox still holds.

If you need this, restrict the security group to the networks your readers come from, and open only the image formats. Add this block to the PlantUML server on port 443 in /etc/nginx/sites-available/sac-plantuml, before location / {:

# sac opt-in: public PNG and SVG rendering (no gate); everything else keeps the password prompt location ~ ^/(png|svg)/[0-9A-Za-z_~-]+$ { include /etc/nginx/plantuml-proxy.conf; }

Then run sudo nginx -t && sudo systemctl reload nginx. Remove the block to close it again.

10

Security notes

nginx and PlantUML are blocked from the instance metadata service. PlantUML may talk to nothing but the loopback address:

for u in nginx plantuml; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | LC_ALL=C sort | tr '\n' ' '; echo; done printf 'plantuml allow: '; systemctl show -p IPAddressAllow --value plantuml | tr ' ' '\n' | LC_ALL=C sort | tr '\n' ' '; echo
nginx 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128 plantuml 0.0.0.0/0 ::/0 plantuml allow: 127.0.0.0/8 ::1/128

Run these requests to see the guards at work. The gate answers before PlantUML does, and the upstream fetch endpoints are refused. A state-changing request that another web site's page sends is refused, and so is a request body with a Content-Encoding. A request for a host name the instance does not serve gets no answer at all:

H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/plantuml-server-credentials.txt) gate() {
sudo awk -F= '/^PLANTUML_USERNAME=/{u=$2} /^PLANTUML_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/sac/data/plantuml-server-credentials.txt; } curl -sk -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/uml/" curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/proxy?src=https://example.com/diagram.puml" curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/uml/?metadata=https://example.com/diagram.png" curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Origin: https://evil.example' --data 'x=1' "https://$H/coder" printf 'x' | curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Encoding: gzip' --data-binary @- "https://$H/coder" curl -sk -o /dev/null -w '%{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
401 404 400 403 415 000

PlantUML runs as its own user with a read-only file system:

systemctl show -p User -p ProtectSystem -p PrivateTmp -p TasksMax plantuml
TasksMax=256 User=plantuml PrivateTmp=yes ProtectSystem=strict

Other notes:

  • Resource limits. Diagrams are capped at 4096 pixels per side (PLANTUML_LIMIT_SIZE), request bodies at 4 MiB, and each client at 32 connections and 20 requests a second. PlantUML is capped at 64 rendering threads, 75% of the instance's memory and 256 tasks. nginx gives up on a render after 60 seconds, so split a very large diagram into several. Choose a bigger instance type for heavy rendering.
  • What PlantUML can write. PlantUML can write only to its private temporary directory, which systemd empties every time the service stops. The credentials file, the TLS key and the nginx configuration are invisible to it.
  • The editor's components. The browser editor bundles the Monaco editor 0.36.1, which contains an old DOMPurify. Monaco uses it only on its own help text, never on your diagrams. The Content-Security-Policy blocks any script the page did not load itself. SecureAnyCloud re-checks it with every PlantUML Server release.
  • The gate password never reaches PlantUML. nginx removes it, and the browser's cookies, from every request.
  • Logs. nginx logs are in /var/log/nginx, with the real client address. Jetty and PlantUML log to the journal (journalctl -u plantuml).
11

Keep it up to date

Ubuntu security updates, including OpenJDK, install automatically. To apply all updates now:

sudo apt-get update && sudo apt-get upgrade -y && sudo systemctl restart plantuml

New PlantUML Server and Jetty releases arrive as new versions of this listing, built and checked the same way. PlantUML Server keeps no data on the instance, because your diagrams live wherever you keep their text. Moving to a new version means launching it and pointing your DNS name at it.

Copy /etc/plantuml/ across if you changed it. Read the PlantUML Server release notes before you move.

Support and licensing

PlantUML Server and the PlantUML engine are open source under the GNU General Public License, version 3 or later (Arnaud Roques and the PlantUML contributors). The source code is at github.com/plantuml/plantuml-server and github.com/plantuml/plantuml.

The image runs the unmodified official release. /usr/share/doc/sac-plantuml-server/licences/ holds the source offer and the Eclipse Jetty licence (EPL 2.0 or Apache 2.0). Diagrams you generate belong to you and are not covered by the GPL.

There is no per-user licence fee. The AMI price covers SecureAnyCloud's build, hardening, first-boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

The other versions on this listing are on the developer and API tools guide.

Deploy on AWS

Launch PlantUML Server 1.2026.9 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS