From 95cfd387b1cced4d17c05d7b267807a6d409314c Mon Sep 17 00:00:00 2001 From: Tobias Wiesing Date: Sun, 25 Oct 2026 11:31:24 +0100 Subject: [PATCH] Add static asset caching and mod_expires to fpm-apache Match fpm-nginx's performance.conf: Cache-Control for assets, media and fonts, set only when the response has none, and CORS for fonts. Enable mod_expires for .htaccess rules, and remove mod_deflate's -gzip ETag, which never matched in If-None-Match. --- .../docs/2.image-variations/fpm-apache.md | 9 +++++ .../entrypoint.d/10-init-webserver-config.sh | 2 +- src/variations/fpm-apache/Dockerfile | 2 +- .../apache2/conf-available/performance.conf | 36 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/variations/fpm-apache/etc/apache2/conf-available/performance.conf diff --git a/docs/content/docs/2.image-variations/fpm-apache.md b/docs/content/docs/2.image-variations/fpm-apache.md index 58cab7791..80fc7ca6a 100644 --- a/docs/content/docs/2.image-variations/fpm-apache.md +++ b/docs/content/docs/2.image-variations/fpm-apache.md @@ -199,6 +199,15 @@ services: For production deployments, consider using a reverse proxy like Traefik or Caddy to handle SSL termination instead of managing certificates in the container. :: +## Static Asset Caching +Like the [`fpm-nginx`](/docs/image-variations/fpm-nginx) variation, Apache sends `Cache-Control: public, max-age=31536000, immutable` for static assets, media and fonts (`/etc/apache2/conf-available/performance.conf`). Fonts also get `Access-Control-Allow-Origin: *`. Compressed responses are sent without an `ETag`, so browsers revalidate them with `Last-Modified` and get a `304 Not Modified` (Apache's default `-gzip` ETag never matched). + +The header is only set when the response has no `Cache-Control` yet, so your own rules take precedence: `mod_expires` rules or `Header set` in an `.htaccess` file. `mod_expires` is enabled, so the usual `` blocks work. To opt out for some files, set your own `Cache-Control` for them; `Header unset` is not enough, as the default is added afterwards. With `DISABLE_DEFAULT_CONFIG=true`, `performance.conf` is not enabled; `mod_expires` still is. + +::note +`immutable` tells browsers not to revalidate for a year. That suits assets whose URL changes with their content (a hash in the file name, or a `?ver=` query string as WordPress adds). If you replace files under the same URL, such as uploads in WordPress, set a shorter lifetime for them in your `.htaccess`. +:: + ## Environment Variables The FPM-Apache variation supports extensive customization through environment variables. diff --git a/src/s6/etc/entrypoint.d/10-init-webserver-config.sh b/src/s6/etc/entrypoint.d/10-init-webserver-config.sh index 4a06a093b..72b13135d 100644 --- a/src/s6/etc/entrypoint.d/10-init-webserver-config.sh +++ b/src/s6/etc/entrypoint.d/10-init-webserver-config.sh @@ -134,7 +134,7 @@ SERVER_TYPE=$(detect_web_server_type) if [ "$DISABLE_DEFAULT_CONFIG" = false ]; then if [ "$SERVER_TYPE" = "Apache" ]; then - enable_apache_conf remoteip security serversideup + enable_apache_conf performance remoteip security serversideup enable_apache_site "$SSL_MODE" elif [ "$SERVER_TYPE" = "NGINX" ]; then process_template /etc/nginx/nginx.conf.template /etc/nginx/nginx.conf diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index 1978e9831..ef75acd5d 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -155,7 +155,7 @@ RUN docker-php-serversideup-dep-install-debian "${DEPENDENCY_PACKAGES_DEBIAN}" ln -sf /dev/stderr /var/log/apache2/error.log && \ \ # enable Apache2 mods - a2enmod actions autoindex deflate headers http2 proxy proxy_fcgi remoteip rewrite setenvif ssl unique_id && \ + a2enmod actions autoindex deflate expires headers http2 proxy proxy_fcgi remoteip rewrite setenvif ssl unique_id && \ \ # force Docker ENVs to handle the apache variables for the run user and group echo "export APACHE_RUN_USER=${APACHE_RUN_USER}" >> /etc/apache2/envvars && \ diff --git a/src/variations/fpm-apache/etc/apache2/conf-available/performance.conf b/src/variations/fpm-apache/etc/apache2/conf-available/performance.conf new file mode 100644 index 000000000..bc7ae23ac --- /dev/null +++ b/src/variations/fpm-apache/etc/apache2/conf-available/performance.conf @@ -0,0 +1,36 @@ +## +# Performance Configuration +## + +# Static asset caching, matching the fpm-nginx variation +# (server-opts.d/performance.conf). +# +# Cache-Control is only set when the response has none yet, so an +# application's own rules win: mod_expires or "Header set" / "Header always +# set" rules in an .htaccess file. %{resp:...} reads both header tables; +# "setifempty" would only see the one "Header set" writes to. +# +# matches the file Apache serves. A request for a missing +# asset that a rewrite passes to index.php gets no header from here. + +# Assets and media + + Header set Cache-Control "public, max-age=31536000, immutable" "expr=-z %{resp:Cache-Control}" + + +# Fonts + + Header set Access-Control-Allow-Origin "*" "expr=-z %{resp:Access-Control-Allow-Origin}" + Header set Cache-Control "public, max-age=31536000, immutable" "expr=-z %{resp:Cache-Control}" + + +# Remove the ETag of a compressed response. By default mod_deflate appends +# "-gzip" to it and then never matches that ETag in If-None-Match, so +# revalidating a compressed response always returns 200 with the full body. +# Without the ETag, browsers revalidate with If-Modified-Since and get 304. +# "NoChange" would give the gzip and identity responses one strong ETag, +# which lets an If-Range request mix their bytes. +# https://httpd.apache.org/docs/2.4/mod/mod_deflate.html#deflatealteretag + + DeflateAlterETag Remove +