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 to api/index.php;

  • run .php files through PHP (PHP-FPM in this page) and never serve their source;

  • pass the Authorization request 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.

flowchart LR V[Visitor] -->|HTTPS| C[CDN or load balancer<br/>TLS ends here] C -->|HTTP plus X-Forwarded headers| W[nginx or Apache<br/>static files and routing] W -->|FastCGI| F[PHP-FPM pool<br/>Joomla code] F --> D[(MariaDB)] F -.->|outbound HTTPS| U[update.joomla.org] U -.->|Automated Core Updates<br/>call the public site| C

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 +FollowSymlinks and Options -Indexes (no directory listings);

  • X-Content-Type-Options: nosniff, and a Content-Security-Policy: script-src 'none' header for .svg files so that an uploaded SVG cannot run script when opened directly;

  • a query-string filter that returns 403 for base64_encode(...) calls, <script> tags, GLOBALS and _REQUEST manipulation;

  • RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}], which copies the Authorization header 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 to api/index.php; every other request that is not a real file or directory goes to index.php;

  • a fallback redirect from / to /index.php/ when mod_rewrite is missing;

  • serving precompressed .css.gz and .js.gz files 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; /trails/alpine gives an Apache 404

AllowOverride None, mod_rewrite not loaded, or the file is still called htaccess.txt

500 error on every request after copying .htaccess

a directive not allowed by AllowOverride, or a module such as mod_headers missing (the IfModule guards avoid this for the shipped file)

API calls answer 401 although the token is valid

the Authorization header never reached PHP; keep the rewrite rule above or CGIPassAuth On

PHP source is downloaded instead of executed

the SetHandler block does not match, or FPM is not running

Service Unavailable

the socket path in SetHandler differs from the pool’s listen value, or its permissions do not allow the Apache user

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_params already defines HTTPS from nginx’s $https variable (on on 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_files fallback 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/articles and /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/ and administrator/logs/. With a public folder they are not mapped to any URL.

  • fastcgi_read_timeout matters 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_pass to a TCP port of an old PHP-FPM, includes both fastcgi_params and fastcgi.conf, and its return 403 with an error_page line 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

How to size and read it:

  • pm.max_children is 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, with server reached pm.max_children in the FPM log.

  • post_max_size must be larger than upload_max_filesize, and nginx’s client_max_body_size (or Apache’s LimitRequestBody) must be at least as large as both, or the Media Manager fails before PHP is involved. The Developer Manual recommends a memory_limit of at least 256 MB.

  • listen.owner and listen.group must name the web server’s user, or the socket is created but the web server cannot use it (error 502 in nginx, Service Unavailable in Apache).

  • env[...] is how a variable becomes visible to getenv() when clear_env is on (the default). Joomla’s configuration.php is 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/cache and administrator/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

0

None

No redirect, no forced secure cookie.

1

Administrator Only

AdministratorApplication::route() redirects a non-HTTPS request to the same URL with https and a 301. isHttpsForced() is true for the administrator client. The session cookie is marked Secure.

2

Entire Site

As 1 for the administrator, and SiteRouter attaches a parse rule (parseCheckSSL) that redirects non-HTTPS site requests with a 301. Links generated for e-mail (registration, password reset, privacy requests, feeds) are built with HTTPS.

Three details are worth knowing:

  • The session service provider turns the secure-cookie option on for any value of force_ssl of 1 or more, so after enabling it every visitor and administrator receives the cookie with the Secure attribute. A browser then never sends that cookie over plain HTTP.

  • When you save a non-zero value in the screen, ApplicationModel first requests https://<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 to 0 and 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.php by hand skips the check. Setting force_ssl = 2 on 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.

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:

  1. 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 not off means HTTPS; otherwise $_SERVER['HTTP_X_FORWARDED_PROTO'] non-empty and different from http means HTTPS; otherwise HTTP. This test does not depend on behind_loadbalancer. A proxy that sends X-Forwarded-Proto: https is therefore enough for force_ssl not to redirect forever, and the proxy should overwrite the header from its own knowledge, never forward the client’s value.

  2. the host. The same method uses $_SERVER['HTTP_HOST'], not an X-Forwarded-Host header. The proxy must pass the public Host header on (proxy_set_header Host $host; in nginx, ProxyPreserveHost On in Apache). A proxy that rewrites Host to an internal name makes Joomla build links, redirects and the force_ssl redirect for that internal name.

  3. the client address. REMOTE_ADDR is the proxy’s address. This is what behind_loadbalancer handles.

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-For with 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

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;
    }
}

Notes:

  • X-Forwarded-For is 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 with set_real_ip_from and real_ip_header (the CDN’s published address ranges and its own header, for example CF-Connecting-IP for Cloudflare, which Joomla does not read by itself) and then forward $remote_addr.

  • Do not let the edge cache /administrator or /api, and do not let it strip Set-Cookie from them.

  • The same block shape works for HAProxy, Traefik, Caddy and cloud load balancers; the checklist is the same: public Host preserved, X-Forwarded-Proto set from the real connection, X-Forwarded-For set 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).

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 force_ssl = 2

Joomla thinks the request is HTTP: the proxy sends neither HTTPS nor X-Forwarded-Proto: https to PHP. Send the header, or set HTTPS in the FastCGI parameters.

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 force_ssl with the forwarded header.

Loop only on the administrator

force_ssl = 1 while the proxy sends X-Forwarded-Proto: http, or a second redirect rule on /administrator.

Links and CSS use http:// on an HTTPS page

live_site starts with http://, or the scheme header is missing and Joomla builds HTTP URLs.

Redirects point to an internal host name

The proxy rewrote Host. Preserve it.

Every visitor shows the proxy’s IP in the action log

behind_loadbalancer is off and the web server did not restore the address.

The administrator logs visitors in with the wrong address, or an attacker’s chosen address shows up

behind_loadbalancer is on but the proxy appends to an incoming X-Forwarded-For or the web server is also reachable directly.

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, any POST and any request that carries the session cookie.

  • Do not block /api for 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 /api must be routed (the /api/ location of the nginx block, the rewrite rule of htaccess.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, optionally proxy_user and proxy_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

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, tmp and logs.

  • behind_loadbalancer = 1 only when the origin is reachable only through the proxy and the proxy sets X-Forwarded-For itself.

  • The Authorization header 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_errors off 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.php front 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 .htaccess or 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_ssl and behind_loadbalancer the source tree and the language strings of Global Configuration are the authoritative description.