Web Server, Reverse Proxy and HTTPS
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
This page is for administrators and DevOps engineers who put the Tallpine Hiking Club site on a server of their own.
It covers the web server (Apache or nginx), the PHP-FPM pool behind it, TLS, the two Global Configuration settings that
make Joomla aware of HTTPS and of a proxy (force_ssl and behind_loadbalancer), the headers a reverse proxy, a load
balancer or a CDN must send, and the one requirement that most often surprises people who lock a site down: Automated
Core Updates only work for a site that the Joomla project can reach from the internet.
It was written against Joomla 6.2.x with PHP 8.3 or later (PHP 8.4 in the examples) and checked on 2026-10-30. The
Developer Manual lists, for Joomla 6.x, Apache 2.4, nginx 1.26 as supported with 1.29 recommended, and IIS 10; PHP 8.3.0
as the minimum with 8.4 recommended. IIS is not covered here, only mentioned where it differs. The behaviour described
for force_ssl, behind_loadbalancer and the forwarded headers was read from the 6.2-dev branch of the source tree.
The requirements themselves are in Requirements and Hosting; the
configuration.php properties are in configuration.php and
.htaccess. This page assumes the layout of
Installation and the Public Folder (document root = public).
What Joomla expects from a web server
Joomla is a set of three front controllers: index.php for the site, administrator/index.php for the back end and
api/index.php for the Web Services API (see Web Services API). With SEF URLs
the web server’s job is small:
-
serve a file or a directory when one exists at the requested path;
-
send every other path to
index.php, except paths below/api/, which go toapi/index.php; -
run
.phpfiles through PHP (PHP-FPM in this page) and never serve their source; -
pass the
Authorizationrequest header to PHP, because API token authentication depends on it; -
never let a visitor request
configuration.php, logs, temporary files or the code tree directly.
Joomla itself does not need mod_rewrite to route: the Global Configuration option Use URL Rewriting only controls
whether index.php appears in generated URLs. The Developer Manual lists mod_rewrite on Apache and the URL Rewrite
Module on IIS as the optional pieces for SEO URLs, and the User Guide page on SEF URLs with nginx says that Joomla’s own
routing does not depend on rewriting executed by the web server; on nginx the try_files line below does that job.
Whether the page /trails/alpine is served by a menu item or a 404 is decided by PHP, not by the server.
The figure shows the three places where a mistake usually hides: the TLS boundary (is Joomla told that the request was secure?), the proxy hop (which address and which scheme does PHP see?) and the dotted return path (can the update service reach the site?). The rest of the page follows that order.
Apache 2.4
A virtual host with PHP-FPM
The Joomla User Guide describes the PHP handlers Apache can use. mod_php is fast but ties every site to one PHP
version and is recommended only on a local machine; on a server use FastCGI or PHP-FPM. With FPM the pool runs as its own
user and Apache talks to it through mod_proxy_fcgi. PHP settings can then no longer be changed in .htaccess; they
belong in the pool (see below).
<VirtualHost *:80>
ServerName www.tallpine.example
Redirect permanent / https://www.tallpine.example/
</VirtualHost>
<VirtualHost *:443>
ServerName www.tallpine.example
DocumentRoot /var/www/tallpine/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/www.tallpine.example/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.tallpine.example/privkey.pem
Protocols h2 http/1.1
<Directory /var/www/tallpine/public>
# htaccess.txt renamed to .htaccess needs FileInfo, Options and Limit
AllowOverride All
Require all granted
</Directory>
# Send PHP to the FPM pool through a Unix socket
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.4-fpm-tallpine.sock|fcgi://localhost"
</FilesMatch>
# The FPM handler drops the Authorization header unless it is passed on explicitly
CGIPassAuth On
ErrorLog ${APACHE_LOG_DIR}/tallpine-error.log
CustomLog ${APACHE_LOG_DIR}/tallpine-access.log combined
</VirtualHost>
The modules to enable on a Debian-style system are proxy, proxy_fcgi, rewrite, headers, ssl and http2, and
the MPM must be event (not prefork, which is for mod_php). AllowOverride All is the reason an .htaccess file
works at all; with AllowOverride None Apache ignores it silently and SEF URLs return 404 for every path except the
home page. Copying the rules into the virtual host and setting AllowOverride None is faster and is the better
production choice, because Apache then does not look for .htaccess files in every directory of every request.
What htaccess.txt does
Joomla ships htaccess.txt; renaming it to .htaccess (the installer and the public-folder generator copy it for you)
gives these rules in 6.2:
-
Options +FollowSymlinksandOptions -Indexes(no directory listings); -
X-Content-Type-Options: nosniff, and aContent-Security-Policy: script-src 'none'header for.svgfiles so that an uploaded SVG cannot run script when opened directly; -
a query-string filter that returns 403 for
base64_encode(...)calls,<script>tags,GLOBALSand_REQUESTmanipulation; -
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}], which copies theAuthorizationheader into an environment variable PHP can read when the handler would otherwise remove it; -
routing: a request below
/api/that is not a real file or directory goes toapi/index.php; every other request that is not a real file or directory goes toindex.php; -
a fallback redirect from
/to/index.php/whenmod_rewriteis missing; -
serving precompressed
.css.gzand.js.gzfiles when the client accepts gzip.
It contains no HTTPS redirect and no X-Forwarded-* handling; both are the administrator’s job and are done in the
virtual host above and in the Global Configuration. A full line-by-line reading is in
configuration.php and .htaccess.
Common Apache problems
| Symptom | Usual cause |
|---|---|
Only the home page works; |
|
500 error on every request after copying |
a directive not allowed by |
API calls answer 401 although the token is valid |
the |
PHP source is downloaded instead of executed |
the |
|
the socket path in |
nginx with PHP-FPM
nginx has no .htaccess. Everything the shipped file does must be written in the server block, once. The block below
serves the public folder, redirects HTTP to HTTPS, routes /api/ to its own front controller and forwards the
Authorization header. It is the same shape as the short example in
configuration.php and .htaccess, with TLS and caching added.
server {
listen 80;
listen [::]:80;
server_name www.tallpine.example;
return 301 https://www.tallpine.example$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.tallpine.example;
ssl_certificate /etc/letsencrypt/live/www.tallpine.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.tallpine.example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
root /var/www/tallpine/public;
index index.php;
client_max_body_size 64m; # must not be lower than upload_max_filesize in the pool
add_header X-Content-Type-Options "nosniff" always;
# SVG files must not run script when opened directly (same as htaccess.txt)
location ~* \.svg$ {
add_header Content-Security-Policy "script-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
try_files $uri =404;
}
# Web Services API: its own front controller
location /api/ {
try_files $uri $uri/ /api/index.php?$args;
}
# Site and administrator
location / {
try_files $uri $uri/ /index.php?$args;
}
# Never execute PHP from writable or upload folders
location ~* ^/(images|media|files|cache|tmp|logs)/.*\.(php|phtml|phar)$ {
return 403;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
fastcgi_read_timeout 120s;
fastcgi_pass unix:/run/php/php8.4-fpm-tallpine.sock;
}
location ~ /\.ht { deny all; }
}
Points that decide whether it works:
-
fastcgi_paramsalready definesHTTPSfrom nginx’s$httpsvariable (onon a TLS listener, empty otherwise), which is how PHP learns that the request was secure when nginx itself terminates TLS. This is the line to revisit when TLS ends on a proxy in front (next sections). -
The
try_filesfallback is the whole of the SEF routing. Without the separate/api/location, API calls reach the site application and answer 404; the nginx notes of the User Guide predate Joomla 4 and do not mention it. After any change, request/api/index.php/v1/content/articlesand/api/v1/content/articles. -
If the document root is the classic Joomla directory rather than a public folder, add explicit denials for
configuration.php,libraries/,cli/,tmp/,cache/andadministrator/logs/. With a public folder they are not mapped to any URL. -
fastcgi_read_timeoutmatters for the long-running requests of the administrator (extension installation, Joomla Update, Smart Search indexing from the browser). The default of 60 seconds can end them early. -
Do not copy the sample on the User Guide’s nginx hosting-notes page unchanged. It listens on port 80 only, passes
fastcgi_passto a TCP port of an old PHP-FPM, includes bothfastcgi_paramsandfastcgi.conf, and itsreturn 403with anerror_pageline points to a file it does not provide.
The PHP-FPM pool
A dedicated pool per site keeps one site’s processes, memory limit and file ownership apart from another’s. The pool file
belongs in /etc/php/8.4/fpm/pool.d/tallpine.conf on Debian-style systems.
[tallpine]
user = tallpine
group = tallpine
listen = /run/php/php8.4-fpm-tallpine.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
; Settings that .htaccess cannot change under FPM
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 72M
php_admin_value[max_execution_time] = 120
php_admin_flag[display_errors] = off
php_admin_flag[expose_php] = off
; Keep session and upload scratch space private to the site
php_admin_value[session.save_path] = /var/lib/php/sessions/tallpine
php_admin_value[upload_tmp_dir] = /var/lib/php/uploads/tallpine
; Hand secrets to PHP as environment variables instead of writing them into files
env[TALLPINE_DB_PASSWORD] = $TALLPINE_DB_PASSWORD
Source: PHP Manual: FPM configuration
How to size and read it:
-
pm.max_childrenis the hard cap on simultaneous PHP requests. Divide the RAM that PHP may use by the average resident size of a worker (measure it on the real site) and leave room for MariaDB. A too-low value shows as 502 or 504 errors under load, withserver reached pm.max_childrenin the FPM log. -
post_max_sizemust be larger thanupload_max_filesize, and nginx’sclient_max_body_size(or Apache’sLimitRequestBody) must be at least as large as both, or the Media Manager fails before PHP is involved. The Developer Manual recommends amemory_limitof at least 256 MB. -
listen.ownerandlisten.groupmust name the web server’s user, or the socket is created but the web server cannot use it (error 502 in nginx,Service Unavailablein Apache). -
env[...]is how a variable becomes visible togetenv()whenclear_envis on (the default). Joomla’sconfiguration.phpis generated and rewritten by the Global Configuration screen, so reading the database password from the environment means editing the file by hand and never saving the screen afterwards; the subject is treated in configuration.php and .htaccess. -
Run PHP-FPM as a user different from the web server’s user and make code read-only for it where possible; only
images,files,tmp,cache,administrator/cacheandadministrator/logs(or their relocated equivalents) need to be writable. Automated and manual core updates need write access to the code tree, so the site owner and the FPM user are normally the same, as in the pool above.
HTTPS
Certificates and server settings
Joomla does not issue certificates. Use the web server or the proxy in front for that: Let’s Encrypt through certbot or
the ACME client built into Caddy or your platform, a certificate from the hosting provider, or the CDN’s edge certificate.
Certificates renew every few months, so the renewal job and a reload of the web server are part of the site’s operations
(see Backups and Recovery for what else to automate). Both server blocks above
accept TLS 1.2 and 1.3 only; take cipher lists from the Mozilla SSL configuration generator rather than writing them by
hand.
force_ssl: what the three values do
Global Configuration, Server tab, Force HTTPS, is the force_ssl property of configuration.php. The language string
of the screen says that "HTTPS must be enabled on your server or load balancer to utilise this option", and that
Behind Load Balancer must be on when TLS ends on a load balancer that talks plain HTTP to the web server.
| Value | Label | Effect in 6.2 |
|---|---|---|
|
None |
No redirect, no forced secure cookie. |
|
Administrator Only |
|
|
Entire Site |
As |
Three details are worth knowing:
-
The session service provider turns the secure-cookie option on for any value of
force_sslof 1 or more, so after enabling it every visitor and administrator receives the cookie with theSecureattribute. A browser then never sends that cookie over plain HTTP. -
When you save a non-zero value in the screen,
ApplicationModelfirst requestshttps://<your host>/from the server itself, with certificate verification and any configured outbound proxy turned off, and accepts the codes 200, 503, 401 and the 301 to 310 family. If the request fails it resets the value to0and shows a warning that SSL is not available. The check proves that something answers on port 443 of that host name from the server; it does not prove that the certificate is valid. -
Editing
configuration.phpby hand skips the check. Settingforce_ssl = 2on a host without a working certificate locks everybody out of both the front and back end; the way back is to edit the file again.
// configuration.php (fragment): HTTPS for the whole Tallpine site
public $force_ssl = 2;
public $live_site = ''; // leave empty; see below
public $cookie_domain = ''; // leave empty unless sessions must span sub-domains
public $cookie_path = '';
live_site should stay empty. When it is set, Joomla builds every absolute URL from it, so a value starting with http://
makes a site that is served over HTTPS generate insecure links and mixed-content warnings, and a staging copy that is
reached under a different host name keeps pointing at production.
Redirects, HSTS and mixed content
Use one layer for the redirect from HTTP to HTTPS: the port 80 server block (as above) is the cheapest, because it never
reaches PHP. Keep force_ssl on as well, because it sets the secure cookie flag and makes the application correct when a
request somehow arrives over HTTP, and because it is the only layer that exists when the proxy does not redirect.
HSTS tells browsers to refuse plain HTTP for the whole host for a period. It can be sent by the web server
(add_header Strict-Transport-Security "max-age=31536000" always;) or by the System - HTTP Headers plugin. That plugin
is enabled by default and sets X-Frame-Options and Referrer-Policy; HSTS, Content-Security-Policy and COOP are
available in its tabs, with HSTS and CSP off by default. Pick one place for HSTS; a header sent twice is a configuration
smell. The User Guide gives the caveats: the policy applies to the host and, if you enable it, to all sub-domains, each of
which then needs a valid certificate; browsers cache the policy for the configured max-age, so a mistake outlives the
fix; and preloading means submitting the domain to a browser list. Start with a short max-age.
Source: Joomla User Guide: HTTP Headers
Mixed content comes from content, not from the server: a hand-typed http:// image link in an article, a
live_site that starts with http://, or a template or module that hardcodes a scheme. Search for http:// in the
database (articles, modules, template parameters) after enabling HTTPS, and let the Content Security Policy report-only
mode of the HTTP Headers plugin list what is left.
Behind a reverse proxy, load balancer or CDN
What PHP must be told
When TLS ends in front of the web server, PHP receives plain HTTP from a proxy. Three facts get lost and Joomla needs all of them:
-
the scheme.
Joomla\CMS\Uri\Uri::getInstance()builds the request URI from the server variables. It decides the scheme in this order:$_SERVER['HTTPS']set and notoffmeans HTTPS; otherwise$_SERVER['HTTP_X_FORWARDED_PROTO']non-empty and different fromhttpmeans HTTPS; otherwise HTTP. This test does not depend onbehind_loadbalancer. A proxy that sendsX-Forwarded-Proto: httpsis therefore enough forforce_sslnot to redirect forever, and the proxy should overwrite the header from its own knowledge, never forward the client’s value. -
the host. The same method uses
$_SERVER['HTTP_HOST'], not anX-Forwarded-Hostheader. The proxy must pass the publicHostheader on (proxy_set_header Host $host;in nginx,ProxyPreserveHost Onin Apache). A proxy that rewritesHostto an internal name makes Joomla build links, redirects and theforce_sslredirect for that internal name. -
the client address.
REMOTE_ADDRis the proxy’s address. This is whatbehind_loadbalancerhandles.
Source: joomla-cms: Uri::getInstance()
behind_loadbalancer
The Behind Load Balancer switch (property behind_loadbalancer, Global Configuration, Server tab) is read by includes/framework.php, administrator/includes/framework.php and
api/includes/framework.php before anything else happens. When it equals 1 they call IpHelper::setAllowIpOverrides(true);
otherwise setAllowIpOverrides(false). The comment in the source says why: the default of the underlying library is to
allow overrides, which is the wrong default for a site that is not behind a trusted proxy.
With overrides allowed, IpHelper::getIp() takes the X-Forwarded-For header first, then Client-IP, then
REMOTE_ADDR. When the header contains a list (client, proxy1, proxy2) it takes the first non-empty entry, the leftmost
one. That is the entry a client can write itself. The consequence is the security rule of this setting:
-
enable it only when every request really passes through a proxy that replaces
X-Forwarded-Forwith the address it saw (proxy_set_header X-Forwarded-For $remote_addr;), or at least strips incoming copies; and -
make sure the web server cannot be reached directly from the internet, or an attacker can send the header to it and be recorded in the action log under any address, and slip past IP-based protections such as the brute-force limits of the administrator login or an IP allow-list.
The post-installation message Behind Load Balancer appears when Joomla sees an X-Forwarded-For or Client-IP header and
the switch is off, and its action turns the setting on. Read it as a hint, not as a check that your proxy is trustworthy.
A stricter alternative leaves behind_loadbalancer at 0 and makes the web server in front of PHP fix REMOTE_ADDR
itself, from trusted proxy addresses only. Joomla then sees the right address everywhere, including in code that reads
REMOTE_ADDR directly:
# backend nginx (behind the edge proxy at 10.0.0.10)
set_real_ip_from 10.0.0.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# TLS ended upstream: tell PHP the request was secure when the edge says so
map $http_x_forwarded_proto $tallpine_https {
default "";
https on;
}
# Apache backend with mod_remoteip
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.0.0.10
SetEnvIf X-Forwarded-Proto https HTTPS=on
Source: nginx: ngx_http_realip_module
In the nginx location that calls PHP, add fastcgi_param HTTPS $tallpine_https if_not_empty; after include fastcgi_params
so that $_SERVER['HTTPS'] is set exactly when the edge says the client used HTTPS. Either way, check the result rather
than assuming it: the System → System Information screen shows the server variables, and a login attempt appears in
Users → User Actions Log with the address the log recorded.
The edge proxy
The proxy that faces the internet terminates TLS and speaks plain HTTP to the web server that runs Joomla. For nginx as the edge:
server {
listen 443 ssl;
http2 on;
server_name www.tallpine.example;
ssl_certificate /etc/letsencrypt/live/www.tallpine.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.tallpine.example/privkey.pem;
client_max_body_size 64m;
location / {
proxy_pass http://10.0.0.20:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}
Source: nginx: ngx_http_proxy_module
Notes:
-
X-Forwarded-Foris set to$remote_addr, not appended with$proxy_add_x_forwarded_for, because Joomla trusts the leftmost entry. If a CDN sits in front of this proxy, restore the real client address first withset_real_ip_fromandreal_ip_header(the CDN’s published address ranges and its own header, for exampleCF-Connecting-IPfor Cloudflare, which Joomla does not read by itself) and then forward$remote_addr. -
Do not let the edge cache
/administratoror/api, and do not let it stripSet-Cookiefrom them. -
The same block shape works for HAProxy, Traefik, Caddy and cloud load balancers; the checklist is the same: public
Hostpreserved,X-Forwarded-Protoset from the real connection,X-Forwarded-Forset rather than appended, and a timeout long enough for the administrator.
For Apache as the edge: ProxyPreserveHost On, RequestHeader set X-Forwarded-Proto "https", ProxyPass / http://10.0.0.20:8080/
and ProxyPassReverse / http://10.0.0.20:8080/ (the last one rewrites Location headers of redirects).
Source: Apache HTTP Server: mod_proxy
Redirect loops and their causes
A redirect loop after enabling HTTPS is almost always a disagreement about the scheme between two layers:
| Symptom | Cause and fix |
|---|---|
The browser reports too many redirects after |
Joomla thinks the request is HTTP: the proxy sends neither |
Loop only with a CDN |
The CDN connects to the origin over plain HTTP and the origin web server redirects to HTTPS. Use a CDN mode that connects over HTTPS (with a certificate on the origin), or remove the web server redirect and rely on |
Loop only on the administrator |
|
Links and CSS use |
|
Redirects point to an internal host name |
The proxy rewrote |
Every visitor shows the proxy’s IP in the action log |
|
The administrator logs visitors in with the wrong address, or an attacker’s chosen address shows up |
|
CDNs
A CDN or a caching proxy takes static files, and optionally guest pages, away from PHP. The caching rules for Joomla are in Caching and Performance. For the HTTPS side:
-
Use the CDN’s full TLS mode (the connection from CDN to origin is encrypted and the origin certificate is checked); a mode that fetches from the origin over HTTP leaves a stretch of the path unprotected and is the usual source of loops.
-
Allow only the CDN’s address ranges to reach the origin ports (firewall or security group). That also makes it safe to enable
behind_loadbalancer. -
Bypass the cache for
/administrator,/api, anyPOSTand any request that carries the session cookie. -
Do not block
/apifor the public internet if you use Automated Core Updates (next section).
Reaching the site for Automated Core Updates
Automated Core Updates (introduced in 5.4 and 6.0, on by default for new installations and off for upgraded sites) update a site for minor, patch and security releases without an administrator. They work the other way round from what the name suggests: a server run by the Joomla project triggers the update by calling the site, and the site downloads and applies the update itself. The updates are TUF-verified (a security design in the core since 5.1).
The Joomla Update options say it plainly: "Automated updates are only available for sites that are publicly accessible on the internet. If your site is behind a firewall or not publicly accessible, such as a local test environment, you will need to update manually." Enabling the feature registers the site with the project’s service, which receives the site URL, the PHP version, the database type and version, the CMS version and the server operating system. If registration finds the site unreachable, Joomla shows "The automated update service is unavailable for your site as it’s not accessible from the internet. Automated updates have been disabled."
What this means for the web server configuration of the Tallpine site:
-
The public host name must be reachable on port 443 from the internet, with a certificate the public internet accepts. The Update options also show the time the service last reached the site; if that date is old, saving the configuration again repeats the registration.
-
The service talks to the Joomla Update web-service endpoints under
/api, authenticated by an update token that the site stores. So/apimust be routed (the/api/location of the nginx block, the rewrite rule ofhtaccess.txt) and not blocked by an IP allow-list, HTTP basic authentication or a WAF rule that rejects unknown clients. -
The site also needs outgoing HTTPS to
update.joomla.org. Behind a mandatory outbound proxy, fill in the Proxy settings in Global Configuration (proxy_enable,proxy_host,proxy_port, optionallyproxy_userandproxy_pass). These settings are for the site calling out; they have nothing to do with a reverse proxy in front of it. -
Only the Default update channel and the Stable minimum stability are supported; with another channel or stability the options show an information note that automated updates are not supported.
-
A site that is private on purpose (a staging copy, an intranet, a pre-launch site behind HTTP authentication) should disable the feature explicitly and update through the CLI or the administrator, as described in Updating Joomla and Automated Core Updates.
Some operators restrict /api to their own network because they use the Web Services API only for the headless front end.
That choice is compatible with Automated Core Updates only if they keep the update endpoints open to the service; the safer
pattern is to expose /api publicly, give every API user a token with the least privileges, and leave the rest to the
Joomla permission system (see Security Hardening, MFA and
Passkeys).
Containers and Kubernetes
Compose with nginx and the FPM image
The official image has an fpm variant that runs PHP-FPM only on port 9000 and serves no HTTP. A second container serves
the files. Both mount the same volume. The Docker concepts used here (volumes, networks, Compose) are in
Docker and Docker networking; the Joomla image and the
Apache variant are in Local Development with Docker.
name: tallpine-prod
services:
php:
image: joomla:6.2-php8.4-fpm
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
JOOMLA_DB_HOST: db
JOOMLA_DB_USER: tallpine
JOOMLA_DB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
JOOMLA_DB_NAME: tallpine
volumes:
- joomla_data:/var/www/html
web:
image: nginx:1.29-alpine
restart: unless-stopped
depends_on:
- php
ports:
- "127.0.0.1:8080:80" # only the host's edge proxy may reach it
volumes:
- joomla_data:/var/www/html:ro
- ./nginx/tallpine.conf:/etc/nginx/conf.d/default.conf:ro
db:
image: mariadb:11.8
restart: unless-stopped
environment:
MARIADB_DATABASE: tallpine
MARIADB_USER: tallpine
MARIADB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
MARIADB_RANDOM_ROOT_PASSWORD: "1"
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
retries: 10
volumes:
joomla_data:
db_data:
The server block in nginx/tallpine.conf is the one from the nginx section, without the TLS lines, with
root /var/www/html;, the explicit denials for the classic layout, fastcgi_pass php:9000;, and the $tallpine_https
map above so that force_ssl sees HTTPS when the host’s edge proxy sets X-Forwarded-Proto. Binding the published port
to 127.0.0.1 is what keeps the web container from being a way around the proxy, which behind_loadbalancer relies on.
Keep the database password in an untracked .env file as in the development page.
Kubernetes ingress
On Kubernetes the ingress controller is the edge proxy. The Joomla pod runs the FPM container with an nginx sidecar or the
Apache image; the controller sends X-Forwarded-For, X-Forwarded-Proto and Host, and the Joomla pod trusts the controller’s pod
network only. A few annotations matter, shown here for ingress-nginx:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tallpine
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "64m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
spec:
ingressClassName: nginx
tls:
- hosts: [www.tallpine.example]
secretName: tallpine-tls
rules:
- host: www.tallpine.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tallpine-web
port:
number: 80
Source: Kubernetes: Ingress
With more than one replica, the pods share state: the default database session handler works unchanged; a shared
volume (or object storage through a plugin) is needed for images and media; the cache needs a shared handler such as
Redis (Redis and Multi-site Options
and Scaling); and every replica must carry the same secret in configuration.php. Joomla Update replaces files inside
the pod’s volume, so a rolling image upgrade is the cleaner path on Kubernetes: build a new image, roll it out with the
usual strategy (see Kubernetes) and run database migrations once with
php cli/joomla.php core:update from a job. If the pods are read-only, leave Automated Core Updates off for the site and
update by image.
Security checklist
-
HTTPS everywhere: redirect on port 80,
force_ssl = 2, secure cookies, HSTS once the certificate renewal is reliable. -
Document root is
public; nothing else is mapped to a URL. -
No PHP execution below
images,media,files,cache,tmpandlogs. -
behind_loadbalancer = 1only when the origin is reachable only through the proxy and the proxy setsX-Forwarded-Foritself. -
The
Authorizationheader reaches PHP; the Web Services API token plugin is enabled and the Basic Auth plugin disabled on fresh 6.2 installations. -
The PHP-FPM pool runs as a separate user, with
display_errorsoff and its own session and upload directories. -
Security headers (
nosniff, framing, referrer policy) come from the web server or the HTTP Headers plugin, once. -
Administrator access protected by multi-factor authentication or passkeys, independent of the transport; see Security Hardening, MFA and Passkeys.
-
Updates applied promptly: either Automated Core Updates with the site reachable, or a scheduled manual process.
Version notes and corrections of older material
-
Requirements for Joomla 6.x are Apache 2.4, nginx 1.26 supported and 1.29 recommended, IIS 10, PHP 8.3 minimum and 8.4 recommended. Earlier lines accepted older nginx and PHP versions; minimum requirements rise only in the first release of a major.
-
The Web Services API and its
api/index.phpfront controller exist since Joomla 4.0. Any nginx or Apache sample that predates Joomla 4, including the hosting notes of the User Guide, has no/api/rule. -
The public folder arrived in Joomla 5.0; it changes the document root of every configuration on this page.
-
Automated Core Updates arrived in 5.4 and 6.0; TUF verification of core updates has been in place since 5.1. They are not available for a site behind a firewall.
-
Older books such as Joomla! 4 Masterclass (2023) target Joomla 4, which reached end of life in October 2025, and do not describe reverse proxies, forwarded headers, the public folder, PHP-FPM pools or Automated Core Updates. Developing Extensions for Joomla! 5 (2023) deals with extension development and includes no server configuration. Neither covers Joomla 6, and the tutorials that circulate with a Joomla 3 era
.htaccessor an nginx file without an/api/location produce a site whose API silently fails. -
Several wiki pages on docs.joomla.org that discuss SSL and proxies mark themselves obsolete; the User Guide and the Developer Manual are the current references, and for
force_sslandbehind_loadbalancerthe source tree and the language strings of Global Configuration are the authoritative description.