configuration.php and .htaccess

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 was written against Joomla 6.2.x and PHP 8.3+ and checked on 2026-10-30. It describes the two plain files that decide how a Joomla site starts and how its URLs reach PHP: configuration.php, the PHP class that holds every setting of the Global Configuration screen plus the database credentials, and htaccess.txt, the Apache rewrite file that the User Guide tells administrators to rename to .htaccess. The running example is the fictitious Tallpine Hiking Club site, which lives in a Docker Compose stack locally and on two hosts (staging and production).

Who needs this page: the administrator who rotates a database password or moves the site, the site builder who enables clean URLs, and the developer who deploys the same code to several environments. The screen that edits most of the file is described tab by tab in Global Configuration; this page is about the file itself, the properties the screen does not show, and the web server rules around it. The web server side (virtual hosts, TLS, reverse proxy) is in Web Server, Reverse Proxy and HTTPS, the code tree layout in Installation and the Public Folder.

How the file is used

The installer writes configuration.php in the site root as the last step of the installation. The file contains one class, JConfig, whose public properties are the settings. During bootstrap, includes/framework.php checks that the file exists and is larger than ten bytes, includes it, and builds the application configuration from an instance of that class. If the file is missing and no installation folder exists, Joomla stops with the message that no configuration file was found and no installation code is available; if the installation folder is still present, the web installer starts. The location is held in the constant JPATH_CONFIGURATION, which includes/defines.php sets to the site root. A defines.php file placed next to index.php is included first and can override it, which is how some hosts keep the file outside the code tree.

flowchart TD R[Request reaches index.php] --> D{defines.php exists?} D -- yes --> DI[Include defines.php<br/>may redefine JPATH_CONFIGURATION] D -- no --> DF[includes/defines.php<br/>JPATH_CONFIGURATION = site root] DI --> F DF --> F{configuration.php exists<br/>and is larger than 10 bytes?} F -- no --> I[Web installer, or the<br/>no-configuration-file message] F -- yes --> C[Include configuration.php<br/>class JConfig] C --> A[Application reads the properties<br/>with the get method] A --> P[IpHelper overrides, error handler,<br/>database, session, cache, router]

Two consequences matter in practice. The file is plain PHP, so a syntax error (a missing semicolon after an edit) takes the whole site down with a blank page or a fatal error before any Joomla error page can be rendered. And because the class is instantiated on every request, anything that is valid PHP in a property default or in a constructor is evaluated every time; that is useful for reading environment variables (see below) and harmful for slow code.

The properties are read through the application, $app->get('name'), never by reading the file again. Extensions that need a setting use the application object (or the injected configuration), and Factory::getConfig() has been deprecated for that reason. The same registry backs the CLI commands config:get and config:set.

The anatomy of JConfig

The installer template is installation/configuration.php-dist in the source tree. It groups the properties with comments; the groups below follow it. Properties in parentheses are the ones a normal installation fills in differently from the template.

Group Properties Meaning

Site

offline, offline_message, display_offline_message, offline_image, sitename, editor, captcha, list_limit, access, frontediting

Offline mode and its message, site name, default editor and captcha plugin, list length, default access level, front-end editing.

Database

dbtype, host, user, password, db, dbprefix, dbencryption, dbsslverifyservercert, dbsslkey, dbsslcert, dbsslca, dbsslcipher

Driver (mysqli, pdomysql or pgsql), server, credentials, database name, table prefix and the TLS options of the connection.

Server

secret, gzip, error_reporting, helpurl, tmp_path, log_path, live_site, force_ssl

The site secret, output compression, error level, help URL template, writable folders, canonical URL and HTTPS enforcement.

Locale and session

offset, lifetime, session_handler, shared_session, session_filesystem_path, session_memcached_server_*, session_metadata, session_redis_*

Time zone, session lifetime in minutes and the session store with its connection settings.

Mail

mailonline, mailer, mailfrom, fromname, massmailoff, replyto, replytoname, sendmail, smtpauth, smtpuser, smtppass, smtphost, smtpsecure, smtpport

Outgoing mail; the credentials are secrets (see Mail Templates and Email Settings).

Cache

caching, cachetime, cache_handler, cache_platformprefix, memcached_*, redis_*

System cache and its store (see Caching and Performance).

Logging

log_categories, log_category_mode, log_deprecated, log_everything, log_priorities

What the logger writes into log_path.

CORS

cors, cors_allow_headers, cors_allow_methods, cors_allow_origin

Cross-origin headers for the Web Services API; the template allows every origin when CORS is turned on, which is rarely right for production.

Proxy and debug

proxy_enable, proxy_host, proxy_port, proxy_user, proxy_pass, debug, debug_lang, debug_lang_const

Outgoing HTTP proxy used by Joomla itself (update checks, extension downloads) and the debug switches.

Meta, SEO and feeds

MetaDesc, MetaAuthor, MetaVersion, MetaRights, robots, sitename_pagetitles, sef, sef_rewrite, sef_suffix, unicodeslugs, feed_limit, feed_email

Default meta data and URL options (see SEO, SEF URLs, Redirects and Schema.org).

Cookies and miscellaneous

cookie_domain, cookie_path, asset_id, behind_loadbalancer

Cookie scope, the root asset id of the ACL tree and the reverse-proxy switch.

A few properties exist in the file but not on the Global Configuration screen. dbprefix, dbtype, host, user, db and the dbssl* values can only be changed by editing the file or with config:set; live_site has no field at all; and asset_id must be left alone because it ties the file to the root row of the permissions tree in the database. Older installations can contain extra properties that current releases ignore, such as ftp_* values from the days of the FTP layer or the root_user property that the installer left behind in Joomla 3; the Global Configuration model can remove root_user on request, so a file with one is not an error.

The settings that matter most

secret

secret is a random string generated by the installer (16 characters of UserHelper::genRandomPassword()). Joomla mixes it into hashes rather than using it as an encryption key, and several features depend on it:

  • the session name and the hash that identifies the application (md5($secret . $seed))

  • the cache storage hash

  • the language cookie name of the language filter

  • token-based features: the API token plugin and the user token plugin derive their HMACs from the site secret

  • the WebAuthn credential repository and the Proof-of-Work captcha added in 6.1, which signs its challenges with it

The practical rules are therefore the following. Generate a different value for each environment. Never copy the staging file to production. Do not commit it; the real file is produced at deployment. After a leak, change it: every logged-in session ends, existing API tokens issued for users stop validating and have to be regenerated, and cached data becomes unreadable and is rebuilt. Rotating the secret is a legitimate emergency step, but treat it as a maintenance-window operation. The secret is among the fields that the Global Configuration model refuses to show in the form, which is why the screen never discloses it.

Database settings

dbtype is mysqli, pdomysql or pgsql. The old mysql and pgsql extension drivers disappeared in Joomla 4; when a file still says mysql, the database service provider switches to the PDO MySQL driver, and Microsoft SQL Server has not been supported since 4.0. host accepts a host name, host:port, or a socket. The Docker examples in this documentation use the Compose service name (db). dbprefix is a short string (letters, digits and underscores, starting with a letter, at most 15 characters) that is put in front of every table name. A random prefix only makes the table names harder to guess; it is not a security control and it must not be changed in the file alone, because the tables in the database keep their old names.

dbencryption, dbsslverifyservercert, dbsslkey, dbsslcert, dbsslca and dbsslcipher configure TLS between PHP and the database server. Managed databases usually require dbencryption = 1 and a CA file; the Global Configuration screen checks the connection before it saves these values, and a wrong value is the most common reason that a site stops working after an edit.

tmp_path and log_path

tmp_path is where Joomla unpacks extension packages, stores uploads and writes update packages. log_path is where the log files (joomla_update.php, everything.php, deprecated.php and the files of extensions that register a logger) are written. The defaults are the tmp folder in the site root and administrator/logs. The template in configuration.php-dist ships /tmp and /administrator/logs as placeholders, which the installer replaces with absolute paths.

The Global Configuration form tells administrators to use absolute paths that are writable by PHP and warns against the system-wide /tmp. The reason is that a shared temporary directory exposes extracted packages and uploads to other users of the host. The log folder description adds that if the folder is missing or not writable, Joomla will not load at all. When saving, the model checks both folders; for a custom path that is not writable it falls back to the default folder and shows a warning. A path that was valid on the old host and does not exist on the new one is the classic symptom after a move (see below).

// Typical values on the Tallpine production host (absolute paths)
public $tmp_path = '/srv/tallpine/tmp';
public $log_path = '/srv/tallpine/logs';

Keep both folders outside the web root when the public folder layout is used (see Installation and the Public Folder); otherwise protect them with the rules in the web server sections below, because log files can contain user names, IP addresses and stack traces.

live_site

live_site is an optional full URL such as https://tallpine.example. When it is empty, which is the normal case, Joomla derives the base URL from the request: the scheme from HTTPS or HTTP_X_FORWARDED_PROTO, the host from HTTP_HOST and the path from the script name. When it is set, Uri::base() uses it instead and upgrades http:// to https:// when the request itself is secure. The command-line application also reads it, and falls back to a placeholder (https://joomla.invalid/set/by/console/application) if it is empty, which is why links generated by scheduled tasks and console commands can be wrong until it is set. Pitfalls:

  • A wrong live_site breaks assets, redirects and the administrator login in every environment that does not use that exact URL, so a file copied from production to staging with live_site still set sends staging users to production.

  • The value must not end with a slash and must not include /administrator.

  • Leave it empty for a site reachable under several host names (multi-domain, multilingual by domain).

  • Set it, or let the environment set it, only when the application is behind something that rewrites the host or path and the headers cannot be corrected, or when a command-line job must generate absolute URLs.

force_ssl

force_ssl takes 0 (none), 1 (administrator only) or 2 (entire site). The installer writes 0. The effect is spread over several places:

  • the administrator application redirects a non-HTTPS request with a 301 when the value is 1 or higher

  • the site router attaches a rule that redirects non-HTTPS requests when the value is 2

  • the session and cookie code marks cookies as secure when HTTPS is forced for the client (isHttpsForced() is true for the site client at 2 and for the administrator client at 1 or more)

Before the screen saves a non-zero value it makes an HTTPS request to the site’s own host (ignoring certificate errors) and resets the value to 0 with a warning if the request does not succeed with an expected status. Editing the file bypasses that check, so enabling force_ssl in the file on a server without a certificate locks everybody out. The reverse also applies: behind a TLS-terminating proxy that talks plain HTTP to PHP, enabling it without the next setting produces a redirect loop.

behind_loadbalancer

behind_loadbalancer (the Behind Load Balancer switch) tells Joomla that the connection comes from a trusted proxy. includes/framework.php, administrator/includes/framework.php and api/includes/framework.php read it very early and call IpHelper::setAllowIpOverrides(true) when it equals 1, and setAllowIpOverrides(false) otherwise. With it on, headers such as X-Forwarded-For may replace REMOTE_ADDR, so that the user’s real address appears in the action log, in brute-force protections and in the Privacy tools. With it off (the default), those headers are ignored. The framework file explains the reason: the library default is to allow overrides, which is wrong for a site that is not behind a trusted proxy, because any visitor could then forge their address.

Therefore turn it on only when every request really passes through a proxy that sets the headers itself and strips them from incoming requests. The post-installation message "Behind Load Balancer" shows up when Joomla sees an X-Forwarded-For or Client-IP header while the setting is off. The setting concerns client addresses; the scheme is read separately from HTTP_X_FORWARDED_PROTO when the URI is built, so the proxy must send that header as well. The matching proxy configuration is in Web Server, Reverse Proxy and HTTPS.

A configuration.php for several environments

A file that is committed or baked into an image cannot contain real credentials. PHP allows an expression in a constructor, so the JConfig class can read the environment. The following file is the deployment template for the Tallpine site; nothing in it is a real secret, and the values come from variables set by Docker Compose, Kubernetes or the host’s service manager.

<?php
/**
 * Deployment template. Do not edit through Global Configuration
 * (see the pitfall about saving below).
 */
class JConfig
{
    public $offline = false;
    public $sitename = 'Tallpine Hiking Club';
    public $editor = 'tinymce';
    public $list_limit = 20;
    public $access = 1;

    public $dbtype = 'mysqli';
    public $host = 'db';
    public $user = 'tallpine';
    public $password = '';
    public $db = 'tallpine';
    public $dbprefix = 'tp_';
    public $dbencryption = 0;

    public $secret = '';
    public $gzip = false;
    public $error_reporting = 'none';
    public $tmp_path = '/srv/tallpine/tmp';
    public $log_path = '/srv/tallpine/logs';
    public $live_site = '';
    public $force_ssl = 0;
    public $behind_loadbalancer = false;

    public $offset = 'UTC';
    public $lifetime = 15;
    public $session_handler = 'database';
    public $caching = 1;
    public $cache_handler = 'file';
    public $cachetime = 15;

    public $mailer = 'smtp';
    public $smtphost = 'mailpit';
    public $smtpport = 1025;
    public $smtpauth = false;
    public $smtpuser = '';
    public $smtppass = '';
    public $smtpsecure = 'none';

    public $debug = false;
    public $sef = true;
    public $sef_rewrite = true;
    public $asset_id = 1;

    public function __construct()
    {
        $this->host = getenv('TALLPINE_DB_HOST') ?: $this->host;
        $this->password = getenv('TALLPINE_DB_PASSWORD') ?: 'CHANGE_ME';
        $this->secret = getenv('TALLPINE_SECRET') ?: 'CHANGE_ME_TO_A_LONG_RANDOM_VALUE';
        $this->smtppass = getenv('TALLPINE_SMTP_PASSWORD') ?: '';
        $this->live_site = getenv('TALLPINE_LIVE_SITE') ?: '';
        $this->force_ssl = (int) (getenv('TALLPINE_FORCE_SSL') ?: 0);
        $this->behind_loadbalancer = getenv('TALLPINE_BEHIND_PROXY') === '1';
    }
}

Compose passes the variables (a .env file kept out of version control, or a secrets mechanism, supplies the values):

services:
  joomla:
    image: joomla:6.2-php8.3-apache
    environment:
      TALLPINE_DB_HOST: db
      TALLPINE_DB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
      TALLPINE_SECRET: ${TALLPINE_SECRET:?set it in .env}
      TALLPINE_BEHIND_PROXY: "1"
      TALLPINE_FORCE_SSL: "2"
    volumes:
      - ./deploy/configuration.php:/var/www/html/configuration.php:ro

Mounting the file read-only is the strongest way to keep Global Configuration from rewriting it, and the screen’s notice that configuration.php is not writable is then expected rather than a fault. The official image itself reads JOOMLA_DB_HOST, JOOMLA_DB_USER, JOOMLA_DB_PASSWORD (or JOOMLA_DB_PASSWORD_FILE), JOOMLA_DB_NAME and JOOMLA_DB_TYPE during its first start, and writes a normal file; the image variables only seed the installation, and changing them afterwards does nothing. Container operations are covered in Compose for local development and Local Development with Docker; on Kubernetes, the database password and the secret belong in a Secret object, described in Kubernetes Secrets. When sessions or cache move to Redis, the session_redis_* and redis_* properties hold the host, port and AUTH value (a secret too); see Redis Reference for the server side.

The pitfall that the comment in the template mentions is how Global Configuration saves. The model builds the new file from the submitted form data, and for the protected fields (password, secret, smtppass, the OAuth2 mail values and the Redis AUTH values) it takes the current values from the running application. When an administrator presses Save, the values that the constructor read from the environment are written into the file as literals. On a deployment that is meant to be environment-driven, mount the file read-only or never save the screen on that host.

Editing safely and permissions

Joomla keeps the file read-only. When Global Configuration saves, it switches the file to mode 0644 if the PHP user owns it, rewrites it as a PHP class named JConfig, and sets it back to 0444. The rewritten file is regenerated from the submitted values, which has two consequences: comments and custom code in the file are lost, and properties that are not known to the form are kept only if they were loaded. Hand-written constructors, as in the previous section, do not survive a save.

The recommended procedure for a manual edit is:

  1. Make a copy: cp configuration.php configuration.php.bak, outside the web root if possible (a .bak file below the web root is served as plain text on a server that does not run PHP for that extension).

  2. Make the file writable for the edit, change it, and run php -l configuration.php to catch syntax errors.

  3. Restore the mode and the owner.

  4. Clear the cache and reload the site.

cd /srv/tallpine/public_code
sudo -u deploy cp configuration.php /srv/tallpine/backup/configuration.php.$(date +%F)
sudo -u deploy chmod 0644 configuration.php
sudoedit configuration.php
php -l configuration.php
sudo -u deploy chmod 0444 configuration.php

The command line avoids the editor altogether and validates what it writes:

php cli/joomla.php config:get sef sef_rewrite force_ssl behind_loadbalancer
php cli/joomla.php config:set sef_rewrite=true force_ssl=2 behind_loadbalancer=true
php cli/joomla.php config:set tmp_path=/srv/tallpine/tmp log_path=/srv/tallpine/logs

config:set takes option=value pairs, checks the database connection when database options change, validates the prefix against the same rules as the installer, and calls the same model as the screen, so it also runs the force_ssl check, the path checks and the protected-field handling. More commands are in Command Line Interface.

The permission model is as follows.

Item Recommended mode Reason

configuration.php

0444 (0440 when the web server group differs from the owner) or 0400

The file holds the database password and the secret; PHP needs to read it, rarely to write it.

tmp, cache, administrator/cache, log folder

0755 owned by the PHP user

These are the folders that PHP must write to.

Everything else in the code tree

0755 for folders and 0644 for files, owned by a deploy user and not writable by PHP

Limits what an exploited extension can overwrite; the cost is that the web updater and the extension installer cannot work, so update through the deployment pipeline or run the CLI as the deploy user.

Never keep configuration.php in version control with real values. Keep a template in the repository (as above) and keep the real file in the secret store. The repository’s secret scanner will otherwise flag the file, and a leaked file means a leaked database password and secret. Hosting panels often show the file in a file manager with 0644; check the mode after every restore, because a restored backup rewrites it. Joomla’s Security Centre and the hardening checklist in Security Hardening, MFA and Passkeys list the same measures.

Errors caused by the file

Symptom Likely cause

Blank page after an edit

PHP syntax error; run php -l configuration.php.

The installer appears again

configuration.php is missing, empty or smaller than ten bytes (a failed write leaves an empty file).

"Error displaying the error page" or database connection error

Wrong host, user, password, db, dbtype, or TLS values (dbencryption).

"Joomla will not load" with a log folder message

log_path points to a folder that does not exist or is not writable.

Extension installation fails with a temp folder error

tmp_path is wrong after a move, or points to a folder the PHP user cannot write to.

Redirect loop on every page

force_ssl is 2 behind a TLS-terminating proxy without X-Forwarded-Proto, or a proxy and the site both redirect.

Wrong client IP in logs, or all users share one address

behind_loadbalancer is off behind a proxy, or on without a proxy that strips forged headers.

Everybody is logged out after a deployment

secret or session_handler changed, or the session path or database is different on the new host.

Settings revert after saving

The file is mounted read-only or owned by another user; the save notice says it could not be written.

htaccess.txt

Joomla ships htaccess.txt in the site root. Apache only reads a file called .htaccess (the name is set by the AccessFileName directive), so the file is activated by copying or renaming it. If the host already provides an .htaccess with its own rules, merge the two rather than replacing it. The User Guide recommends editing .htaccess rather than htaccess.txt, because updates can overwrite the latter, and after a Joomla update it is worth comparing your .htaccess with the current htaccess.txt, since the file changes between versions (the SEF section, for example, gained the API application rules in Joomla 4). Older books and tutorials show the Joomla 3 or 4 contents of this file; Developing Extensions for Joomla! 5 (Packt, 2023) and Joomla! 4 Masterclass (Packt, 2023) predate the current layout and do not describe the API rewrite or the pre-compressed asset section.

cd /var/www/tallpine
cp htaccess.txt .htaccess
php cli/joomla.php config:set sef_rewrite=true

The 6.2 file has these sections, in order.

Section What it does

Options

Options FollowSymlinks` (required by `mod_rewrite` on some hosts) and `Options -Indexes`; `+IndexIgnore * under mod_autoindex. Comment out +FollowSymlinks when the host forbids changing options and returns a 500 error. It is needed again for the public folder, whose files are symbolic links.

Security headers

X-Content-Type-Options: nosniff on every response (needs mod_headers); a commented-out pair of Cross-Origin-Resource-Policy and Cross-Origin-Embedder-Policy headers; and a Content-Security-Policy: script-src 'none' header on .svg files so that an uploaded SVG cannot run script when opened directly.

Common exploits

Four RewriteCond lines that match query strings containing base64_encode(...), a script tag (plain or URL-encoded), GLOBALS assignments and _REQUEST assignments; matching requests receive 403. This is a crude filter and not a web application firewall; comment it out only when legitimate requests are blocked.

Custom redirects

An empty area for your own 301 rules, such as the canonical www redirect (see the User Guide page on adding www to a URL).

RewriteBase

A commented-out RewriteBase / for hosts where URL and file path differ. It is needed in some subdirectory installations.

Core SEF section

First, a rule that copies the Authorization header into an environment variable (required for the Web Services API under FastCGI). Then the API rule: a request that starts with /api/, is not /api/index.php, and is neither a file nor a directory is rewritten to api/index.php. Finally the front-end rule: anything that is not /index.php and not an existing file or directory goes to index.php.

Fallback without mod_rewrite

When mod_rewrite is not loaded and mod_alias is, the start page is redirected temporarily to /index.php/ so that the site still works with index.php in the URLs.

GZIP and Brotli

Serves a pre-compressed .css.gz or .js.gz file when it exists and the browser accepts gzip, sets the correct type and Content-Encoding, and prevents double compression. If the layout breaks and the browser console reports ERR_CONTENT_DECODING_FAILED, the server already compresses these files and the block must be disabled.

The file is only half of the story. The Use URL Rewriting option of Global Configuration (the sef_rewrite property) removes index.php from generated URLs; the rewrite rules make those URLs work. Turning the option on without the file, or on a server where AllowOverride is None, makes every page except the home page return 404. The related checklist (FollowSymlinks, AllowOverride All, the module list) is in SEO, SEF URLs, Redirects and Schema.org. OpenLiteSpeed reads .htaccess but caches it: restart the server after a change.

Security reasoning: Apache’s .htaccess is convenient but costs a file-system lookup per request and lets every writable directory carry its own rules. If you control the server, place the same directives in the virtual host and set AllowOverride None; the rules above translate one to one.

<VirtualHost *:443>
    ServerName tallpine.example
    DocumentRoot /var/www/tallpine/public

    <Directory /var/www/tallpine/public>
        AllowOverride None
        Require all granted
        Options -Indexes +FollowSymLinks

        RewriteEngine On
        RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

        RewriteCond %{REQUEST_URI} ^/api/
        RewriteCond %{REQUEST_URI} !^/api/index\.php
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule .* api/index.php [L]

        RewriteCond %{REQUEST_URI} !^/index\.php
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule .* index.php [L]
    </Directory>

    Header always set X-Content-Type-Options "nosniff"
    # SSLEngine and certificate directives omitted
</VirtualHost>

nginx equivalent

nginx ignores .htaccess. The User Guide pages on SEF URLs with nginx and on nginx hosting notes give the short form: a try_files fallback to index.php, PHP handed to PHP-FPM, a long expiry for static files and a deny rule for scripts in writable folders. On Joomla 4.0 and later the API application needs its own location, which older nginx notes do not mention and which is the usual reason that the Web Services API and the headless front end fail with 404 on nginx.

server {
    listen 443 ssl;
    server_name tallpine.example;
    root /var/www/tallpine/public;
    index index.php;

    add_header X-Content-Type-Options "nosniff" always;

    # Exploit filter equivalent to the htaccess.txt query-string rules
    if ($query_string ~* "(base64_encode[^(]*\([^)]*\)|GLOBALS(=|\[|%[0-9A-Z]{0,2})|_REQUEST(=|\[|%[0-9A-Z]{0,2}))") {
        return 403;
    }

    # SVG files must not run script when opened directly
    location ~* \.svg$ {
        add_header Content-Security-Policy "script-src 'none'" always;
        add_header X-Content-Type-Options "nosniff" always;
        try_files $uri =404;
    }

    location /api/ {
        try_files $uri $uri/ /api/index.php?$args;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # No PHP execution in writable folders
    location ~* ^/(images|media|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_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht { deny all; }
}

Points to check: with the public folder root points to the public directory and configuration.php is not reachable at all; without it, add location = /configuration.php { deny all; } and deny the libraries, cache and tmp directories too. Test the exploit filter with a request such as ?x=GLOBALS[a]=1, which must return 403. The HTTP_AUTHORIZATION line forwards the Authorization header to PHP-FPM, which the API token authentication needs. The nginx notes page in the User Guide is older than Joomla 4; its sample targets PHP 5 era sockets and a return 403 followed by an error_page line that never runs, so adapt it rather than copying it.

IIS equivalent

Microsoft IIS reads web.config. Joomla ships web.config.txt, which is renamed to web.config and requires the URL Rewrite Module. It contains three rules with the same logic as htaccess.txt: Joomla! Common Exploits Prevention (four query-string conditions, returns 403 and stops processing), Joomla! API Application SEF URLs (rewrites to api/index.php) and Joomla! Public Frontend SEF URLs (rewrites to index.php), together with directoryBrowse enabled="false" and an X-Content-Type-Options: nosniff header; the cross-origin headers are commented out as in the Apache file. It has no counterpart for the SVG header or the pre-compressed assets: if you need them, add a customHeaders entry and enable IIS static compression. IIS is a supported platform (the Developer Manual lists IIS 10), unlike Drupal where IIS support was removed in 11.0.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <location path=".">
    <system.webServer>
      <directoryBrowse enabled="false" />
      <rewrite>
        <rules>
          <!-- The "Common Exploits Prevention" rule (403 on four query-string patterns) comes first in the shipped file -->
          <rule name="Joomla! API Application SEF URLs">
            <match url="^api/(.*)" ignoreCase="false" />
            <conditions logicalGrouping="MatchAll">
              <add input="{URL}" pattern="^/api/index.php" ignoreCase="true" negate="true" />
              <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
              <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
            </conditions>
            <action type="Rewrite" url="api/index.php" />
          </rule>
          <rule name="Joomla! Public Frontend SEF URLs">
            <match url="(.*)" ignoreCase="false" />
            <conditions logicalGrouping="MatchAll">
              <add input="{URL}" pattern="^/index.php" ignoreCase="true" negate="true" />
              <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
              <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
            </conditions>
            <action type="Rewrite" url="index.php" />
          </rule>
        </rules>
      </rewrite>
      <httpProtocol>
        <customHeaders>
          <add name="X-Content-Type-Options" value="nosniff" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
</configuration>

The block above is the shipped file without the exploit-prevention rule and the commented headers. Hide the writable folders with the IIS request filtering feature and the configuration.php file with a hiddenSegments entry if the code tree is under the site root.

Moving a site between hosts

The User Guide documents moving a site between subdirectories and gives general migration advice (back up first, test on a copy, check extension compatibility); the host-to-host steps below follow the same logic and the file format described above. Use a backup tool or a manual copy; Joomla has no built-in backup (see Backups and Recovery).

  1. Prepare the target. Check PHP 8.3+, extensions and database against Requirements and Hosting. Joomla does not support downgrades or moving between MySQL and PostgreSQL, so restore into the same database family and a version that is not older.

  2. Put the site offline (Global Configuration, or config:set offline=true) and take a final backup of files and database.

  3. Copy the files and the database dump. Exclude cache, administrator/cache, tmp and the logs; keep images, media, templates and configuration.php.

  4. Create the database and user on the new host and import the dump. Keep the table prefix.

  5. Edit configuration.php: host, user, password, db, dbtype, dbencryption, tmp_path, log_path, and live_site (remove or correct it). Generate a new secret if the old environment is not trusted anymore, and accept that sessions and API tokens reset. Check session_*, cache_* and redis_* values, cookie_domain, force_ssl and behind_loadbalancer.

  6. Edit the web server rules: copy htaccess.txt to .htaccess (or the nginx or IIS equivalent), comment out or correct RewriteBase for a subdirectory, and make sure the document root is the public folder if you use it.

  7. Fix permissions and ownership for the new PHP user, and create the tmp and log folders.

  8. Clear the cache in the administrator (System, Clear Cache), then reload the site and test the home page, a deep SEF link, the administrator login, a media upload, an extension install, mail sending (see Mail Templates and Email Settings) and the Web Services API.

  9. Switch DNS, put the site online and watch the logs. Keep the old host untouched until the scheduled tasks and the update checks have run once on the new one (see Scheduled Tasks).

# On the old host
php cli/joomla.php config:set offline=true
mysqldump --single-transaction --no-tablespaces tallpine > tallpine.sql
tar --exclude=cache --exclude=tmp --exclude=administrator/cache --exclude=administrator/logs \
    -czf tallpine-files.tar.gz -C /var/www/tallpine .

# On the new host (credentials come from the environment)
mysql -h "$TALLPINE_DB_HOST" -u tallpine -p tallpine < tallpine.sql
tar -xzf tallpine-files.tar.gz -C /var/www/tallpine
php /var/www/tallpine/cli/joomla.php config:set host="$TALLPINE_DB_HOST" tmp_path=/srv/tallpine/tmp log_path=/srv/tallpine/logs
php /var/www/tallpine/cli/joomla.php config:set offline=false
# live_site is not a Global Configuration field: remove or correct it in configuration.php by hand

The same list explains the two common failures after a move. A site that shows the installer was copied without configuration.php (or with an empty one). A site that loads but shows no styling or sends users to the old domain still has live_site set, or its .htaccess carries an old redirect. Moving to a subdirectory or from one removes the subdirectory from log_path, tmp_path and live_site and from the RewriteBase line; the User Guide page lists $ftp_root too, which only exists in files created by old releases. Moving between environments that share a code base (staging to production) is better done with deployments plus a separate database restore than by copying the configuration.

On a cloud deployment, the same file is the reason why more than one web node needs shared state: with more than one machine, session_handler, the system cache and the tmp and images folders have to be shared or externalised, and the database must be a separate service. That topic is in Multi-site Options and Scaling.

Version notes and corrections

  • The JConfig format has been stable since Joomla 4: the template of 6.2 adds no property that a 4.x file lacks, apart from values introduced by individual features (the CORS block, Redis session settings and behind_loadbalancer).

  • JPATH_PLATFORM is no longer defined in Joomla 6 except through the Behaviour - Backward Compatibility 6 plugin; custom files that include configuration.php should guard with defined('_JEXEC') or die; instead.

  • htaccess.txt carries the API rewrite rules needed since the Web Services API arrived in Joomla 4, an SVG content-security rule, pre-compressed asset serving and commented cross-origin isolation headers; compare your .htaccess after major updates.

  • Developing Extensions for Joomla! 5 and Joomla! 4 Masterclass describe a layout that still works, but neither covers the public folder (5.0), TUF-verified updates (5.1), Automated Core Updates (5.4/6.0) or the Joomla 6 defaults. With Automated Core Updates, a read-only configuration.php is not a problem, but a read-only code tree is: the updater must be able to write the files, so deployments that lock the tree must disable the feature and update through their pipeline (see Updating Joomla and Automated Core Updates).

  • The legacy wiki pages on configuration.php and on .htaccess are marked obsolete; the User Guide and the source tree are the current references. See also Where Older Books and Tutorials Are Outdated.