link:example$war/deps.edn[role=include]
Pedestals primary mode of deployment is embedded: the Pedestal application starts up and, along the way, launches a servlet container (such as reference:jetty.adoc) and configures it to receive requests and process them.
An alternate, and older, approach is to bundle the application as a WAR (Web Archive) file and deploy that into a running servlet container. Although there’s far less call for this kind of multi-tenant application hosting in modern times (since the advent of Docker containers and other kinds of virtualization), this is a long term feature of Pedestal that is still supported.
In this deployment mode, the servlet container will instantiate an instance of a Pedestal-provided servlet class; when the servlet initializes, it will create a bridge from the Servlet world into the Pedestal world. We’ll cover the details below.
In this guide we’ll show a very simple application, yet another "hello world" service.
We’ll show how to package up the application’s sources, resources, and library dependencies into a WAR file.
Finally, we’ll demonstrate how to deploy the WAR file into a Jetty service running inside a Docker container.
You’ll need to have Docker installed as part of this guide.
We’ve mentioned deployments and WAR files, so let’s detour into some more specifics about what a WAR file actually is.
A WAR file is an archive file, like a .zip or .jar file, with a particular structure.
WEB-INF/web.xml
— file: deployment descriptor needed by the container
WEB-INF/classes
— directory of resource files and compiled Clojure namespaces
WEB-INF/lib
— directory containing additional libraries that are needed by the application, such as Clojure and Pedestal
This structure provides the servlet container with the information it needs to set up a classloader specifically for the deployed WAR file, and direct incoming HTTP requests into it.
As with the other guides, we’ll define the dependencies of our project using a deps.edn
file.
link:example$war/deps.edn[role=include]
1 | WebSocket classes must be included even if not used by the application. |
2 | Used when compiling the project into a WAR file. |
The main dependency is on io.pedestal/pedestal.servlet
; all other necessary Pedestal dependencies
are provided as transitive dependencies from this. pedestal.servlet
is not linked to any particular implementation of the Servlet API.
In a servlet container deployment, the WAR file normally does not have dependencies on
the Servlet API; the servlet container, which has control over the classpath of the deployed
WAR, adds the Servlet API in automatically. pedestal.servlet
does not have a dependency on
the Servlet API
--- well, actually, it does under development and when compiling some Java classes, but it doesn’t
export those dependencies to projects like this one.
However, using WebSockets is different [1] and if the WAR file makes use of WebSockets it must include the WebSocket API (mostly interfaces) and implementation files (classes implementing those interfaces). These should be matched to the servlet container, so we’re using the Jetty implementations here.
Even though this application doesn’t use WebSockets, the Pedestal libraries do have WebSocket support built-in, so these dependencies must be included, even if they won’t be actively used.
link:example$war/src/org/example/war/service.clj[role=include]
Our application is very simple, it responds to the URL GET /hello
and responds with a snippet of text.
This is all the code for our application (!) --- we’ve skimped on writing tests.
The connection-map
function is used to create a connection map as if we were going to
create a reference:connector.adoc … but we stop short. In fact, all we really care
about at the end are the :interceptors and :initial-context keys of this connection map.
The magic happens in the create-bridge
function; this will be invoked from the
servlet to create the ConnectorBridge object. The
api:create-bridge[ns=io.pedestal.connector.servlet] function does the heavy lifting.
This bridge object has a service
method that is passed the HttpServletRequest
and HttpServletResponse and does all the Pedestal-related processing.
link:example$war/web.xml[role=include]
Inside all this XML are three basic constructors:
The servlet handles every request passed to it (that’s the /*
)
The servlet is class ConnectorServlet, provided by the pedestal-servlet
library
The servlet invokesthe function org.example.war.service/create-bridge
when it initializes
So, the request comes into the Jetty servlet container and it decides which WAR deployment it belongs to, then which servlet inside the WAR deployment is responsible, then asks the servlet to do the work, which passes through the ConnectorBridge and into Pedestal to do the actual work.
To create a WAR file, we need to package up the sources, the deployment descriptor, and any
libraries, such as pedestal.servlet
(and all the its transitive dependencies). We can do this
in a quick-and-dirty style using a small amount of Clojure code.
When we discussed dependencies, there was a :build alias: this build alias sets up build
as a classpath root, and brings in dependencies on io.github.clojure/tools.build
(for reading
the deps.edn
file) and babashka/fs
(for copying files and building archives).
link:example$war/build/build.clj[role=include]
1 | Create the basis |
2 | Iterate over the classpath entries and copy them into the working directory |
3 | Zip up the working directory to create the WAR file |
A basis is a fully expanded definition of the contents of the deps.edn
file, including all
the source roots, direct dependencies, and transitive dependencies. We use all that to copy
files to a working directory, and then we package it all together as a WAR file.
The command clojure -T:build war
will run this code:
$ clojure -T:build war
Copying cheshire-6.0.0.jar
Copying transit-clj-1.0.333.jar
Copying transit-java-1.0.371.jar
Copying jackson-core-2.18.3.jar
...
Copying ring-core-1.14.1.jar
Copying tigris-0.1.2.jar
Copying resources
Copying src
Copying web.xml
Created target/app.war
$
The first time through it may take some time to download the 13 MB of dependencies. |
This is just a minimal example; a more complete version might:
AOT-compile your application and its dependencies
Generate the web.xml file automatically
Package up static resources that Jetty can serve automatically, outside of Pedestal
Add other servlets or servlet filters to augment what Pedestal is doing
Now that we have a WAR file, we can show how to deploy it.
We’ll make use of Docker to simplify deployment … really, to not leave a mess on your computer after we’re done.
link:example$war/Dockerfile[role=include]
A Dockerfile describes how to build a Docker image — a snapshot of a virtual computer running some kind of Linux with everything installed and configured and ready to go. Most of the work was done for us in the image we extend from, and standard jetty 12 image.
It’s just a matter of enabling WAR deployments and copying the app.war
file into the image.
The WAR files go in the /var/lib/jetty/webapps
folder inside the Docker image, and the special
name ROOT.war
is recognized by Jetty as a WAR file that deploys without any kind of prefix
on the path.
You can build this manually with docker build …
, and run it with docker run …
…
but our final step is to show a simple script file that puts all the pieces together
To simplify things, we’ll provide a script that puts everything together and ensures that Jetty is running:
link:example$war/run.sh[role=include]
This script builds the WAR file, then builds the Docker image and tags it as war-demo
.
Finally, it starts Docker executing the image in a new container, and it binds port 8080
on your computer to port 8080 inside the Docker container. That’s the default port that Jetty uses.
If we run that script:
$ ./run.sh
Copying cheshire-6.0.0.jar
Copying transit-clj-1.0.333.jar
...
Copying src
Copying web.xml
Created target/app.war
[+] Building 1.6s (9/9) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile
...
=> => writing image sha256:5b54af80cb94b25da6b86f7e82fa6115eca301c11fedafae120275122ad74664 0.0s
=> => naming to docker.io/library/war-demo 0.0s
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
/usr/lib/jvm/java-21-amazon-corretto/bin/java -Djava.io.tmpdir=/tmp/jetty -Djetty.home=/usr/local/jetty -Djetty.base=/var/lib/jetty -Djava.io.tmpdir=/tmp/jetty --class-path /var/lib/jetty/resources:/usr/local/jetty/lib/logging/slf4j-api-2.0.17.jar:/usr/local/jetty/lib/logging/jetty-slf4j-impl-12.0.22.jar:/usr/local/jetty/lib/jetty-http-12.0.22.jar:/usr/local/jetty/lib/jetty-server-12.0.22.jar:/usr/local/jetty/lib/jetty-xml-12.0.22.jar:/usr/local/jetty/lib/jetty-util-12.0.22.jar:/usr/local/jetty/lib/jetty-io-12.0.22.jar:/usr/local/jetty/lib/jetty-deploy-12.0.22.jar:/usr/local/jetty/lib/jetty-session-12.0.22.jar:/usr/local/jetty/lib/jetty-security-12.0.22.jar:/usr/local/jetty/lib/jetty-ee-12.0.22.jar org.eclipse.jetty.xml.XmlConfiguration java.io.tmpdir=/tmp/jetty java.version=21.0.7 jetty.base=/var/lib/jetty jetty.base.uri=file:///var/lib/jetty jetty.home=/usr/local/jetty jetty.home.uri=file:///usr/local/jetty jetty.webapp.addHiddenClasses=org.eclipse.jetty.logging.,file:///usr/local/jetty/lib/logging/,org.slf4j. runtime.feature.alpn=true slf4j.version=2.0.17 /usr/local/jetty/etc/jetty-bytebufferpool.xml /usr/local/jetty/etc/jetty-threadpool.xml /usr/local/jetty/etc/jetty.xml /usr/local/jetty/etc/jetty-deploy.xml /usr/local/jetty/etc/sessions/id-manager.xml /usr/local/jetty/etc/jetty-ee-webapp.xml /usr/local/jetty/etc/jetty-http.xml --env ee10 -cp /usr/local/jetty/lib/jakarta.servlet-api-6.0.0.jar -cp /usr/local/jetty/lib/jetty-ee10-servlet-12.0.22.jar -cp /usr/local/jetty/lib/jetty-ee10-webapp-12.0.22.jar contextHandlerClass=org.eclipse.jetty.ee10.webapp.WebAppContext /usr/local/jetty/etc/jetty-ee10-webapp.xml /usr/local/jetty/etc/jetty-ee10-deploy.xml
2025-06-06 23:27:34.455:INFO :oejs.Server:main: jetty-12.0.22; built: 2025-06-02T15:25:31.946Z; git: 335c9ab44a5591f0ea941bf350e139b8c4f5537c; jvm 21.0.7+6-LTS
2025-06-06 23:27:34.476:INFO :oejdp.ScanningAppProvider:main: Deployment monitor ee10 in [file:///var/lib/jetty/webapps/] at intervals 0s
2025-06-06 23:27:34.482:INFO :oejd.DeploymentManager:main: addApp: App@279fedbd[ee10,null,/var/lib/jetty/webapps/ROOT.war]
2025-06-06 23:27:34.768:INFO :oejew.StandardDescriptorProcessor:main: NO JSP Support for /, did not find org.eclipse.jetty.ee10.jsp.JettyJspServlet
2025-06-06 23:27:34.788:INFO :oejsh.ContextHandler:main: Started oeje10w.WebAppContext@5a5338df{ROOT,/,b=file:///tmp/jetty-0_0_0_0-8080-ROOT_war-_-any-10435194439566279050/webapp/,a=AVAILABLE,h=oeje10s.SessionHandler@418c5a9c{STARTED}}{/var/lib/jetty/webapps/ROOT.war}
2025-06-06 23:27:34.827:INFO :oejes.ServletContextHandler:main: Started oeje10w.WebAppContext@5a5338df{ROOT,/,b=file:///tmp/jetty-0_0_0_0-8080-ROOT_war-_-any-10435194439566279050/webapp/,a=AVAILABLE,h=oeje10s.SessionHandler@418c5a9c{STARTED}}{/var/lib/jetty/webapps/ROOT.war}
2025-06-06 23:27:34.829:INFO :oejs.DefaultSessionIdManager:main: Session workerName=node0
2025-06-06 23:27:34.836:INFO :oejs.AbstractConnector:main: Started ServerConnector@2796aeae{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
2025-06-06 23:27:34.846:INFO :oejs.Server:main: Started oejs.Server@8f4ea7c{STARTING}[12.0.22,sto=0] @677ms
Again, there will be a lot of downloads the first time you run this script … but in the end, Jetty starts up and immediately deploys the ROOT.war
file.
Jetty will continue to run until you enter Ctrl-C, so in another window, you can use curl
to send a request to the application:
$ curl -v localhost:8080/hello
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: Jetty(12.0.22)
< Strict-Transport-Security: max-age=31536000; includeSubdomains
< X-Frame-Options: DENY
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
Greetings from inside the WAR file.
$
The -v
option shows the headers and data sent and received; you can not only see the
content provided by the hello
handler function, but you can also see all the headers
provided by the default interceptors.
Deploying a Pedestal application as a WAR file is a completely viable approach, especially useful when the Pedestal application must work along-side pure-Java servlets and servlet filters.
In fact, the Clojure code is the smallest part of this guide - much work was needed to package up the code into a WAR file, and a bit more (specific to this guide) to deploy that WAR file into a Docker container.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close