Skip to main content
Advertisement

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

MethodStatic File HandlingTomcat LoadCache Control
All forwarded to TomcatTomcat DefaultServletHighDifficult
Apache direct servingApache mod_staticLowEasy
CDN offloadingCDNNoneCDN config

Summary

ItemMethod
mod_jkJkUnMount /static/* worker1
mod_proxyProxyPass /static !
Extension-basedRewriteRule \.(css|js|...)$ - [L]
Handler assignmentSetHandler balancer-manager
Cache headersmod_expires + ExpiresActive On
SPA supportRewriteRule ^ /index.html [L]
Advertisement