Apache Static/Dynamic File Separation
In Apache+Tomcat integrations, having Apache serve static files (CSS, JS, images) directly while forwarding only dynamic requests (JSP, servlets) to Tomcat significantly improves performance. This page covers how to implement this separation for each integration method.
Separation Principle
[Client]
↓
[Apache HTTPD]
├── /static/, /images/, /css/, /js/ → Apache direct serving (fast)
└── /api/, /app/, *.do, *.jsp → Forwarded to Tomcat (dynamic)
Benefits of direct serving:
- Reduces Tomcat load (saves threads)
- Easier to control caching headers
- Minimal disk I/O using sendfile() system calls
- Compression (gzip) can be applied at the Apache level
mod_jk — JkUnMount Method
Forward everything to Tomcat with JkMount, then define exceptions with JkUnMount.
<VirtualHost *:80>
ServerName example.com
# Static file directories
DocumentRoot /var/www/myapp
Alias /static /var/www/myapp/static
Alias /images /var/www/myapp/images
# Forward all to Tomcat
JkMount /* worker1
# Exception paths (served directly by Apache)
JkUnMount /static/* worker1
JkUnMount /images/* worker1
JkUnMount /css/* worker1
JkUnMount /js/* worker1
JkUnMount /fonts/* worker1
JkUnMount /favicon.ico worker1
JkUnMount /robots.txt worker1
<Directory "/var/www/myapp/static">
Options -Indexes
AllowOverride None
Require all granted
# Cache headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/svg+xml "access plus 6 months"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
</Directory>
CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
</VirtualHost>
Extension-Based JkUnMount
You can also exclude static files based on file extension patterns.
# Extension-based exclusions (mod_jk)
JkUnMount /*.css worker1
JkUnMount /*.js worker1
JkUnMount /*.png worker1
JkUnMount /*.jpg worker1
JkUnMount /*.gif worker1
JkUnMount /*.ico worker1
JkUnMount /*.woff worker1
JkUnMount /*.woff2 worker1
JkUnMount /*.ttf worker1
JkUnMount /*.svg worker1
JkUnMount /*.map worker1
Note: Extension patterns can be combined with path patterns (
/static/*) and work as OR conditions.
mod_proxy_http — ProxyPass Exclusion Method
Use the ! exclusion in ProxyPass directives to exclude static files.
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# Map static file directories
Alias /static /var/www/myapp/static
Alias /images /var/www/myapp/images
# Exclude static paths (! = do not proxy)
ProxyPass /static !
ProxyPass /images !
ProxyPass /css !
ProxyPass /js !
ProxyPass /fonts !
ProxyPass /favicon.ico !
ProxyPass /robots.txt !
# Forward only dynamic requests to Tomcat
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/ connectiontimeout=10 timeout=60
ProxyPassReverse / http://127.0.0.1:8080/
# Static file directory settings
<Directory "/var/www/myapp/static">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
<Directory "/var/www/myapp/images">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
# Static file cache + compression
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/webp "access plus 6 months"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/css application/javascript
AddOutputFilterByType DEFLATE image/svg+xml application/json
</IfModule>
CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
</VirtualHost>
Extension-Based Routing — Using RewriteRule
mod_rewrite provides finer-grained control.
sudo a2enmod rewrite
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
RewriteEngine On
# Static file extensions served by Apache (skip Tomcat)
RewriteRule \.(css|js|png|jpg|jpeg|gif|ico|woff|woff2|ttf|svg|webp|map)$ - [L]
# Everything else forwarded to Tomcat
RewriteRule ^/(.*)$ http://127.0.0.1:8080/$1 [P,L]
ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>
SetHandler — Force Handler for Specific Paths
SetHandler forces a specific handling method for a given path. Useful for status pages and health check endpoints.
# Server status page (internal network only)
<Location /server-status>
SetHandler server-status
Require ip 127.0.0.1 10.0.0.0/8
</Location>
# Server info page
<Location /server-info>
SetHandler server-info
Require ip 127.0.0.1
</Location>
# mod_proxy_balancer management page
<Location /balancer-manager>
SetHandler balancer-manager
Require ip 127.0.0.1
</Location>
# Health check endpoint (for load balancer probes)
<Location /health>
SetHandler default-handler
# Responds with static file (no Tomcat needed)
</Location>
# Enable mod_status
sudo a2enmod status
sudo systemctl reload apache2
Preventing Tomcat Double-Serving
When Apache serves static files, prevent Tomcat's DefaultServlet from double-serving the same files.
<!-- web.xml — Restrict DefaultServlet static path -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/dynamic-static/*</url-pattern>
<!-- /static/* is handled by Apache, not mapped to Tomcat -->
</servlet-mapping>
Or place static files outside the WAR:
<!-- server.xml — Add external path to Context -->
<Context path="/myapp" docBase="/opt/tomcat/webapps/myapp">
<!-- /static is outside the Context -->
</Context>
Build Artifact Deployment Structure
A common structure for serving SPA (Vue/React) builds directly from Apache:
/var/www/myapp/
├── static/ ← React/Vue build (Apache serving)
│ ├── css/
│ ├── js/
│ └── media/
├── index.html ← SPA entry point (Apache serving)
└── api/ ← Only API requests go to Tomcat (ProxyPass)
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
# Only API goes to Tomcat
ProxyPass /api/ http://127.0.0.1:8080/api/
ProxyPassReverse /api/ http://127.0.0.1:8080/api/
# SPA routing — unknown paths fall back to index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ /index.html [L]
<Directory "/var/www/myapp">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
Performance Comparison
| Method | Static File Handling | Tomcat Load | Cache Control |
|---|---|---|---|
| All forwarded to Tomcat | Tomcat DefaultServlet | High | Difficult |
| Apache direct serving | Apache mod_static | Low | Easy |
| CDN offloading | CDN | None | CDN config |
Summary
| Item | Method |
|---|---|
| mod_jk | JkUnMount /static/* worker1 |
| mod_proxy | ProxyPass /static ! |
| Extension-based | RewriteRule \.(css|js|...)$ - [L] |
| Handler assignment | SetHandler balancer-manager |
| Cache headers | mod_expires + ExpiresActive On |
| SPA support | RewriteRule ^ /index.html [L] |