<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Embedded Jetty on Sven Ruppert</title><link>https://svenruppert.com/tags/embedded-jetty/</link><description>Sven Ruppert — Java Veteran, Speaker, Trainer &amp; Bushcrafter. Articles, talks, workshops and videos on Core Java, Cybersecurity, Vaadin and Developer Relations.</description><generator>Hugo</generator><language>en</language><managingEditor>sven.ruppert@gmail.com (Sven Ruppert)</managingEditor><webMaster>sven.ruppert@gmail.com (Sven Ruppert)</webMaster><copyright>© 2026 Sven Ruppert</copyright><atom:link href="https://svenruppert.com/tags/embedded-jetty/index.xml" rel="self" type="application/rss+xml"/><image><url>https://svenruppert.com/img/sven-ruppert.jpg</url><title>Sven Ruppert</title><link>https://svenruppert.com/tags/embedded-jetty/</link></image><lastBuildDate>Thu, 24 Sep 2026 10:00:00 +0200</lastBuildDate><item><title>Vaadin Deployment on Hetzner — Part 3: Embedded Jetty</title><link>https://svenruppert.com/posts/vaadin-deployment-on-hetzner-part-3-embedded-jetty/</link><pubDate>Thu, 24 Sep 2026 10:00:00 +0200</pubDate><author>sven.ruppert@gmail.com (Sven Ruppert)</author><dc:creator>Sven Ruppert</dc:creator><guid isPermaLink="true">https://svenruppert.com/posts/vaadin-deployment-on-hetzner-part-3-embedded-jetty/</guid><description>Embedded Jetty turns the server into part of the application: a Maven reactor, one module per delivery form, and a clean bootstrap without an installed Jetty.</description><content:encoded>&lt;![CDATA[<p>Third installment of the series<em>Vaadin – Deployment on Hetzner</em>. Jetty turns from a separately installed server component into a part of the application: its own bootstrap, its own classpath, no module mechanism anymore. All commands, outputs, and measurements come from an actual run on a Debian 13 server.</p><h2 id="starting-point">Starting Point</h2><p>At the end of Part 2, a Vaadin application runs as<code>ROOT.war</code> in a Jetty
12.1.12 installed under<code>/opt/jetty</code>, reachable over HTTPS behind Caddy,
as a hardened systemd service. Two things are maintained separately there:
the application and the server it runs in.</p><p>This part removes that separation. Afterwards, the application brings its
server along itself — as a dependency, not as an installation.</p><h3 id="what-shifts-as-a-result">What Shifts as a Result</h3><figure><img src="/images/2026/09/vaadin-deployment-on-hetzner-part-3-embedded-jetty-teil03-eingebettet.png" alt="The server becomes part of the application" loading="lazy" decoding="async"/><p><em>Figure 1: The dashed boundary now includes the server as well.</em></p><p>Part 1, Chapter 1 named the connecting thread of this series: the dashed
boundary around what belongs to the application. In Parts 1 and 2, only the
WAR lay behind this boundary. Now Jetty moves inside.</p><table><thead><tr><th/><th>Parts 1 and 2</th><th>from here on</th></tr></thead><tbody><tr><td>Jetty</td><td>installation under<code>/opt/jetty</code></td><td>Maven dependency</td></tr><tr><td>Startup</td><td><code>java -jar /opt/jetty/start.jar</code></td><td>its own<code>main()</code> method</td></tr><tr><td>Configuration</td><td><code>start.d/*.ini</code></td><td>Java code</td></tr><tr><td>Artifact</td><td><code>ROOT.war</code></td><td>application archive plus<code>lib/</code></td></tr><tr><td>Jetty version determined by</td><td>the server</td><td>the project</td></tr></tbody></table><p>The last row is the actual point. Anyone who wants to switch the Jetty
version afterwards changes a number in the<code>pom.xml</code> and ships the
application again — no longer a symlink on the server.</p><h3 id="the-repository-becomes-a-reactor">The Repository Becomes a Reactor</h3><p>Up to this point, the demo application was<strong>one</strong> Maven module. That worked
as long as there was one delivery format. With the second one, it no longer
worked well: the starter for embedded operation lived in<code>src/main/java</code> and
was therefore compiled with every build — including the WAR build, where
nobody calls it. This is exactly where the finding from Part 1, Section 4.5
came from: the complete Jetty ended up in the WAR because a single
dependency stood there without a<code>&lt;scope&gt;</code>.</p><p>The remedy back then was a declaration. The cause only disappears now:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>Blog-Vaadin-Deployment-on-Hetzner/ reactor
├── core/ the application, the tests, the frontend bundle
├── war-jetty/ ROOT.war for the external Jetty (parts 1 and 2)
└── embedded-jetty/ own main() (this part)</code></pre></div><p>One module per delivery format. The WAR module contains<strong>not a single line
of Java source code</strong> — only a<code>web.xml</code>. The embedded module contains exactly
one class. Everything else lives in the core and is used by both.</p><div class="pull-quote"><p><strong>About the names:</strong> The directories are named after the delivery format,
not after the part number — the WAR module belongs to<strong>two</strong> parts. The<code>artifactId</code>s additionally carry the prefix<code>vaadinapp-</code>, because a module
named<code>core</code> would collide with<code>com.svenruppert:core</code>, a dependency of
this application. A detail that is easy to overlook when splitting up a
project, and one the build acknowledges with an incomprehensible message.</p></div><h3 id="the-code-for-this-part">The Code for This Part</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/svenruppert/Blog-Vaadin-Deployment-on-Hetzner</span></span><span class="line"><span class="cl"><span class="nb">cd</span> Blog-Vaadin-Deployment-on-Hetzner</span></span><span class="line"><span class="cl">git checkout teil-03</span></span></code></pre></div></div><p>The state of Parts 1 and 2 — one module, building to<code>target/ROOT.war</code> —
remains available on the tag<code>teil-02</code>. A<code>git diff teil-02 teil-03</code> shows
exactly what this part describes.</p><h3 id="what-this-part-does-not-cover">What This Part Does Not Cover</h3><p><strong>Packaging.</strong> At the end of this part, an application archive sits next to
a directory containing its libraries. Whether that becomes a single archive
or a tidy directory structure is the question for Part 4 — and it can only
be asked meaningfully once the unpackaged case has been seen standing on its
own.</p><h2 id="what-does-embedded-jetty-mean">What Does Embedded Jetty Mean?</h2><p>The term sounds like a variant of the same thing. In fact, it describes a
reversal of responsibility.</p><h3 id="a-library-instead-of-a-server-installation">A Library Instead of a Server Installation</h3><p>In the setup so far, Jetty is a program. It lives under<code>/opt/jetty</code>, it has
its own directory structure, it is configured through files, and it starts
the application. The application is an archive this program reads in.</p><p>Embedded, Jetty is a library. It sits on the classpath next to Jackson and
SLF4J, it has no directory structure of its own, it is configured through
Java code, and<strong>the application starts it</strong>. The program that loads the
application becomes a dependency the application uses.</p><h3 id="application-lifecycle-instead-of-container-lifecycle">Application Lifecycle Instead of Container Lifecycle</h3><p>The practical difference shows in the startup sequence.</p><p>With the external Jetty, systemd starts the container, the container looks
for archives in<code>webapps/</code>, unpacks them, builds one web application context
each, and calls the initializers inside. The application has no entry point;
it gets found.</p><p>Embedded, systemd starts<strong>the application</strong>. Its<code>main()</code> method creates a
server, mounts a context, starts it, and waits. The entry point is ordinary
Java code that a narrative can walk along — and that is exactly what
Chapters 4 through 9 do.</p><h3 id="what-becomes-visible-in-the-process">What Becomes Visible in the Process</h3><p>The external Jetty takes care of a whole series of things without any of
them showing up in the project. The module files under<code>start.d/</code> numbered
sixteen in the reference setup. Sixteen decisions that someone once made and
that nobody sees afterwards.</p><p>Embedded, every one of them that is actually needed must appear in the code.
That is more work — and it is the reason this part is instructive in the
first place: what used to be configuration becomes readable.</p><p>Three of these decisions fail<strong>silently</strong> if you forget them. They get a
chapter of their own in Chapter 10, because they make the difference between
&ldquo;runs&rdquo; and &ldquo;runs correctly&rdquo;.</p><h3 id="what-stays-the-same">What Stays the Same</h3><p>More remarkable than the differences is how little changes in operation.
Service account, directory layout, hardening, configuration outside the
artifact, credentials via<code>systemd-creds</code>, Caddy in front — all unchanged.
The unit file swaps one line.</p><p>That is no coincidence but the result of the separation that Part 0 and
Chapters 9 through 11 of Part 1 built up: the operational framework does not
know<strong>how</strong> the application starts internally, and it does not need to.</p><h2 id="maven-dependencies">Maven Dependencies</h2><p>The external Jetty was set up with<code>--add-modules=server,http,ee11-deploy,ee11-annotations,ee11-websocket-jakarta</code>.
Embedded, there are no modules — there are dependencies. Translating one
into the other is the subject of this chapter.</p><h3 id="four-entries">Four Entries</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">xml</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;groupId&gt;</span>org.eclipse.jetty<span class="nt">&lt;/groupId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;artifactId&gt;</span>jetty-server<span class="nt">&lt;/artifactId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;groupId&gt;</span>org.eclipse.jetty.ee11<span class="nt">&lt;/groupId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;artifactId&gt;</span>jetty-ee11-webapp<span class="nt">&lt;/artifactId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;groupId&gt;</span>org.eclipse.jetty.ee11<span class="nt">&lt;/groupId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;artifactId&gt;</span>jetty-ee11-annotations<span class="nt">&lt;/artifactId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;groupId&gt;</span>org.eclipse.jetty.ee11.websocket<span class="nt">&lt;/groupId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;artifactId&gt;</span>jetty-ee11-websocket-jakarta-server<span class="nt">&lt;/artifactId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span></code></pre></div></div><table><thead><tr><th>Dependency</th><th>replaces the module</th><th>used for</th></tr></thead><tbody><tr><td><code>jetty-server</code></td><td><code>server</code>,<code>http</code></td><td><code>Server</code>,<code>ServerConnector</code>,<code>HttpConfiguration</code></td></tr><tr><td><code>jetty-ee11-webapp</code></td><td><code>ee11-deploy</code></td><td><code>WebAppContext</code></td></tr><tr><td><code>jetty-ee11-annotations</code></td><td><code>ee11-annotations</code></td><td>the scanner that finds Vaadin&rsquo;s routes</td></tr><tr><td><code>jetty-ee11-websocket-jakarta-server</code></td><td><code>ee11-websocket-jakarta</code></td><td>Vaadin Push</td></tr></tbody></table><p>No version numbers — those come from the two BOMs that were already imported
in Part 1.<code>${jetty.version}</code> thus remains the only place where the Jetty
release is recorded, now for the build instead of for the installation.</p><h3 id="the-servlet-api-switches-sides">The Servlet API Switches Sides</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">xml</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;groupId&gt;</span>jakarta.servlet<span class="nt">&lt;/groupId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;artifactId&gt;</span>jakarta.servlet-api<span class="nt">&lt;/artifactId&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;scope&gt;</span>compile<span class="nt">&lt;/scope&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span></code></pre></div></div><p>In the WAR module, this says<code>provided</code>: available for compilation, left out
at packaging time because the container supplies it. Here there is no
container supplying anything — the application<strong>is</strong> one. So the API has to
come along.</p><p>That is the same reasoning as in Part 1, Section 4.5, only applied in the
opposite direction. Anyone who reads<code>provided</code> as &ldquo;does not belong in the
archive&rdquo; has misunderstood it; it means &ldquo;someone else provides this&rdquo;. When
there is no someone else, the answer changes.</p><h3 id="four-entries-nobody-ordered">Four Entries Nobody Ordered</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">xml</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;&lt;groupId&gt;</span>org.ow2.asm<span class="nt">&lt;/groupId&gt;&lt;artifactId&gt;</span>asm<span class="nt">&lt;/artifactId&gt;&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;&lt;groupId&gt;</span>org.ow2.asm<span class="nt">&lt;/groupId&gt;&lt;artifactId&gt;</span>asm-tree<span class="nt">&lt;/artifactId&gt;&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;&lt;groupId&gt;</span>org.ow2.asm<span class="nt">&lt;/groupId&gt;&lt;artifactId&gt;</span>asm-commons<span class="nt">&lt;/artifactId&gt;&lt;/dependency&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;&lt;groupId&gt;</span>org.ow2.asm<span class="nt">&lt;/groupId&gt;&lt;artifactId&gt;</span>asm-analysis<span class="nt">&lt;/artifactId&gt;&lt;/dependency&gt;</span></span></span></code></pre></div></div><p>ASM is the library Jetty&rsquo;s annotation scanner uses to read class files. It
appears here only to force<strong>a single version</strong>. Default resolution mixes<code>asm</code> 9.8 (via<code>j2objc-annotations</code>) with<code>asm-tree</code> and<code>asm-commons</code> 9.9.1
(via Jetty). This mixture breaks on Java 26 bytecode:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>java.lang.IllegalArgumentException: Unsupported class file major version 70</code></pre></div><p>The context then fails to start, and the message mentions neither Jetty nor
Vaadin. It is included here because without prior knowledge it is almost
impossible to place: class file version 70 is Java 26.</p><h3 id="what-disappears">What Disappears</h3><p>One dependency is removed from the core module without replacement — the
library that encapsulated the embedded startup until now. With it goes the<code>&lt;scope&gt;provided&lt;/scope&gt;</code> workaround from Part 1, Section 4.5, and the whole
profile machinery that squeezed an additional archive out of a WAR module.</p><p>The reason this works: the embedded module has<code>jar</code> packaging from the
start. Nothing needs to be worked around when the structure matches the
intent.</p><h2 id="the-applications-entry-point">The Application&rsquo;s Entry Point</h2><p>A web application with a<code>main()</code> method looks unusual at first. It is the
core of this part.</p><h3 id="the-class">The Class</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="kd">class</span><span class="nc">Application</span><span class="w"/><span class="kd">implements</span><span class="w"/><span class="n">HasLogger</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">DEFAULT_HOST</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"127.0.0.1"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">DEFAULT_PORT</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">8080</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kd">public</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">main</span><span class="p">(</span><span class="n">String</span><span class="o">[]</span><span class="w"/><span class="n">args</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Application</span><span class="p">().</span><span class="na">launch</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="err">…</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>It is the<strong>only</strong> class in the<code>embedded-jetty</code> module. Everything else —
views, security, persistence, translations — lives in the core and is pulled
in via a dependency. That is not frugality but the statement the split
makes: how the application starts is a question of the delivery format, and
nothing else.</p><h3 id="host-and-port">Host and Port</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">launch</span><span class="p">()</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">host</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">resolve</span><span class="p">(</span><span class="s">"app.host"</span><span class="p">,</span><span class="w"/><span class="s">"APP_HOST"</span><span class="p">,</span><span class="w"/><span class="n">DEFAULT_HOST</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="kt">int</span><span class="w"/><span class="n">port</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">resolvePort</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="err">…</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">resolve</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">systemProperty</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">envVariable</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fallback</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fromSystem</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">getProperty</span><span class="p">(</span><span class="n">systemProperty</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fromSystem</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">fromSystem</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fromSystem</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fromEnv</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">getenv</span><span class="p">(</span><span class="n">envVariable</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fromEnv</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">fromEnv</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fromEnv</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fallback</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Three sources in a fixed order: system property, environment variable,
default value. The external Jetty made the same decision in<code>start.d/http.ini</code>; here it stands as a method you can read.</p><p>The default is<code>127.0.0.1</code>, not<code>0.0.0.0</code>. That is the same decision as in
Part 1, Chapter 8, and here it matters even more: a typo in a configuration
file gets caught during review, a wrong default in the code takes effect
everywhere nobody configured anything.</p><h3 id="the-check-that-comes-before-everything-else">The Check That Comes Before Everything Else</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">Application</span><span class="p">.</span><span class="na">class</span><span class="p">.</span><span class="na">getClassLoader</span><span class="p">().</span><span class="na">getResource</span><span class="p">(</span><span class="n">BUNDLE_MARKER</span><span class="p">)</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">abort</span><span class="p">(</span><span class="s">"Vaadin production bundle missing from the classpath (expected "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">BUNDLE_MARKER</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"). Build with `mvn -Pproduction package` before starting; without it the "</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"server starts, answers 200 and serves a blank page."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Five lines that catch one of the most annoying failure modes. Chapter 8
describes it in detail; the short version: if the built frontend is missing,
the server starts anyway, answers with 200, and serves a white page. The
error then shows up not in the server log but in the browser console — and
anyone who does not look there searches in the wrong place.</p><p>The check turns this into a startup message with instructions for action.</p><h3 id="failing-with-a-statement">Failing with a Statement</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@SuppressFBWarnings</span><span class="p">(</span><span class="n">value</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"DM_EXIT"</span><span class="p">,</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">justification</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"intentional — main-class launcher exits non-zero so a process supervisor sees the failure"</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kt">void</span><span class="w"/><span class="nf">abort</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">message</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">message</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">error</span><span class="p">(</span><span class="n">message</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">exit</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p><code>System.exit(1)</code> in an application is normally a warning sign. In a launcher
class it is the right answer: systemd distinguishes a service that
terminates with an error code from one that simply does not respond. Without
this call, the JVM would keep running,<code>Restart=on-failure</code> would not kick
in, and the service would count as &ldquo;running&rdquo;.</p><h2 id="creating-and-configuring-jetty">Creating and Configuring Jetty</h2><p>Four lines bring the server up. Two of them deserve an explanation.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Server</span><span class="w"/><span class="n">server</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Server</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">HttpConfiguration</span><span class="w"/><span class="n">httpConfig</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">HttpConfiguration</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">httpConfig</span><span class="p">.</span><span class="na">addCustomizer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ForwardedRequestCustomizer</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">httpConfig</span><span class="p">.</span><span class="na">setSendServerVersion</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">ServerConnector</span><span class="w"/><span class="n">connector</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServerConnector</span><span class="p">(</span><span class="n">server</span><span class="p">,</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">HttpConnectionFactory</span><span class="p">(</span><span class="n">httpConfig</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">connector</span><span class="p">.</span><span class="na">setHost</span><span class="p">(</span><span class="n">host</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">connector</span><span class="p">.</span><span class="na">setPort</span><span class="p">(</span><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">server</span><span class="p">.</span><span class="na">addConnector</span><span class="p">(</span><span class="n">connector</span><span class="p">);</span></span></span></code></pre></div></div><h3 id="server-without-a-thread-pool"><code>Server</code> Without a Thread Pool</h3><p><code>new Server()</code> creates a<code>QueuedThreadPool</code> with default values behind the
scenes. A custom pool would be possible:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">QueuedThreadPool</span><span class="w"/><span class="n">pool</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">QueuedThreadPool</span><span class="p">(</span><span class="n">200</span><span class="p">,</span><span class="w"/><span class="n">8</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">Server</span><span class="w"/><span class="n">server</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">Server</span><span class="p">(</span><span class="n">pool</span><span class="p">);</span></span></span></code></pre></div></div><p>Here, deliberately, there is none. The reference setup drew the limit
elsewhere:<code>TasksMax=256</code> in the systemd unit from Part 1, Chapter 11. Two
upper limits for the same thing are one upper limit too many — the smaller
one wins, and whoever changes the other wonders why nothing happens.
Measured in operation, 30 threads are running; the defaults are sufficient
by a wide margin.</p><h3 id="forwardedrequestcustomizer--the-line-whose-absence-nobody-notices"><code>ForwardedRequestCustomizer</code> — the Line Whose Absence Nobody Notices</h3><p>This single line replaces<code>--add-modules=forwarded</code> from Part 2, Chapter 4.
What it does and why its absence is dangerous is covered in Chapter 10; here,
only the place where it belongs: on the<code>HttpConfiguration</code>,<strong>before</strong> the
connector is created. Added afterwards, it no longer takes effect, because
the<code>HttpConnectionFactory</code> has already captured the configuration.</p><h3 id="setsendserverversionfalse"><code>setSendServerVersion(false)</code></h3><p>Without this line, Jetty sends a<code>Server: Jetty(12.1.12)</code> header entry with
every response. That is not a security hole, but it is free information for
every scanner about which release to attack. The external Jetty switched
this off via<code>start.d/http-config.ini</code>; here it is one line of Java.</p><h3 id="bind-address-the-same-decision-a-different-place">Bind Address: the Same Decision, a Different Place</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">connector</span><span class="p">.</span><span class="na">setHost</span><span class="p">(</span><span class="n">host</span><span class="p">);</span><span class="w"/><span class="o">//</span><span class="w"/><span class="n">127</span><span class="p">.</span><span class="na">0</span><span class="p">.</span><span class="na">0</span><span class="p">.</span><span class="na">1</span></span></span></code></pre></div></div><p>Part 1, Chapter 8 explained why the application server listens exclusively
on the loopback address: what cannot be reached does not have to be
defended. The reasoning holds unchanged — only now it lives in the source
code instead of a configuration file.</p><p>And it is the precondition for the line above it. Evaluating forwarding
headers is only defensible because the only party able to set them is the
reverse proxy on the same machine. If the connector were bound to<code>0.0.0.0</code>,
anyone on the network could claim the request arrived over HTTPS from any
address they like.</p><p><strong>These two lines belong together, and in the embedded setup they stand
visibly side by side for the first time.</strong> In the external Jetty they lived
in two different<code>.ini</code> files.</p><h2 id="configuring-the-servlet-context">Configuring the Servlet Context</h2><p>The server alone does not answer any request. It needs a context — the
counterpart to what the external Jetty created when unpacking a WAR.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">WebAppContext</span><span class="w"/><span class="n">webapp</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">WebAppContext</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">setContextPath</span><span class="p">(</span><span class="s">"/"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">setBaseResource</span><span class="p">(</span><span class="n">webResources</span><span class="p">(</span><span class="n">webapp</span><span class="p">));</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">String</span><span class="w"/><span class="n">scanPattern</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">getProperty</span><span class="p">(</span><span class="s">"app.scan.pattern"</span><span class="p">,</span><span class="w"/><span class="n">DEFAULT_SCAN_PATTERN</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">setAttribute</span><span class="p">(</span><span class="n">MetaInfConfiguration</span><span class="p">.</span><span class="na">CONTAINER_JAR_PATTERN</span><span class="p">,</span><span class="w"/><span class="n">scanPattern</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">setAttribute</span><span class="p">(</span><span class="n">MetaInfConfiguration</span><span class="p">.</span><span class="na">WEBINF_JAR_PATTERN</span><span class="p">,</span><span class="w"/><span class="n">scanPattern</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">setParentLoaderPriority</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span></span></span></code></pre></div></div><h3 id="webappcontext-or-servletcontexthandler"><code>WebAppContext</code> or<code>ServletContextHandler</code>?</h3><p>Jetty offers both.<code>ServletContextHandler</code> is the leaner path: no scan, no
separate class loader, less startup time. For many embedded applications it
is the right choice.</p><p>For a Vaadin application it is not. Vaadin finds its<code>@Route</code> classes
through the<code>RouteRegistryInitializer</code> — a<code>ServletContainerInitializer</code>
announced via<code>META-INF/services</code> and<strong>searched for</strong> by the container.
Take that search away, and you have to register the routes by hand. The
effort does not disappear; it merely changes location.</p><p><code>WebAppContext</code> brings the search along. The price is in the next line.</p><h3 id="the-scan-pattern-is-a-tuning-knob">The Scan Pattern Is a Tuning Knob</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">DEFAULT_SCAN_PATTERN</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">".*\\.jar$"</span><span class="p">;</span></span></span></code></pre></div></div><p>This pattern says: open<strong>every</strong> archive on the classpath and search it for
initializers with ASM. In the reference setup, that is 127 of them.</p><p>That is why the pattern does not sit in the code as a constant but behind a
system property:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">java -Dapp.scan.pattern<span class="o">=</span><span class="s1">'.*(vaadinapp|flow|vaadin|jcustos)-.*\.jar$'</span> …</span></span></code></pre></div></div><p>Narrowing it down shortens startup measurably — and buys you a list that
wants to be maintained with every new dependency. The reference setup
therefore leaves it wide; the option is in the code anyway, because
otherwise nobody would find it.</p><p><strong>How much the scan accounts for can be quantified with involuntary
precision</strong>: a start without the scan takes 139 ms, with the scan 1,523 ms.
How this measurement came about is told in Chapter 12 — it was a failure.</p><h3 id="setparentloaderprioritytrue"><code>setParentLoaderPriority(true)</code></h3><p><code>WebAppContext</code> comes with its own class loader, which normally searches the
web application<strong>first</strong> and only then the parent. In the WAR model that is
correct: an application may bring its own release of a library without the
container interfering.</p><p>Embedded, this separation no longer exists. Application and server sit on
the same classpath; a class loader that continues to assume two worlds only
creates confusion — in the worst case the same class loaded twice, with a<code>ClassCastException</code> nobody understands.<code>true</code> switches back to the
ordinary Java order.</p><p>It is the same issue that Part 1, Section 4.5 described from the other side:
there, Jetty&rsquo;s precedence rules protected the installed Jetty from being
displaced by a bundled one. Here there is no installed Jetty left to
protect.</p><h2 id="integrating-vaadin">Integrating Vaadin</h2><p>The context is in place. Now the application goes in — and in the process, a
file resurfaces that was taken for granted in the WAR model.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">ServletHolder</span><span class="w"/><span class="n">holder</span><span class="w"/><span class="o">=</span><span class="w"/><span class="k">new</span><span class="w"/><span class="n">ServletHolder</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">AppServlet</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">holder</span><span class="p">.</span><span class="na">setInitParameter</span><span class="p">(</span><span class="s">"i18n.provider"</span><span class="p">,</span><span class="w"/><span class="s">"com.svenruppert.flow.i18n.AppI18NProvider"</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">holder</span><span class="p">.</span><span class="na">setAsyncSupported</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">holder</span><span class="p">.</span><span class="na">setInitOrder</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">webapp</span><span class="p">.</span><span class="na">addServlet</span><span class="p">(</span><span class="n">holder</span><span class="p">,</span><span class="w"/><span class="s">"/*"</span><span class="p">);</span></span></span></code></pre></div></div><p>Five lines that do exactly what the<code>web.xml</code> does in the WAR module:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">xml</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;servlet&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;servlet-name&gt;</span>Vaadin Servlet<span class="nt">&lt;/servlet-name&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;servlet-class&gt;</span>com.svenruppert.flow.AppServlet<span class="nt">&lt;/servlet-class&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;async-supported&gt;</span>true<span class="nt">&lt;/async-supported&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;load-on-startup&gt;</span>1<span class="nt">&lt;/load-on-startup&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;init-param&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;param-name&gt;</span>i18n.provider<span class="nt">&lt;/param-name&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;param-value&gt;</span>com.svenruppert.flow.i18n.AppI18NProvider<span class="nt">&lt;/param-value&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/init-param&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/servlet&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;servlet-mapping&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;servlet-name&gt;</span>Vaadin Servlet<span class="nt">&lt;/servlet-name&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;url-pattern&gt;</span>/*<span class="nt">&lt;/url-pattern&gt;</span></span></span><span class="line"><span class="cl"><span class="nt">&lt;/servlet-mapping&gt;</span></span></span></code></pre></div></div><p>Line for line the same. That is no coincidence — the deployment descriptor
and programmatic registration are two notations for the same Servlet API.</p><h3 id="the-parameter-that-costs-a-language">The Parameter That Costs a Language</h3><p><code>i18n.provider</code> looks like a nicety and is not. Vaadin V25 does<strong>not</strong> look
up the<code>I18NProvider</code> via<code>META-INF/services</code> but exclusively through this
init parameter. If it is missing, the framework uses<code>DefaultI18NProvider</code> —
and that one has a defect Part 1 already mentioned: it falls back to the
JVM&rsquo;s default language. On a German machine, every request for English then
returns German text.</p><p>The application starts without complaint. The language switcher appears, it
can be operated, and it changes nothing. Whoever discovers this in
production searches for a long time.</p><p><strong>It is the first of three values that get lost silently during the
migration</strong> — Chapter 10 covers the other two.</p><h3 id="setinitorder1"><code>setInitOrder(1)</code></h3><p>The counterpart to<code>&lt;load-on-startup&gt;1&lt;/load-on-startup&gt;</code>. Without it, the
container creates the servlet only on the first request. The application
still runs — but the first visitor pays for the entire initialization:
building the Vaadin services, opening the datastore, and wiring the security
layers.</p><p>For a service behind a reverse proxy this means: the first request after
every restart runs into a timeout, and the access log records a 502 with no
explanation. The number 1 moves this work into startup, where it belongs and
where<code>systemctl start</code> waits for it.</p><h3 id="appservlet-stays-where-it-is"><code>AppServlet</code> Stays Where It Is</h3><p>What is remarkable is what does<strong>not</strong> appear here: no modified servlet
class. It is the same<code>AppServlet</code> as in the WAR, unchanged, from the core
module.</p><p>Both delivery formats register the same class with the same parameters — one
through a descriptor, the other through five lines of Java. The difference
between Parts 2 and 3 is not in the application. It is in who starts it.</p><h2 id="production-resources">Production Resources</h2><p>A WAR has a root directory. Everything inside it is served by the container.
An application archive has none — and that is exactly where the migration
fails most often.</p><h3 id="two-kinds-of-files-one-problem">Two Kinds of Files, One Problem</h3><p>The application serves two kinds of static files:</p><table><thead><tr><th/><th>where they come from</th></tr></thead><tbody><tr><td>the built Vaadin frontend</td><td><code>build-frontend</code>, under<code>META-INF/VAADIN/webapp/</code></td></tr><tr><td>images and icons</td><td>maintained by hand, in the WAR under<code>src/main/webapp/</code></td></tr></tbody></table><p>The first kind was never a problem: Vaadin puts it in a place it finds again
on its own. The second kind lived in the WAR root — and that root no longer
exists.</p><h3 id="the-solution-is-in-the-servlet-specification">The Solution Is in the Servlet Specification</h3><p>An archive may contribute web resources if they sit under<code>META-INF/resources</code>. The container serves them as if they were in the web
application&rsquo;s root directory. That is precisely what this mechanism is for,
and it applies to both delivery formats.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>core/src/main/resources/META-INF/resources/
├── images/portrait-sven.jpg
└── icons/github-mark.svg, linkedin-mark.png</code></pre></div><p>This keeps the references in the source code unchanged:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="k">new</span><span class="w"/><span class="n">Image</span><span class="p">(</span><span class="s">"images/portrait-sven.jpg"</span><span class="p">,</span><span class="w"/><span class="err">…</span><span class="p">)</span></span></span></code></pre></div></div><p>One source, two packagings, the same URLs. The WAR gets the files through<code>WEB-INF/lib/vaadinapp-core.jar</code>, the embedded setup through the classpath.</p><h3 id="the-four-lines-without-which-it-is-not-enough">The Four Lines Without Which It Is Not Enough</h3><p>For the WAR, the container takes care of the rest. Embedded, the context
must learn where its base is — and<strong>one</strong> lookup is not enough:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="n">Resource</span><span class="w"/><span class="nf">webResources</span><span class="p">(</span><span class="n">WebAppContext</span><span class="w"/><span class="n">webapp</span><span class="p">)</span><span class="w"/><span class="kd">throws</span><span class="w"/><span class="n">IOException</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">ResourceFactory</span><span class="w"/><span class="n">factory</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">webapp</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">List</span><span class="o">&lt;</span><span class="n">Resource</span><span class="o">&gt;</span><span class="w"/><span class="n">roots</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">Collections</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">list</span><span class="p">(</span><span class="n">Application</span><span class="p">.</span><span class="na">class</span><span class="p">.</span><span class="na">getClassLoader</span><span class="p">().</span><span class="na">getResources</span><span class="p">(</span><span class="s">"META-INF/resources/"</span><span class="p">))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">stream</span><span class="p">()</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">map</span><span class="p">(</span><span class="n">url</span><span class="w"/><span class="o">-&gt;</span><span class="w"/><span class="n">factory</span><span class="p">.</span><span class="na">newResource</span><span class="p">(</span><span class="n">url</span><span class="p">.</span><span class="na">toString</span><span class="p">()))</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">filter</span><span class="p">(</span><span class="n">Objects</span><span class="p">::</span><span class="n">nonNull</span><span class="p">)</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="p">.</span><span class="na">toList</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"Serving static web resources from {} classpath root(s)"</span><span class="p">,</span><span class="w"/><span class="n">roots</span><span class="p">.</span><span class="na">size</span><span class="p">());</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">ResourceFactory</span><span class="p">.</span><span class="na">combine</span><span class="p">(</span><span class="n">roots</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p><code>getResources</code>, plural, not<code>getResource</code>. In the reference setup, this line
reports at startup:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>Serving static web resources from 5 classpath root(s)</code></pre></div><p>Five, not one: the core module contributes images and icons, the Vaadin
archives their themes and the client engine. Take only the first root, and
you get an application in which either your own images or the Vaadin theme
is missing — depending on which archive the class loader finds first.</p><h3 id="the-failure-case-demonstrated-once">The Failure Case, Demonstrated Once</h3><p>The most expensive mistake in this part does not look like one at all. A
build without the production profile:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mvn clean package<span class="c1"># without -Pproduction</span></span></span></code></pre></div></div><p>Afterwards, the built frontend is missing. The server starts anyway:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>[main] INFO org.eclipse.jetty.server.Server - Started oejs.Server@… @1123ms</code></pre></div><p><code>curl</code> reports 200. The access log is unremarkable. In the browser the page
stays<strong>white</strong>, and the only hint sits in the developer console: references
to<code>VAADIN/build/…</code> that return 404.</p><p>That is why<code>Application</code> contains the check from Chapter 4:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="kd">final</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">BUNDLE_MARKER</span><span class="w"/><span class="o">=</span><span class="w"/><span class="s">"META-INF/VAADIN/webapp/index.html"</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">Application</span><span class="p">.</span><span class="na">class</span><span class="p">.</span><span class="na">getClassLoader</span><span class="p">().</span><span class="na">getResource</span><span class="p">(</span><span class="n">BUNDLE_MARKER</span><span class="p">)</span><span class="w"/><span class="o">==</span><span class="w"/><span class="kc">null</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">abort</span><span class="p">(</span><span class="s">"Vaadin production bundle missing from the classpath (expected "</span><span class="w"/><span class="o">+</span><span class="w"/><span class="n">BUNDLE_MARKER</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"). Build with `mvn -Pproduction package` before starting; without it the "</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="o">+</span><span class="w"/><span class="s">"server starts, answers 200 and serves a blank page."</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>A mute browser error becomes a startup message with instructions for action,
and the service terminates with an error code instead of appearing to run.</p><p><strong>Five lines against a lost evening.</strong> Part 1, Chapter 4 described the same
trap for the WAR; there it remained a warning. Here it is caught, because
embedded there is nobody left who would complain.</p><h3 id="where-the-bundle-is-built">Where the Bundle Is Built</h3><p>One point that surprises when splitting into modules:<code>build-frontend</code> runs<strong>not</strong> in the packaging module but in the core.</p><p>The Vaadin documentation is unambiguous here. The build generates the
frontend files in the module that configures the plugin; with<code>jar</code>
packaging they end up under<code>target/classes/META-INF/VAADIN/webapp/</code> and
thus travel into the core archive. Both packagings fetch them from there via
the classpath.</p><p>The cross-check after the build:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">unzip -l core/target/vaadinapp-core-*.jar<span class="p">|</span> grep -c<span class="s1">'META-INF/VAADIN/webapp'</span></span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>137</code></pre></div><p>Put the plugin into a packaging module instead, and you build the bundle
where the other packaging cannot see it — and find the mistake only on the
first page load.</p><h2 id="lifecycle">Lifecycle</h2><p>The server is assembled. What remains is the question of who stops it — and
what happens when nobody thinks of it.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Server</span><span class="w"/><span class="n">server</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">server</span><span class="p">(</span><span class="n">host</span><span class="p">,</span><span class="w"/><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">server</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">().</span><span class="na">info</span><span class="p">(</span><span class="s">"Embedded Jetty serving http://{}:{}/"</span><span class="p">,</span><span class="w"/><span class="n">host</span><span class="p">,</span><span class="w"/><span class="n">port</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="n">server</span><span class="p">.</span><span class="na">join</span><span class="p">();</span></span></span></code></pre></div></div><h3 id="start-returns-join-does-not"><code>start()</code> Returns,<code>join()</code> Does Not</h3><p><code>start()</code> brings the server up and returns control. Without the last line,<code>main()</code> would then run to its end, the JVM would exit, and the service
would be gone again after one second.</p><p><code>join()</code> blocks until the server stops. This one line is the difference
between a program that gets something done and a service that runs.</p><p>The message in between is not decoration. It is the only place where the
journal records which address the service actually listens on — and thus the
first thing you look for when Caddy reports 502.</p><h3 id="stopping-without-damaging-the-datastore">Stopping Without Damaging the Datastore</h3><p>The application keeps an Eclipse Store datastore open. A process that simply
gets killed leaves it unchanged in the best case and incomplete in the
worst.</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">server</span><span class="p">.</span><span class="na">setStopAtShutdown</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span></span></span></code></pre></div></div><p>With this, Jetty registers its own shutdown hook with the JVM: on<code>SIGTERM</code>,
the server shuts down in an orderly fashion, the contexts are stopped, and
the application gets the opportunity to close its resources.</p><p>The counterpart in the systemd unit has been in place since Part 1:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>KillSignal=SIGTERM
TimeoutStopSec=30</code></pre></div><p>systemd sends<code>SIGTERM</code> and waits thirty seconds before getting tougher.
Both sides have to match — a service with a shutdown hook and<code>KillSignal=SIGKILL</code> would have the hook for nothing.</p><h3 id="the-line-that-was-not-needed-in-the-war-model">The Line That Was Not Needed in the WAR Model</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>SuccessExitStatus=143</code></pre></div><p>143 is<code>128 + 15</code>, that is, &ldquo;terminated by signal 15&rdquo;. A service that shuts
down properly on<code>SIGTERM</code> exits with exactly this value — and without this
line, systemd would consider that a failure. With<code>Restart=on-failure</code>, that
would lead to a service that restarts itself after every planned stop.</p><p>The line dates from Part 1 and continues to apply unchanged. It appears here
because it is easy to overlook in the embedded setup: whoever rewrites the
unit instead of adapting it leaves it out and wonders.</p><h3 id="failure-at-startup">Failure at Startup</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="p">}</span><span class="w"/><span class="k">catch</span><span class="w"/><span class="p">(</span><span class="n">Exception</span><span class="w"/><span class="n">e</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">logger</span><span class="p">().</span><span class="na">error</span><span class="p">(</span><span class="s">"Failed to start embedded Jetty on {}:{}"</span><span class="p">,</span><span class="w"/><span class="n">host</span><span class="p">,</span><span class="w"/><span class="n">port</span><span class="p">,</span><span class="w"/><span class="n">e</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">stop</span><span class="p">(</span><span class="n">server</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">abort</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p><code>stop(server)</code> before<code>abort(...)</code>: if the server has already claimed a port
and something went wrong only afterwards, this call releases it. Without it,
the port would stay occupied until the process ends — and on the automatic
restart five seconds later, the next attempt would fail with a<code>BindException</code> whose cause then looks like an entirely different problem.</p><h2 id="what-the-module-mechanism-used-to-take-care-of">What the Module Mechanism Used to Take Care Of</h2><p>The server runs, the application responds, the pages render. This is where a
migration usually stops — and this is exactly where the most important part
is still missing.</p><p>Part 2 spent two chapters setting up the external Jetty for operation behind
Caddy. Both happened through modules:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo java -jar /opt/jetty/start.jar --add-modules<span class="o">=</span>forwarded</span></span><span class="line"><span class="cl">sudo java -jar /opt/jetty/start.jar --add-modules<span class="o">=</span>ee11-websocket-jakarta</span></span></code></pre></div></div><p>Embedded, there are no modules. Both have to go into the code — and<strong>both
fail silently if you forget them.</strong> No error, no log entry, no complaint.</p><h3 id="first-the-request-does-not-come-from-where-it-comes-from">First: The Request Does Not Come from Where It Comes From</h3><p>Behind Caddy, every request reaches the application server via<code>127.0.0.1</code>,
unencrypted, with a different hostname than the one the browser called. From
the application&rsquo;s point of view, this looks like local access over HTTP.</p><p>The consequence is not an error message but a wrong conclusion: Vaadin sets
the session cookie<strong>without</strong> the<code>Secure</code> flag, because the connection
appears to be unencrypted. A cookie without this flag may also be sent by
the browser over unencrypted connections.</p><p>One line fixes this:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">httpConfig</span><span class="p">.</span><span class="na">addCustomizer</span><span class="p">(</span><span class="k">new</span><span class="w"/><span class="n">ForwardedRequestCustomizer</span><span class="p">());</span></span></span></code></pre></div></div><p>The proof takes two calls — without and with the header Caddy sets:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -D- -o /dev/null http://127.0.0.1:8080/login<span class="p">|</span> grep -i set-cookie</span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>set-cookie: JSESSIONID=node0…; Path=/</code></pre></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -D- -o /dev/null -H<span class="s1">'X-Forwarded-Proto: https'</span><span class="se">\</span></span></span><span class="line"><span class="cl"> http://127.0.0.1:8080/login<span class="p">|</span> grep -i set-cookie</span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>set-cookie: JSESSIONID=node0…; Path=/; Secure</code></pre></div><p>Same server, same application, one difference in the result. Checked via the
public address:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -D- -o /dev/null https://demo.svenruppert.com/login<span class="p">|</span> grep -i set-cookie</span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>set-cookie: JSESSIONID=node01ggmnynrf3f7q162xs5dytkyna5.node0; Path=/; Secure</code></pre></div><h3 id="why-evaluating-them-is-defensible-at-all">Why Evaluating Them Is Defensible at All</h3><p>Forwarding headers are claims made by the sender. Whoever evaluates them
believes a stranger about where the request comes from and over which
protocol.</p><p>What makes this defensible is a decision from Part 1, Chapter 8: the
connector listens on<code>127.0.0.1</code>. The only party able to set these headers
is thus a process on the same machine — the reverse proxy. If it were bound
to<code>0.0.0.0</code>, anyone on the network could claim their request arrived over
HTTPS from any address, and the application would write it into the log and
into its access decisions.</p><p>In the external Jetty, these two decisions that belong together lived in two
different<code>.ini</code> files. In the code, they are seven lines apart.</p><h3 id="second-push-needs-a-container-that-nobody-brings-along">Second: Push Needs a Container That Nobody Brings Along</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">JakartaWebSocketServletContainerInitializer</span><span class="p">.</span><span class="na">configure</span><span class="p">(</span><span class="n">webapp</span><span class="p">,</span><span class="w"/><span class="kc">null</span><span class="p">);</span></span></span></code></pre></div></div><p>Vaadin Push keeps a persistent connection open. For that, the servlet
context needs WebSocket support, which is not present on its own in the
embedded setup. The call must happen<strong>before</strong> the context starts;
afterwards it is too late, and no message tells you so.</p><p>Without it, Vaadin falls back to long polling, or the connection fails. The
application remains usable — server-initiated updates just no longer arrive,
or arrive late.</p><p>The proof is a connection upgrade:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w<span class="s1">'%{http_code}\n'</span><span class="se">\</span></span></span><span class="line"><span class="cl"> -H<span class="s1">'Connection: Upgrade'</span> -H<span class="s1">'Upgrade: websocket'</span><span class="se">\</span></span></span><span class="line"><span class="cl"> -H<span class="s1">'Sec-WebSocket-Version: 13'</span> -H<span class="s1">'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=='</span><span class="se">\</span></span></span><span class="line"><span class="cl"><span class="s1">'http://127.0.0.1:8080/VAADIN/push?v-r=push&amp;X-Atmosphere-transport=websocket'</span></span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>101</code></pre></div><p><strong>101 Switching Protocols</strong> — the upgrade succeeds. A 200 here would be the
warning sign: then ordinary request processing would have answered, and the
upgrade would not have happened.</p><h3 id="third-the-language-provider-from-chapter-7">Third: The Language Provider from Chapter 7</h3><p>The<code>i18n.provider</code> parameter belongs to the same family. It, too, used to
live in a file the container read — the<code>web.xml</code> —, its absence, too,
produces no error, and its consequence, too, only shows in operation.</p><h3 id="the-pattern-behind-it">The Pattern Behind It</h3><p>Three values, three failures without a symptom. That is no coincidence but
the nature of configuration that exists as a<strong>default</strong>: if it is missing,
a default value applies, and default values are made for the simple case —
directly reachable server, no encryption, no persistent connections, one
language.</p><p>The embedded setup is therefore no less secure than the external one. It
merely shifts where the decision lives: from a file someone once created
into code that gets compiled. What is missing in both cases is feedback —
which is why these three points are paired with the three checks from this
chapter, and why those checks belong in the operating procedures and not
only here.</p><h2 id="configuring-the-embedded-server">Configuring the Embedded Server</h2><p>Part 2, Chapter 6 established the rule: configuration does not belong in the
artifact. It applies here unchanged — only the artifact is a different one.</p><h3 id="three-sources-fixed-order">Three Sources, Fixed Order</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">java</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">private</span><span class="w"/><span class="kd">static</span><span class="w"/><span class="n">String</span><span class="w"/><span class="nf">resolve</span><span class="p">(</span><span class="n">String</span><span class="w"/><span class="n">systemProperty</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">envVariable</span><span class="p">,</span><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fallback</span><span class="p">)</span><span class="w"/><span class="p">{</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fromSystem</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">getProperty</span><span class="p">(</span><span class="n">systemProperty</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fromSystem</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">fromSystem</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fromSystem</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="n">String</span><span class="w"/><span class="n">fromEnv</span><span class="w"/><span class="o">=</span><span class="w"/><span class="n">System</span><span class="p">.</span><span class="na">getenv</span><span class="p">(</span><span class="n">envVariable</span><span class="p">);</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">if</span><span class="w"/><span class="p">(</span><span class="n">fromEnv</span><span class="w"/><span class="o">!=</span><span class="w"/><span class="kc">null</span><span class="w"/><span class="o">&amp;&amp;</span><span class="w"/><span class="o">!</span><span class="n">fromEnv</span><span class="p">.</span><span class="na">isBlank</span><span class="p">())</span><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fromEnv</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="w"/><span class="k">return</span><span class="w"/><span class="n">fallback</span><span class="p">;</span><span class="w"/></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><table><thead><tr><th>Source</th><th>how it is set</th><th>used for</th></tr></thead><tbody><tr><td>system property</td><td><code>-Dapp.port=8081</code> in the unit</td><td>one-off deviation, clearly visible</td></tr><tr><td>environment variable</td><td><code>EnvironmentFile=/etc/vaadinapp/environment</code></td><td>the standard case on the server</td></tr><tr><td>default in the code</td><td>—</td><td>the development machine</td></tr></tbody></table><p>The order is not arbitrary. The system property wins because it lives in the
unit and thus under the server&rsquo;s version control; the environment variable
comes from a file that someone else can maintain as well; the default only
applies where nobody said anything.</p><h3 id="what-is-deliberately-absent-here">What Is Deliberately Absent Here</h3><p><strong>No configuration file format.</strong> It would be possible to read an<code>application.properties</code>. The reference setup does not, because every
additional source makes the question &ldquo;where does this value come from&rdquo;
harder to answer — and because systemd, with<code>EnvironmentFile</code>, already
provides an answer that needs no library.</p><p><strong>No reloading at runtime.</strong> The service reads its configuration at startup.
Whoever changes it restarts — four seconds of downtime, measured in
Chapter 12. An application that picks up configuration while running has to
answer, for every value, what happens to the requests that are in flight at
that moment. That is effort that does not pay off here.</p><h3 id="the-difference-from-the-war-model">The Difference from the WAR Model</h3><p>Exactly<strong>one</strong>:<code>app.storage.dir</code>. This property has been in the unit since
Part 1, because the application&rsquo;s default is a relative path and<code>ProtectSystem=strict</code> makes the working directory read-only. Nothing
changes here — the finding from Part 1 applies to both delivery formats
alike.</p><p>Everything else — credentials via<code>systemd-creds</code>,<code>EnvironmentFile</code>, file
permissions — is carried over unchanged from Part 2. That is the actual
message of this chapter: the operational framework does not care who starts
the server.</p><h2 id="embedded-jetty-under-systemd">Embedded Jetty Under systemd</h2><p>The unit from Part 1 changes<strong>one</strong> directive. The rest stays, line for
line — and that is the outcome the separation from Part 0 was aiming at.</p><h3 id="before-and-after">Before and After</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">diff</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> ExecStart=/var/lib/vaadinapp/.sdkman/candidates/java/current/bin/java \</span></span><span class="line"><span class="cl"><span class="gd">- -Djetty.home=/opt/jetty \</span></span></span><span class="line"><span class="cl"><span class="gd">- -Djetty.base=/opt/vaadinapp \</span></span></span><span class="line"><span class="cl"> -Djava.io.tmpdir=/var/lib/vaadinapp/work \</span></span><span class="line"><span class="cl"> -Dapp.storage.dir=/var/lib/vaadinapp/data \</span></span><span class="line"><span class="cl"> --add-exports java.base/jdk.internal.misc=ALL-UNNAMED \</span></span><span class="line"><span class="cl"> --enable-native-access=ALL-UNNAMED \</span></span><span class="line"><span class="cl"> -XX:MaxRAMPercentage=75 \</span></span><span class="line"><span class="cl"> -XX:+ExitOnOutOfMemoryError \</span></span><span class="line"><span class="cl"><span class="gd">- -jar /opt/jetty/start.jar</span></span></span><span class="line"><span class="cl"><span class="gi">+ -cp /opt/vaadinapp/app.jar:/opt/vaadinapp/lib/* \</span></span></span><span class="line"><span class="cl"><span class="gi">+ com.svenruppert.flow.Application</span></span></span></code></pre></div></div><p>Two lines go, two come in.<strong>Both hardening drop-ins remain untouched</strong> —
the same directives, the same hardening score of 1.1.</p><p>The asterisk in<code>lib/*</code> is not a shell glob. systemd does not expand it, and
that is a good thing: Java knows this notation in the classpath itself and
resolves it at startup.</p><h3 id="the-directory-on-the-server">The Directory on the Server</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>/opt/vaadinapp/
├── app.jar 8,908 bytes — just the launcher class
├── lib/ 127 archives, 29.74 MB
├── logs/
└── releases/ archive of the shipped releases</code></pre></div><p>The application archive is nine kilobytes. It contains one class. Everything
else sits next to it — including the application&rsquo;s core, as one of the 127
archives.</p><h3 id="-the-first-start-fails">⚠️ The First Start Fails</h3><p>It actually did fail, with a 503:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>java.lang.RuntimeException: Unable to create FileSystem for /opt/vaadinapp/lib/._jspecify-1.0.0.jar
Caused by: java.util.zip.ZipException: zip END header not found</code></pre></div><p><code>._jspecify-1.0.0.jar</code> — with a leading dot and underscore. That is not a
library but a companion file macOS creates for files with extended
attributes. When packing with<code>tar</code>, they travel into the archive; the hint
is already there at unpack time:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'</code></pre></div><p>Eleven such files ended up in<code>lib/</code>. And Java&rsquo;s classpath wildcard<code>lib/*</code>
picks up<strong>every</strong> file ending in<code>.jar</code> — it checks nothing, it only
expands.</p><p>The remedy when packing:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">COPYFILE_DISABLE</span><span class="o">=</span><span class="m">1</span> tar czf paket.tgz app.jar lib</span></span></code></pre></div></div><p>or on the target system:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">find /opt/vaadinapp -name<span class="s1">'._*'</span> -delete</span></span></code></pre></div></div><p><strong>Why this could not happen with the WAR:</strong> a WAR is a single archive
written by Maven. Only a classpath built from a directory makes the
application depend on that directory containing nothing but valid archives.
The delivery format brings a new source of error that has nothing to do with
Jetty.</p><h3 id="-why-there-is-no-java--jar">⚠️ Why There Is No<code>java -jar</code></h3><p>The obvious idea: use<code>addClasspath</code> to write a<code>Class-Path</code> into the
application archive&rsquo;s manifest, and<code>java -jar app.jar</code> will do.</p><p>That works —<strong>apparently</strong>. The class loader resolves the archives, the
server starts, the application responds. Every request, however, then ends
with:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>java.lang.NullPointerException: Cannot invoke
"com.vaadin.flow.server.StaticFileHandler.serveStaticResource(…)"</code></pre></div><p>The reason sits one level deeper. With<code>java -jar</code>, the<code>java.class.path</code>
property contains<strong>a single entry</strong> — the application archive. The
manifest&rsquo;s<code>Class-Path</code> entries are not in it. And this is precisely the
property Jetty&rsquo;s<code>MetaInfConfiguration</code> reads to decide which archives the
annotation scanner opens.</p><p>No scan, no<code>RouteRegistryInitializer</code>, no<code>StaticFileHandler</code>. The server
is healthy; the servlet is not.</p><p><strong>The telltale measurement is the startup time.</strong> With the identical 127
archives:</p><table><thead><tr><th>Start</th><th>Jetty&rsquo;s own timestamp</th></tr></thead><tbody><tr><td><code>java -jar app.jar</code></td><td><strong>139 ms</strong></td></tr><tr><td><code>java -cp 'app.jar:lib/*'</code></td><td><strong>1,523 ms</strong></td></tr></tbody></table><p>The missing second is the scan. Whoever does not know the difference takes
the faster start for a win.</p><p>That is why the manifest deliberately carries<strong>no</strong><code>Class-Path</code>, and the
launch command is explicitly:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">java -cp<span class="s1">'app.jar:lib/*'</span> com.svenruppert.flow.Application</span></span></code></pre></div></div><h3 id="the-teardown">The Teardown</h3><p>After the switch, the Jetty installation becomes superfluous:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo rm -f /opt/jetty<span class="c1"># Symlink</span></span></span><span class="line"><span class="cl">sudo rm -rf /opt/jetty-home-12.1.12<span class="c1"># the installation</span></span></span><span class="line"><span class="cl">sudo rm -rf /opt/vaadinapp/<span class="o">{</span>start.d,webapps,resources,environments<span class="o">}</span></span></span></code></pre></div></div><table><thead><tr><th>Directory</th><th>Size</th></tr></thead><tbody><tr><td><code>/opt/jetty-home-12.1.12</code></td><td>53 MB</td></tr><tr><td><code>/opt/vaadinapp/webapps</code></td><td>21 MB</td></tr><tr><td><code>start.d</code>,<code>resources</code>,<code>environments</code></td><td>80 KB</td></tr><tr><td><strong>total</strong></td><td><strong>71.1 MB</strong></td></tr></tbody></table><p>What is remarkable about the 53 MB: the distribution contains<strong>232
archives</strong>, of which<strong>26</strong> were on the classpath in operation. That is not
a defect but the way a server distribution is built — it ships all modules,
some get activated. Embedded, this reserve disappears, because the
dependencies are named.</p><p>Afterwards,<code>/opt</code> contains nothing but<code>vaadinapp</code>.</p><h2 id="caddy-remains-unchanged">Caddy Remains Unchanged</h2><p>The shortest chapter of this part, and one of the most telling.</p><h3 id="the-file">The File</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>demo.svenruppert.com {
reverse_proxy 127.0.0.1:8080
…
}</code></pre></div><p>Unchanged. No restart, no reload, no adjustment. Caddy talks to<code>127.0.0.1:8080</code> and does not know what runs behind it — previously an
installed servlet container with an archive inside, now an application with
its own server.</p><h3 id="why-this-cannot-be-taken-for-granted">Why This Cannot Be Taken for Granted</h3><p>Because it could easily have gone differently. The embedded setup could have</p><ul><li>listened on a different port,</li><li>bound to<code>0.0.0.0</code>,</li><li>ignored the forwarding headers,</li><li>refused WebSocket connections.</li></ul><p>In all four cases Caddy would have needed adjustment, or the application
would have worked incorrectly. That none of this was necessary is the result
of the decisions from Chapters 5, 7, and 10 — they are all aligned to offer
the same interface as before.</p><h3 id="the-cross-check">The Cross-Check</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w<span class="s1">'%{http_code}\n'</span> https://demo.svenruppert.com/</span></span><span class="line"><span class="cl">curl -s -D- -o /dev/null https://demo.svenruppert.com/login<span class="p">|</span> grep -i set-cookie</span></span><span class="line"><span class="cl">curl -s -D- -o /dev/null https://demo.svenruppert.com/<span class="p">|</span> grep -i strict-transport</span></span></code></pre></div></div><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>200
set-cookie: JSESSIONID=node01ggmnynrf3f7q162xs5dytkyna5.node0; Path=/; Secure
strict-transport-security: max-age=31536000; includeSubDomains</code></pre></div><p>The same three lines as at the end of Part 2.</p><h3 id="the-boundary-that-becomes-visible-here">The Boundary That Becomes Visible Here</h3><p>Caddy and the application have a contract: HTTP on<code>127.0.0.1:8080</code>, with
forwarding headers. As long as both sides keep it, each side is replaceable
for the other.</p><p>That is why the series chose this order. Had the reverse proxy not already
been set up and described in Part 2, this part would have to cover it as
well — and the claim &ldquo;none of this changes&rdquo; would not be verifiable, because
there would be no before.</p><h2 id="security-implications">Security Implications</h2><p>The switch shifts a responsibility, and an uncomfortable one at that.</p><h3 id="the-jetty-version-now-belongs-to-the-application">The Jetty Version Now Belongs to the Application</h3><p>Before: when a security advisory for Jetty appears, someone downloads the
new distribution, moves the symlink, restarts the service. The application
is not touched, not rebuilt, not retested.</p><p>After: the same advisory means changing a number in the<code>pom.xml</code>,
rebuilding, running the test suite, and shipping a release.</p><table><thead><tr><th/><th>external Jetty</th><th>embedded</th></tr></thead><tbody><tr><td>Who decides on the version</td><td>the server maintainer</td><td>the application project</td></tr><tr><td>Effort for a security update</td><td>symlink and restart</td><td>build and release</td></tr><tr><td>Who notices that one is needed</td><td>whoever maintains the server</td><td>whoever checks the dependencies</td></tr></tbody></table><p><strong>The last row is the dangerous one.</strong> With the external Jetty, the
installation sits visibly under<code>/opt/jetty</code>; it shows up in every
inventory. Embedded, Jetty is one line among 127 in a dependency tree. A
server whose operating system is well maintained can contain a
three-year-old Jetty release without anyone noticing.</p><p>What helps against this is not a decision about the setup but one about the
process: a dependency check in the build that reports known
vulnerabilities. The project generates a bill of materials
(<code>CycloneDX-SBom</code>) with every build — so the foundation for that is in
place.</p><h3 id="what-gets-better">What Gets Better</h3><p><strong>There is no second release anymore.</strong> Part 1, Section 4.5 described the
case of a WAR bringing its own Jetty, with the installed release shadowing
it. As long as the two match, all goes well; after that, the wrong class
wins at some unshielded spot. This source of error disappears: there is only
one Jetty left, and it is recorded in the<code>pom.xml</code>.</p><p><strong>What was built is what runs.</strong> A WAR is an incomplete artifact — it needs
a container of matching release with matching modules. A classpath is
complete. What starts on the development machine also starts on the server,
because both use the same list.</p><p><strong>The server&rsquo;s attack surface shrinks.</strong> 71 MB less installed software, 232
fewer archives on disk, no deployment directory that a process with write
permissions could fill. The teardown from Chapter 12 is not just tidying up.</p><h3 id="what-stays-the-same-1">What Stays the Same</h3><p>The hardening score.<code>systemd-analyze security vaadinapp</code> reports<strong>1.1</strong>
before and after, with the same directives in the same drop-in.</p><p>That is expected and still worth mentioning: hardening describes what the
process<strong>may do</strong> — which directories it may write, which system calls it
may issue, which privileges it may escalate. These questions are independent
of who starts the server inside the process. A hardening score that changed
with the launch method would have been attached at the wrong place.</p><h2 id="external-versus-embedded-jetty">External Versus Embedded Jetty</h2><p>Both setups ran on the same machine, with the same application, behind the
same Caddy. What follows is measured, not estimated.</p><h3 id="the-numbers">The Numbers</h3><table><thead><tr><th>Metric</th><th>WAR on external Jetty</th><th>embedded</th></tr></thead><tbody><tr><td>Downtime until first response</td><td>5,461 ms</td><td><strong>4,685 ms</strong></td></tr><tr><td>Jetty uptime until<code>Started Server</code></td><td>3,969 ms</td><td><strong>3,634 ms</strong></td></tr><tr><td>Memory at idle (<code>MemoryCurrent</code>)</td><td>448 MiB</td><td><strong>336 MiB</strong></td></tr><tr><td>RSS</td><td>443 MiB</td><td><strong>361 MiB</strong></td></tr><tr><td>Threads</td><td>34</td><td><strong>30</strong></td></tr><tr><td>Archives on the classpath</td><td>112</td><td>127</td></tr><tr><td>Delivery size</td><td>21.78 MB</td><td>29.75 MB</td></tr><tr><td>additionally required on the server</td><td>53 MB installation</td><td>—</td></tr><tr><td>Hardening score</td><td>1.1</td><td>1.1</td></tr></tbody></table><p>Each value is the mean of three runs. Individual downtime values for the
embedded setup: 4,517, 4,868, and 4,670 ms.</p><h3 id="the-row-that-contradicts-expectation">The Row That Contradicts Expectation</h3><p>Part 2, Chapter 8 ended with an observation: an archive smaller by a quarter
had<strong>not</strong> reduced memory consumption. The explanation back then: what was
removed had never been loaded in the first place.</p><p>Here it is the other way around, and the explanation is the same. The
delivery<strong>grows</strong> from 21.78 to 29.75 MB, the classpath lengthens from 112
to 127 archives — and memory consumption still drops by 112 MiB.</p><p>Both times, what was measured was not the amount of bytes shipped but the
amount of<strong>loaded classes</strong>. The external Jetty brings its module system,
its deployment scanner, and its own startup machinery; the embedded setup
loads only what the application calls.</p><p><strong>Whoever takes an artifact&rsquo;s size as a measure of its resource consumption
is wrong in both directions.</strong></p><h3 id="what-the-numbers-do-not-show">What the Numbers Do Not Show</h3><p>Downtime drops by roughly 780 ms, which is less than the elimination of
unpacking would suggest. The reason is in the measurement from Part 2: over
forty percent of the startup time goes to Vaadin initialization — to the
application itself. Changing the launch mechanism cannot change that.</p><p>Whoever expects a dramatically faster start from embedded operation will be
disappointed. Four seconds remain four seconds.</p><h3 id="responsibility-maintenance-tooling">Responsibility, Maintenance, Tooling</h3><table><thead><tr><th/><th>external Jetty</th><th>embedded</th></tr></thead><tbody><tr><td>Install the server</td><td>yes, once and at every upgrade</td><td>no</td></tr><tr><td>Configure the server</td><td><code>start.d/*.ini</code>, 16 files</td><td>Java code, one class</td></tr><tr><td>Update the server</td><td>move the symlink, restart</td><td>build and release</td></tr><tr><td>Server version visible in</td><td>a directory on disk</td><td>the dependency tree</td></tr><tr><td>One server for several applications</td><td>possible</td><td>no, one per application</td></tr><tr><td>Development run</td><td>start the container</td><td>start<code>main()</code></td></tr></tbody></table><p>The second-to-last row is often overlooked. An installed Jetty can serve
several archives at once. Whoever runs three small applications on one
server pays the base footprint of a JVM three times when embedded. With one
application per server — the case of this series — that is not an argument.</p><h3 id="what-stays-the-same-in-both-cases">What Stays the Same in Both Cases</h3><p>Directory layout, service account, hardening, journald, configuration
outside the artifact, credentials via<code>systemd-creds</code>, Caddy, update and
rollback.<strong>Everything Parts 0 and 2 built survives the switch unchanged.</strong></p><p>That is not a side note but the argument for the order of this series:
whoever builds the operational framework first and keeps it independent of
the delivery format can swap that format later without touching everything
else again.</p><h2 id="when-does-your-own-embedded-jetty-make-sense">When Does Your Own Embedded Jetty Make Sense?</h2><p>The answer hangs on a single question:<strong>who maintains the server?</strong></p><h3 id="in-favor">In Favor</h3><p><strong>When the same person is responsible for the application and the server.</strong>
Then the separation between the two is artificial, and a number in the<code>pom.xml</code> is the shorter path than a symlink on a machine.</p><p><strong>When the application ships frequently.</strong> A security update for Jetty is
then no special case; it travels with the next release anyway.</p><p><strong>When the target server is supposed to contain as little as possible.</strong> A
JVM suffices. No server installation, no deployment directory, no module
configuration — 71 MB less and one software component fewer that someone has
to keep an eye on.</p><p><strong>When the setup is supposed to be reproducible.</strong> A classpath is complete.
A WAR is not: it presupposes a container of matching release with matching
modules, and this precondition is recorded nowhere in the artifact.</p><h3 id="against">Against</h3><p><strong>When several applications run on one server.</strong> An installed Jetty serves
several archives from one JVM. Embedded, every application pays its own base
footprint — with three small applications, that is the decisive point.</p><p><strong>When the server is maintained by someone else.</strong> Then the separation is
not an inconvenience but a boundary of responsibility. A maintainer who can
update Jetty without having every application rebuilt is an advantage, not
an obstacle.</p><p><strong>When nobody checks the dependencies.</strong> An embedded server disappears into
the dependency tree. Without a check in the build that reports known
vulnerabilities, the switch is a step backwards — not technically, but
organizationally.</p><p><strong>When the application needs container features</strong> that otherwise come for
free: JNDI resources, several contexts, a deployment directory for
third-party archives. All of it can be rebuilt in an embedded setup, and
none of it is worth the effort when it is available ready-made.</p><h3 id="the-honest-summary">The Honest Summary</h3><p>For the case of this series — one application, one server, one responsible
person, regular releases — the embedded setup is the better fit. It is not
faster in operation and not leaner as an artifact; it is<strong>more complete</strong>,
and it moves a decision to where it is being made anyway.</p><p>For a server hosting several applications from several teams, the answer is
the reverse. Both setups are correct; they answer different questions.</p><h2 id="outlook-on-part-4">Outlook on Part 4</h2><p>The boundary has shifted for the first time. Jetty now belongs to the
application. What is still open lies in plain sight on the server.</p><h3 id="what-this-part-cost">What This Part Cost</h3><p>The bootstrap from Chapters 4 through 10 comes to about 60 lines — and three
of them are the ones you would not write without this text:</p><ul><li><code>ForwardedRequestCustomizer</code>, otherwise the session cookie loses its<code>Secure</code> flag,</li><li>the WebSocket initialization, otherwise Push fails,</li><li>the<code>i18n.provider</code> parameter, otherwise the language switcher has no
effect.</li></ul><p>None of these three failures announces itself. Whoever writes the bootstrap
themselves takes on a duty of care that previously sat in sixteen module
files maintained by someone else.</p><h3 id="what-still-lies-next-to-it">What Still Lies Next to It</h3><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">code</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><pre tabindex="0"><code>/opt/vaadinapp/
├── app.jar 8,908 bytes — one class
└── lib/ 127 archives, 29.74 MB</code></pre></div><p>An application archive of nine kilobytes and a directory with 127 libraries.
The launch command names both:</p><div class="code-wrap"><div class="code-wrap__bar"><span class="code-wrap__lang">bash</span><button type="button" class="code-wrap__copy" aria-label="Copy code">Copy</button></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">java -cp<span class="s1">'app.jar:lib/*'</span> com.svenruppert.flow.Application</span></span></code></pre></div></div><p>That works, and it is inconvenient. A deployment consists of two parts that
belong together and lie apart; a rollback has to wind both back; and whoever
passes the application on passes on a directory, not an artifact.</p><h3 id="what-part-4-makes-of-it">What Part 4 Makes of It</h3><p><strong>Bootstrap and packaging are two different decisions.</strong> This part answered
the first: who starts the server? Part 4 answers the second: how does the
result get onto the target machine?</p><p>Two answers stand against each other there:</p><table><thead><tr><th/><th/></tr></thead><tbody><tr><td><strong>Fat JAR</strong></td><td>everything in one archive. One<code>java -jar</code>, one file, one rollback</td></tr><tr><td><strong>Thin distribution</strong></td><td>application archive and libraries separate, but shipped in an orderly manner</td></tr></tbody></table><p>The chapters there: how a fat JAR is built, where it causes problems —
Chapter 12 of this part has already given a foretaste —, what a thin
distribution gains, and what releases with symlink and rollback look like on
top of it.</p><p><strong>The choice is not obvious.</strong> A fat JAR is one file and therefore
convenient; but it turns every update of a single library into a complete
rebuild, and it has a known breaking point in<code>META-INF/services</code>. A thin
distribution stays decomposable, but demands a directory that has to be
correct — as Chapter 12 showed with eleven wrong files.</p><h3 id="the-boundary-keeps-moving">The Boundary Keeps Moving</h3><table><thead><tr><th>Part</th><th>What moves into the application</th></tr></thead><tbody><tr><td>1 and 2</td><td>only the WAR</td></tr><tr><td><strong>3</strong></td><td><strong>Jetty becomes a library of the application</strong></td></tr><tr><td>4</td><td>the kind of packaging (fat JAR or thin distribution)</td></tr><tr><td>5</td><td>the Java runtime (jlink)</td></tr></tbody></table><p>At the end of Part 6, the application runs on a server with no Java
installed.</p><p><strong>Why the intermediate state deliberately looks like this:</strong> a fat JAR would
have been possible in this part. It would have obscured the question of what
distinguishes bootstrap from packaging — and precisely this distinction is
the first sentence of Part 4.</p>
]]></content:encoded><category>Java</category><category>Vaadin</category><media:content url="https://svenruppert.com/images/2026/09/vaadin-deployment-on-hetzner-part-3-embedded-jetty-hero.png" medium="image"/><media:thumbnail url="https://svenruppert.com/images/2026/09/vaadin-deployment-on-hetzner-part-3-embedded-jetty-hero.png"/><enclosure url="https://svenruppert.com/images/2026/09/vaadin-deployment-on-hetzner-part-3-embedded-jetty-hero.png" type="image/jpeg" length="0"/></item></channel></rss>