PlantUML Server 1.2026.9 on AWS User Guide
Overview
This guide covers launching and using the PlantUML Server 1.2026.9 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
PlantUML turns short text descriptions into diagrams: 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/), so wikis, documentation sites, CI pipelines and chat tools can show diagrams that are kept as text in your repositories.
The cloudimg image locks PlantUML Server down:
- 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, so a fresh instance is never an open diagram renderer on the internet.
- 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/cloudimg/plantuml-server-credentials.txt(root only); 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. On top of that 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 ametadata=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, so nginx refuses every state changing request (POST and the like) and every WebSocket upgrade whose browser
Originis not this instance, and 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; no inline script, no
eval, no framing by other sites. The only inline code allowed is the editor's own seven button handlers, 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:8080only, as its own unprivileged user, with 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: 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 them to your own networks only.
Step 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 and your Region, then Launch through EC2. Choose m5.large, your key pair, a security group as above and a subnet.
With the AWS CLI (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}]'
Step 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>
Step 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/cloudimg/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: 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
Step 4: Get your URL and password
sudo cat /var/lib/cloudimg/plantuml-server-credentials.txt
The file is readable by root only and 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://www.cloudimg.co.uk/guides/plantuml-server-1-2026-9-on-ubuntu-24-04-aws/
The URL uses the instance's public DNS name from EC2 instance metadata. Opening https://<public-ip>/ sends you on 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).
Step 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, and 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:


Step 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 (curl then asks for the password). They 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/cloudimg/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/cloudimg/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 (https://<your-host>/uml/<encoded> opens it in the editor) and 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/cloudimg/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/cloudimg/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│
└────────┘ └────┘ └──────┘
Class, component, state and activity diagrams are laid out by PlantUML's built-in Smetana engine (see Step 8), with no extra software:
H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/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/cloudimg/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
Step 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), and %getenv returns nothing:
H=$(sudo sed -n 's#^PLANTUML_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/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/cloudimg/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: !include <C4/C4_Container>, !include <awslib/...> 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 (restart with sudo systemctl restart plantuml after changing it).
/usr/local/sbin/plantuml-preflight checks the settings each time PlantUML starts and refuses to start it with a weaker profile: 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 (a shared style sheet on your Git server, for example), switch to the ALLOWLIST profile, which allows only the URL prefixes you list. Every person who can sign in to the gate can then make this instance fetch those URLs, so list only addresses whose content is safe to show to all of them, never an internal admin endpoint, and keep the list short. Three changes, as root:
- Create
/etc/plantuml/allowlist.propertieswith one line, the URL prefixes separated by semicolons, for exampleplantuml.allowlist.url=https://git.example.com/styles/. - In
/etc/plantuml/plantuml.envsetPLANTUML_SECURITY_PROFILE=ALLOWLISTand addPLANTUML_PROPERTY_FILE=/etc/plantuml/allowlist.properties. - Let PlantUML out of the loopback address, keeping the metadata block in place: create
/etc/systemd/system/plantuml.service.d/60-allowlist-egress.confcontaining
[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.
Step 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: 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 yours relies on a Graphviz-only feature and you accept running a package from universe (security fixes for it come with Ubuntu Pro), you can install Graphviz yourself with sudo apt-get install graphviz and remove the !pragma layout smetana line from /etc/plantuml/config.puml; then restart PlantUML. Note that the image's ProtectSystem and memory limits then also apply to the dot process PlantUML starts.
Step 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) and 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. That 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 it, 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/cloudimg-plantuml, before location / {:
# cloudimg 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.
Step 10: Security notes
nginx and PlantUML are blocked from the instance metadata service, and 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
The gate answers before PlantUML does, the upstream fetch endpoints are refused, a state changing request that another web site's page sends is refused, a request body with a Content-Encoding is refused, and 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/cloudimg/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/cloudimg/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, each client at 32 connections and 20 requests a second, and PlantUML at 64 rendering threads, 75% of the instance's memory and 256 tasks. nginx gives up on a render after 60 seconds; a very large diagram is better split into several. Choose a bigger instance type for heavy rendering. - What PlantUML can write. Only 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, and the Content-Security-Policy blocks any script the page did not load itself. cloudimg 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).
Step 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: your diagrams live wherever you keep their text, so moving to a new version is launching it and pointing your DNS name at it. Copy /etc/plantuml/ across if you changed it. Read the PlantUML Server releases 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, and /usr/share/doc/cloudimg-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 cloudimg's build, hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA
The other versions on this listing are on the developer and API tools guide.