WAR Deployment in Practice, and Parallel Deployment
In one line
A deployment that just copies a WAR has a hole, a 404 during unpacking, and Tomcat already has a means to fill that hole (parallel deployment with ##버전, meaning a double hash followed by a version), but most sites don't know about it.
Why this is a problem
A stop-and-deploy is safe but the service is down meanwhile. If you try to avoid downtime by simply overwriting the WAR, that path returns 404 until unpacking finishes, and this time grows with the number of classes and can exceed 30 seconds. You can't dismiss it as short because those 30 seconds fall on nine in the morning on the first day of go-live.
On top of that, the failure is silent. If Tomcat starts unpacking in the middle of the copy and fails on a half-written WAR, it is left in the log but the deployment script's exit code is 0. The person who deployed believes it succeeded and goes home.
How a WAR is deployed
If you copy webapps/order.war, Tomcat unpacks it into webapps/order/ and serves it at the /order path.
That is how autoDeploy="true" works. It is convenient, but there are traps in production.
- Tomcat starts unpacking in the middle of the copy. If you copy a large WAR over the network,
it fails trying to unpack a half-present file. So the deployment script
copies under a different name and then does an atomic rename with
mv. - The
webapps/order/directory is deleted first. From that moment until the new app comes up,/orderis a 404. It is 3 seconds if short, and 30 seconds or more if there are many classes. - Deletion failure. Because of open file handles, the unpacked directory is sometimes not deleted completely and old classes remain. That is why the deployment procedure document contains "stop Tomcat → delete directory → delete work → copy WAR → start". It is safe, but the service is down meanwhile.
Parallel deployment — the zero-downtime means Tomcat has always had
Tomcat has a feature that is not well known. If you add ##버전 (a double hash plus a version) to the file name,
you can deploy several versions at the same time under the same context path.
webapps/order##001.war ← 기존 버전
webapps/order##002.war ← 새 버전
In this state, Tomcat behaves as follows.
- New requests (with no session) go to the highest version,
##002. - Requests that have an existing session keep going to
##001. - Once all the sessions of
##001have expired, you can take##001down and nobody gets hurt.
That is, you move to the new version without cutting sessions. In a single-WAS environment where the traditional zero-downtime deployment of removing and adding servers at an L4 in front is not possible, this is quite a useful card.
Version strings are sorted by string comparison, so if you write ##1, ##2, ##10,
##10 comes before ##2. Pad with zeros like ##001 or use
a timestamp such as ##2026-08-19_1430.
When you still do a stop-and-deploy
Parallel deployment is not a cure-all. You can't use it in the following cases.
- When the DB schema changes — if two versions look at the same DB but the schema differs, one of them breaks. That is why a schema change is split into three phases: expand → migrate → contract, and you don't put two phases in one deployment.
- When static resource caches get tangled — the two versions serve different JS at the same URL.
- When global state is used — singleton caches, schedulers, file locks.
So the practical judgment is this: if only screens and logic change, use parallel deployment; if schema, batches or global state are involved, use stop-and-deploy.
How WAR differs from an executable JAR
These days new development is done with Spring Boot fat jars, and existing systems are WARs. It is common for the two to coexist in the same organization, so it is good to lay out the differences.
| Item | WAR + external Tomcat | Executable JAR (embedded Tomcat) |
|---|---|---|
| Unit of deployment | WAR file | JAR file |
| Tomcat version | Managed by the server administrator (shared by several apps) | Held by the app (may differ per app) |
| Port | server.xml | Launch argument / properties |
| Placing several apps | Several contexts in one WAS | Start several processes |
| Rollback | Replace with the previous WAR | Replace the process with the previous JAR |
| Zero downtime | Parallel deployment or L4 | Process replacement + LB |
| Operations standard | WAS-based procedure documents | Process/container-based procedure documents |
Which way the operations organization's standard procedure documents are written governs the actual choice. Even if a JAR is technically more convenient, in an organization where monitoring, startup and backup procedures are standardized per WAS, one JAR means new procedures have to be written. That cost can exceed the technical advantage.
The minimum a deployment script must have
A deployment script worth using in the field does at least this much.
- Argument validation — that the WAR file actually exists and is not 0 bytes
- Backup — keep the current deployment with a timestamp
- Atomic placement — copy under a temporary name and then
mv - Wait for unpacking — poll for up to N seconds until the context responds
- Verification — that the health check URL returns 200 and the version string is the expected value
- Stop immediately on failure — if step 5 fails, don't report the deployment as successful
Step 4, "wait for unpacking", is especially important. Right after cp, if you curl, it is still 404.
I have seen scripts that judge that as a failure and roll back, and I have seen far more scripts
that print "deployment complete" without waiting and end.
The latter report a failed deployment as a success. That is far worse.
What you see in the field
So the minimum a deployment script must have is fixed. Copy under a different name and then rename atomically with mv, wait until unpacking finishes, and produce the exit code only after confirming that the actual response is 200. If you check right after copying, it is still unpacking and fails, and it is also common to mistake that for a "deployment failure" and roll back.