Serving an Angular App with an API Proxy in Apache

How to configure Apache to serve an Angular SPA while proxying /api requests to a backend via AJP, with proper routing and cache-control headers.

This covers how to configure Apache to serve an Angular single-page application while proxying /api requests to a backend service over AJP. It handles three things that are easy to get wrong: Angular's client-side router, static file caching, and making sure API requests bypass the Angular rewrite rules.

Required Modules

Enable the proxy, AJP, rewrite, and headers modules, then restart Apache:

sudo a2enmod proxy proxy_ajp rewrite headers
sudo systemctl restart apache2

VirtualHost Configuration

<VirtualHost *:443>
  ServerAdmin webmaster@example.com
  DocumentRoot /var/www/my-app
  ServerName my-app.example.com

  ErrorLog \${APACHE_LOG_DIR}/my-app.error.log
  CustomLog \${APACHE_LOG_DIR}/my-app.access.log combined

  # Reverse proxy to backend (AJP)
  ProxyRequests Off
  ProxyPass "/api" "ajp://127.0.0.1:9298/api"
  ProxyPassReverse "/api" "ajp://127.0.0.1:9298/api"

  <Directory /var/www/my-app>
      Options -Indexes +FollowSymLinks
      AllowOverride All
      Require all granted

      # Handle Angular routing — exclude /api from rewrite
      RewriteEngine On
      RewriteCond %{REQUEST_URI} !^/api
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteRule ^ /index.html [QSA,L]
  </Directory>

  # Disable caching for index.html so Angular updates are picked up immediately
  <filesMatch "\.(html)$">
    FileETag None
    <ifModule mod_headers.c>
      Header unset ETag
      Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
      Header set Pragma "no-cache"
      Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
    </ifModule>
  </filesMatch>

  SSLEngine on
  SSLCertificateFile /opt/certs/example.crt
  SSLCertificateKeyFile /opt/certs/example.key
</VirtualHost>

Key Points

ProxyRequests Off — disables forward proxy mode. This is required when using Apache as a reverse proxy. Without it, Apache could be exploited as an open proxy.

ProxyPass + ProxyPassReverse — both are needed. ProxyPass forwards the request to the backend; ProxyPassReverse rewrites any redirect URLs in the backend's response so they point back to the correct public address instead of the internal one.

+FollowSymLinks — required in the <Directory> block or Apache will refuse to evaluate RewriteRule directives.

RewriteCond %{REQUEST_URI} !^/api — prevents Angular's catch-all rewrite from intercepting API requests. Without this, every /api/... request would be silently rewritten to index.html instead of being proxied to the backend.

RewriteCond %{REQUEST_FILENAME} !-f — serves static files (JS, CSS, images, fonts) directly from disk without rewriting. Only requests for paths that don't map to real files get forwarded to index.html.

HTML cache headers — Angular's build output includes content-hashed filenames for JS and CSS bundles, so those can be cached aggressively by the browser. index.html itself has no hash, so browsers will cache the old version indefinitely unless you explicitly disable caching on it. The <filesMatch> block forces the browser to always revalidate index.html.

Build and Deploy

# Build the Angular app for production
ng build --configuration production

# Copy the build output to the web root
cp -r dist/my-app/ /var/www/my-app/

# Fix ownership and permissions
sudo chown -R www-data:www-data /var/www/my-app
sudo chmod -R 755 /var/www/my-app

Deploying to a Subdirectory

If the app is not at the root of the domain (e.g. example.com/my-app/ instead of example.com/), set the base href at build time so Angular generates the correct asset paths and router links:

ng build --base-href /my-app/