Multi-Site Options and Scaling
|
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. |
Joomla has no multisite feature in core. One installation serves one site, one database and one set of users; the
Joomla Product Strategy lists a "Multi-/Subsite Concept" under "Defend and Expand" in the 7.x and beyond roadmap, with
the caveat that all dates are tentative. Until it exists, running several sites means running several installations,
and handling more traffic means running one installation on several web nodes. This page, for administrators and
hosting engineers, covers both: the ways to operate several sites, how users can be shared between them, and what has
to be shared, identical or unique per node when the Tallpine Hiking Club site (package pkg_tallpine, component
com_tallpine, child template cassiopeia_tallpine) outgrows one server.
It was checked on 2026-10-30 against Joomla 6.2.x (5.x is in its security-only phase) on PHP 8.3 or later, MariaDB 11.8,
Redis 8.10, nginx 1.30 and the PhpRedis extension 6.3.0. The behaviour of the session, cache and configuration code was
read in the 6.2-dev branch of the source tree, because the Developer Manual does not document it yet. The settings
screens are described in Global Configuration, the cache layers in
Caching and Performance, and a single-container setup in
Local Development with Docker; this page only places them in a
multi-site or multi-node design.
What "multisite" means, and what Joomla offers
Three different requirements hide behind the word, and Joomla answers them differently.
| Requirement | Joomla answer today | Consequence |
|---|---|---|
Several independent sites (the club, a members' shop, a hut-booking site) |
One installation per site |
Each has its own code, |
One site in several languages or on several domains |
The multilingual features: language content, associations and language switchers |
One installation and one user base; see Multilingual Sites |
One site on several servers for capacity or availability |
A stateless web tier with shared services |
Code and |
The roadmap is the reason none of this is a menu item. The strategy page names multisite among the features that "cannot
be meaningfully realised in the extension ecosystem", so it is planned for core, but it appears only in the 7.x and
beyond column (2027 and beyond), after the 6.x branch (2025 to 2027). Joomla 6.2 has no sites folder, no shared user
table and no per-domain configuration. Anything that claims otherwise is an extension, a hosting-panel feature or a
hack, and has to be judged on its own maintenance record.
Source: Joomla Product Strategy
Option 1: one installation per site
The supported pattern is the boring one: a complete, separate Joomla per site.
| Aspect | In practice |
|---|---|
Code |
One copy per site, from the same release package or the same container image tag |
Database |
One database per site, or one database with a different |
Users and ACL |
Independent: a Super User on one site is nobody on another |
Updates |
Per site: the core update, extension updates and the Automated Core Updates setting are independent |
Backups |
Per site: see Backups and Recovery |
Failure isolation |
A bad extension update or an attack on one site does not reach the others |
Cost |
More disk space and more updates to apply; mitigated by scripting |
A shared database server, one database per site
The simplest layout gives each site its own database and database user on one server. Each configuration.php has its
own $db, $user and $password, so a leaked credential exposes one site only. The Tallpine club runs three sites
this way.
# One database and one least-privilege user per site (MariaDB).
# Passwords come from the environment, never from this file.
mariadb -u root -p <<SQL
CREATE DATABASE tallpine_club CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE tallpine_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE tallpine_huts CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'tp_club'@'%' IDENTIFIED BY '${TP_CLUB_DB_PASSWORD:?set it first}';
CREATE USER 'tp_shop'@'%' IDENTIFIED BY '${TP_SHOP_DB_PASSWORD:?set it first}';
CREATE USER 'tp_huts'@'%' IDENTIFIED BY '${TP_HUTS_DB_PASSWORD:?set it first}';
GRANT ALL PRIVILEGES ON tallpine_club.* TO 'tp_club'@'%';
GRANT ALL PRIVILEGES ON tallpine_shop.* TO 'tp_shop'@'%';
GRANT ALL PRIVILEGES ON tallpine_huts.* TO 'tp_huts'@'%';
SQL
Source: Joomla User Guide: Backup
One database, several table prefixes
The installer asks for a table prefix, and several installations can share one database when their prefixes differ.
The prefix must start with a letter and contain only letters, digits and underscores, it may be at most 15 characters,
and on PostgreSQL it must be lower case. This suits hosting plans that grant a single database, but it couples the
sites: a dump or restore of the database affects all of them, and one database user can read every site’s tables.
Prefer one database per site whenever the host allows it. A prefix is not a way to share users: the user, ACL and
session tables of each prefix are independent tables, and Joomla offers no supported way to point two installations at
one #__users table.
One installation serving several domains
The User Guide describes the opposite approach in "Multiple Domains and Web Sites in a single Joomla! installation": add the domains as add-on domains in the hosting panel, build separate articles, menus and modules per site, give each site its own template through the template styles, and send each domain to its own start page with a rewrite or a small PHP redirect. The guide itself calls separate installations the best practice and presents this as an exception for special cases, and the page states that it was last updated in 2012. Its limits matter:
-
all domains share one user table, one ACL and one extension set, so a Super User on one is a Super User on all
-
every domain shares one cache, one search index (Smart Search) and one set of URLs, and the redirect methods can loop when combined with domain parking
-
Global Configuration (site name, mail, SEF, offline mode) is one set for the whole installation
-
an update or a bad extension affects every site at once
It is acceptable for two near-identical brand sites run by the same team, and a poor fit for sites with different owners, different security levels or different update windows.
Operating several installations without drowning
Because every site is its own installation, the work is repetition, and repetition is scripted. The console already
covers the repeated tasks (core:update:check, update:extensions:check, config:get, config:set, cache:clean,
database:export; see Command Line Interface). A loop over the site
roots gives a fleet-wide check:
#!/usr/bin/env bash
# fleet-check.sh: report pending core and extension updates for every Tallpine site.
set -euo pipefail
SITES=(/srv/joomla/club /srv/joomla/shop /srv/joomla/huts)
for root in "${SITES[@]}"; do
echo "== ${root}"
php "${root}/cli/joomla.php" core:update:check
php "${root}/cli/joomla.php" update:extensions:check
done
Source: Joomla User Guide: Using the CLI
Keep the three installations on the same release line (see Versions, Release Cycle and Support), and use the same PHP image for all of them so that one set of server settings and one OPcache configuration apply.
Sharing users between sites
Because user tables are per installation, "one login for all Tallpine sites" is an integration problem. Three designs exist, in increasing order of coupling.
Provision accounts through the Web Services API
The Web Services API includes a users endpoint. A script, or the membership system the club already has, can create and update the same member on each site with an API token of a Super User or of a user allowed to manage users. This is a copy, not a share: each site keeps its own password hash, so a password change does not propagate unless the same script handles it. It needs no extension and no change to the login flow.
# TOKEN is an API token created in the user's profile, kept in the environment.
curl --silent --request POST "https://shop.tallpine.example/api/index.php/v1/users" \
--header "X-Joomla-Token: ${TALLPINE_SHOP_API_TOKEN:?set it first}" \
--header "Content-Type: application/json" \
--data '{
"name": "Ada Trailhead",
"username": "ada.trailhead",
"email": "ada@tallpine.example",
"password": "CHANGE_ME_ON_FIRST_LOGIN",
"password2": "CHANGE_ME_ON_FIRST_LOGIN",
"groups": [2]
}'
The group id 2 is the stock Registered group; the Trail Warden group of the club has a different id on every site, so
read the ids from GET /api/index.php/v1/users/groups rather than hard-coding them. The API itself, tokens and the
default plugin states are covered in Web Services API.
Authenticate against one external directory
Joomla’s authentication plugins decide who may log in. The core ships Joomla, Cookie and LDAP authentication plugins, and the LDAP plugin lets several installations check credentials against one directory server (a user record is created in each installation at first login). Single sign-on through OpenID Connect, SAML or an OAuth provider is available only through third-party extensions listed in the Joomla Extensions Directory, which Joomla 6.2 does not include; evaluate them like any extension (maintenance, update-server security flag, supported Joomla version) and see Security Hardening, MFA and Passkeys for how they interact with multi-factor authentication. In every case the groups and access levels are still local: map the directory group to a Joomla group on each site (see Users, Groups, Access Levels and ACL).
What does not work: sharing a session cookie across installations
Setting a $cookie_domain of .tallpine.example does not make a login valid on the sibling sites. The cookie name
is derived from each site’s $secret, and the session lives in each site’s own store, so a cookie from one
installation is simply an unknown session for another. "Shared Sessions" in Global Configuration, a toggle in the
Session fieldset, is unrelated: it shares one session between the front end and the administrator of the same
installation. It is also unavailable when Force HTTPS is set to "Administrator Only", and changing it invalidates all
existing sessions.
Scaling one site across several web nodes
A single well-tuned server with the caches of Caching and Performance carries most Joomla sites. A second web node is justified when one server can no longer serve the uncached work, or when a server failure must not take the site down. The first step is the same in either case: separate the web tier from state.
The table below classifies everything a Joomla node holds, which is the working list for any scale-out.
| State | Where it lives by default | In a multi-node design |
|---|---|---|
Content, users, ACL, extensions list, update sites |
The database |
One shared database (made redundant separately) |
Sessions |
Database for new installs, filesystem as the form default |
Database, Redis or Memcached, shared by all nodes |
System cache |
|
Redis or Memcached, or a per-node cache accepting that clearing it on one node leaves the others untouched |
Page cache plugin entries |
The same cache store |
Same as the system cache |
Media: |
The node’s disk |
Shared volume or object storage via a media adapter extension |
Thumbnails |
|
Shared with the media, or regenerated per node |
|
The node’s disk, one file |
Identical on every node |
Extensions and templates |
The node’s disk |
Identical on every node: deployed, not installed through the browser |
|
The node’s disk |
Per node is fine; ship the logs elsewhere |
Scheduled tasks |
Lazy scheduler in the visitor’s request |
One runner |
Identical configuration: the secret and everything derived from it
Every node must run the same configuration.php. The value that bites first is $secret, because Joomla derives
names from it:
-
the session cookie name is an MD5 hash of the
$secretand a per-application seed. Nodes with different secrets issue differently named cookies, so a visitor sent to a second node looks logged out, and an editor meets a login loop -
the cache keys are prefixed with a hash of the
$secretand contain a hash of the installation’s absolute path, so two nodes installed under different paths never read each other’s cache entries, and two sites that share one Redis with the same secret and path would read each other’s entries -
ApplicationHelper::getHash()mixes the secret into hashes used for names and tokens throughout the code
The installer generates the secret on the first node; copy that file to the others (or, better, build it into the image), and never regenerate it per node. Install every node at the same path, which a container image guarantees. Give every distinct site its own secret and, on a shared Redis, its own database number or instance.
Sessions: database, Redis or Memcached
The Session Handler in the Session fieldset of the System tab chooses where session data lives. In Joomla 6.2 the list
holds Database, Filesystem, Redis, Memcached (labelled "Experimental") and APC User Cache (APCu), and the handler is
created from session_handler in configuration.php.
| Handler | Fit for several nodes | Notes |
|---|---|---|
Database |
Yes, with no extra service |
The default written by the installer. Every request reads and writes a row, so it adds load to the database that a busy site would rather spend on content |
Filesystem |
Only with a shared, locked file system |
The form default; the Session Save Path field defaults to the PHP |
Redis |
Yes |
Needs the PhpRedis extension; the settings are host, port, persistent connection, one password field and a database number |
Memcached |
Yes, but marked experimental in the UI |
Needs the Memcached extension |
APCu |
No |
Per-process shared memory of one node |
The session Lifetime (minutes) setting accepts 1 to 16383 and defaults to 15. Redis uses it as the key time to live, the database handler as the age after which garbage collection deletes the row. Two more switches influence cost: "Track Session Metadata" writes the username, user id and application into the session table (the session table then records who is logged in where), and "Track Guest Session Metadata" does so for guests as well; turn the guest tracking off on a busy public site because every anonymous visitor otherwise creates a metadata row.
Redis sessions are the usual choice once a second node exists, with one rule from the Redis side: the session store must not evict keys under memory pressure, because an evicted session is a visitor who is silently logged out. Evicting a cache entry costs one recomputation, while evicting a session costs a login. Run two Redis instances, or at least two databases with a policy you can defend, instead of one shared store.
// Session part of configuration.php on every Tallpine node (values come from the environment, see below).
public $session_handler = 'redis';
public $session_redis_server_host = 'redis-sessions';
public $session_redis_server_port = 6379;
public $session_redis_persist = true;
public $session_redis_server_auth = ''; // set from the environment in the constructor
public $session_redis_server_db = 0;
public $lifetime = 60; // minutes of inactivity
public $shared_session = false;
public $session_metadata = true;
public $session_metadata_for_guest = false;
The Redis session handler throws a RuntimeException ("Redis is not supported on this system") when the extension is
missing, which presents as a blank page or a 500 error on every request, the administrator included. Always confirm the
extension in System, System Information, PHP Information before switching the handler, and keep a way to edit
configuration.php by hand. Redis itself (persistence, replication, passwords, eviction policies) is covered in
Redis architecture, keyspace and eviction,
Redis replication, Sentinel and Cluster and
Redis security; the Joomla form has only a single password field, so a Redis ACL
user name cannot be supplied from the screen.
Session garbage collection for the database and filesystem handlers is a scheduled task ("Session GC") or the console
commands session:gc and session:metadata:gc (the first takes --application with site as the default); Redis and
Memcached expire keys by themselves.
Cache: Redis, Memcached or per node
System caching has three settings: Off, "ON - Conservative caching" and "ON - Progressive caching". The Cache Handler offered by 6.2 is File, Redis, Memcached (experimental) or APC User Cache, with Cache Time in minutes and the "Platform Specific Caching" switch that gives mobile browsers their own cache entries. The older User Guide page "Cache" still lists APC, Eaccelerator, Memcache and XCache; those handlers no longer exist, and the 6.2 source offers the four above. Older books, such as the 2023 Joomla! 4 Masterclass, also date from before these changes and do not discuss multi-node operation at all.
For several nodes the File handler has a practical drawback: Clear Cache in the administrator only empties the cache of the node that served the click. Redis gives one cache that every node reads and clears.
// Cache part of configuration.php.
public $caching = 1; // 1 conservative, 2 progressive
public $cache_handler = 'redis';
public $cachetime = 15; // minutes
public $cache_platformprefix = false;
public $redis_server_host = 'redis-cache';
public $redis_server_port = 6379;
public $redis_persist = true;
public $redis_server_auth = ''; // set from the environment in the constructor
public $redis_server_db = 0;
Source: Joomla User Guide: Cache
The Redis cache handler writes each entry with an expiry (SETEX), so a policy such as allkeys-lru or
volatile-lru suits the cache instance. Pages for logged-in users are not cached by the Page Cache plugin, so the
cache absorbs anonymous traffic and the database still carries editors, members and searches. An edge cache or CDN
in front (see Caching and Performance) removes most of that anonymous
load before it reaches any node.
Media: a shared directory or object storage
The Media Manager reads files through filesystem plugins. Joomla 6.2 ships one, "Filesystem - Local", whose directory
list defaults to images and files; other adapters are not part of core. Uploaded files are therefore written to the
disk of the node that handled the upload, and a second node returns a 404 for them. Two solutions exist:
-
A shared writable volume for every directory the plugin exposes (NFS, a ReadWriteMany volume on Kubernetes, or a clustered file system). It needs no extension, but it carries the usual shared-file-system costs: latency, locking, and one more thing to keep available. On Kubernetes see Persistent volumes and storage classes and Volumes and ephemeral storage.
-
Object storage through a third-party media adapter plugin from the Extensions Directory. The files live in a bucket and are served by the bucket or a CDN, and the nodes stay stateless.
Whichever is chosen, the same upload limits apply on every layer: the Media component’s Maximum Size (10 MB by default),
PHP upload_max_filesize and post_max_size, and the load balancer’s body limit (client_max_body_size on nginx).
The lowest of them decides, and the symptom is a vague upload error in the Media Manager. See
Media Manager for the component settings.
The proxy, TLS and client addresses
Behind a load balancer that terminates TLS, Joomla must still generate https:// links. The URI code recognises an
HTTPS request from $_SERVER['HTTPS'] or from an X-Forwarded-Proto header whose value is not http, so the proxy
must send that header (or set HTTPS), and Force HTTPS in Global Configuration should be set to the entire site. Only
the protocol is read from the header; Joomla does not use X-Forwarded-For for the client address on its own, so
extensions that log or rate-limit by IP see the proxy address unless the web server rewrites it (for example the Apache
mod_remoteip module or the nginx real_ip module restricted to the proxy’s network). The Enable Outbound Proxy
setting in the Proxy fieldset is unrelated: it routes the site’s own outgoing requests, for example update checks,
through a proxy. The full proxy configuration is in
Web Server, Reverse Proxy and HTTPS.
One scheduler runner
Joomla’s scheduler can be started by the Lazy Scheduler (a script injected into visitor requests, enabled by default with a 300-second interval), by a web-cron URL, or by the CLI. With several nodes, the lazy scheduler makes every node a candidate runner. Disable it in the Scheduled Tasks options, and run the CLI from one place, such as a cron job on one host or a Kubernetes CronJob:
# Every minute from exactly one runner; the tasks themselves decide whether they are due.
* * * * * www-data php /var/www/html/cli/joomla.php scheduler:run --no-interaction
Task locking, timeouts and exit codes are explained in Scheduled Tasks. Do not start a second runner for resilience without reading that page first: two runners racing on the same task is exactly what the single-runner design avoids.
Updates and installations are a deployment concern
On several nodes, a change made through the administrator is made on the disk of one node. Installing an extension from the Extensions: Install screen, updating the core with the Joomla Update component, or an Automated Core Update triggered by the joomla.org service writes files on the node that served the request; the other nodes keep the old code, and the database, which is shared, now describes software they do not have.
Treat the code as an artifact:
-
build one image per release containing the core,
pkg_tallpineand its dependencies, and roll nodes to it -
switch Automated Core Updates off on these sites (
php cli/joomla.php core:autoupdate:unregister, see Updating Joomla and Automated Core Updates), and take the update from the image build instead -
run the database part of an update (the extension schema steps) once, from one node, after the new code is in place, and use a rolling strategy that tolerates the old code reading the new schema for a few minutes (see Deployment strategies)
-
restrict the Install screens to the Super User and prefer the
extension:installconsole command on the build host
A reproducible scale-out: Compose with two nodes
The following Compose project runs the Tallpine site on two web nodes behind an nginx load balancer with MariaDB and two
Redis services, as a local rehearsal of the table above. It is a demonstration, not a production design: both nodes
mount one named volume as the web root, which gives them the same code, configuration.php and media, and stands in
for the shared volume or the identical image of a real deployment. The container tooling is described in
Compose for local development and
Docker networking.
# Dockerfile: the official Joomla 6.2 image plus the PhpRedis extension.
FROM joomla:6.2-php8.4-apache
RUN pecl install redis-6.3.0 \
&& docker-php-ext-enable redis
# compose.yaml
services:
lb:
image: nginx:1.30-alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
web2:
condition: service_started
web1: &web
build: .
environment:
JOOMLA_DB_HOST: db
JOOMLA_DB_NAME: tallpine
JOOMLA_DB_USER: tallpine
JOOMLA_DB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
JOOMLA_SITE_NAME: Tallpine Hiking Club
JOOMLA_ADMIN_USER: Site Admin
JOOMLA_ADMIN_USERNAME: tpadmin
JOOMLA_ADMIN_PASSWORD: ${TALLPINE_ADMIN_PASSWORD:?set it in .env}
JOOMLA_ADMIN_EMAIL: admin@tallpine.example
volumes:
- webroot:/var/www/html
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost/ > /dev/null || exit 1"]
interval: 10s
retries: 30
web2:
<<: *web
environment:
JOOMLA_DB_HOST: db
depends_on:
web1:
condition: service_healthy
db:
image: mariadb:11.8
environment:
MARIADB_DATABASE: tallpine
MARIADB_USER: tallpine
MARIADB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
MARIADB_RANDOM_ROOT_PASSWORD: "1"
volumes:
- dbdata:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
retries: 10
redis-sessions:
image: redis:8.10-alpine
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
volumes:
- sessions:/data
redis-cache:
image: redis:8.10-alpine
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru", "--save", ""]
volumes:
webroot:
dbdata:
sessions:
# nginx.conf: round-robin load balancing; TLS would be terminated here in production.
upstream tallpine {
server web1:80;
server web2:80;
}
server {
listen 80;
client_max_body_size 64m;
location / {
proxy_pass http://tallpine;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Source: Docker Official Image: joomla
The web2 node starts only after web1 is healthy, because the image installs Joomla into an empty web root on first
start and two nodes must not race to do it. With the stack up, switch both services to Redis once, from one node
(the command rewrites configuration.php, which both nodes share here):
docker compose up -d --build
docker compose exec --user www-data web1 php cli/joomla.php config:set \
session_handler=redis session_redis_server_host=redis-sessions \
caching=1 cache_handler=redis redis_server_host=redis-cache
# Verify from the outside: log in at http://localhost:8080/administrator/, then stop one node.
docker compose stop web1
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/ # still answered by web2
If the administrator session survives the loss of the node that created it, sessions are shared. Opening
http://localhost:8080/administrator/ after stopping web1 and finding the login still valid is the test that shows the
Redis handler works; with the Filesystem handler on per-node storage the same test sends the user back to the login form.
Source: Joomla Docker image repository
Secrets in configuration.php
configuration.php is a PHP class named JConfig that the application instantiates at startup, so a constructor can
assign values from the environment instead of storing database passwords, the secret and Redis passwords in a file that
is copied into images and backups.
<?php
// configuration.php (abridged): the properties generated by the installer stay; the constructor overrides secrets.
class JConfig
{
public $offline = false;
public $sitename = 'Tallpine Hiking Club';
public $dbtype = 'mysqli';
public $host = 'db';
public $user = 'tallpine';
public $password = '';
public $db = 'tallpine';
public $dbprefix = 'tp_';
public $secret = '';
public $force_ssl = 2;
public $log_path = '/var/www/html/administrator/logs';
public $tmp_path = '/var/www/html/tmp';
public $caching = 1;
public $cache_handler = 'redis';
public $session_handler = 'redis';
// ... the remaining generated properties ...
public function __construct()
{
$this->password = self::requireEnv('TALLPINE_DB_PASSWORD');
$this->secret = self::requireEnv('TALLPINE_SECRET');
$this->session_redis_server_auth = getenv('TALLPINE_REDIS_SESSIONS_PASSWORD') ?: '';
$this->redis_server_auth = getenv('TALLPINE_REDIS_CACHE_PASSWORD') ?: '';
}
private static function requireEnv(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
// Fail at startup rather than run with an empty database password or a guessable secret.
http_response_code(500);
exit('Missing required environment variable ' . $name);
}
return $value;
}
}
This pattern has one sharp edge. Saving Global Configuration, or running config:set, regenerates the whole file from the
values currently loaded and writes it as a new JConfig class: the constructor disappears, and the secrets that were
read from the environment are written to the file in clear text. In an environment-driven deployment, make the file
read-only and owned by a different user than the PHP process, so that a save fails with "Could not write to the
configuration file" instead of silently rewriting it (Joomla tries to make a file it owns writable before saving and
returns it to 0444 afterwards), and change settings by redeploying. The file-level hardening of
configuration.php and .htaccess is described in
configuration.php and .htaccess.
Staging workflows
Joomla has no staging feature and no configuration management: there is no export of settings, menus or module assignments that can be promoted from one site to another. Everything except code lives in the database, so a staging workflow is "copy the production database and media down, change the code up". The Tallpine club runs three environments: local Compose, a staging site, and production.
| Direction | What travels |
|---|---|
Code and extensions, staging to production |
The image or the release package (core, |
Database, production to staging |
A dump, scrubbed and then imported |
Media, production to staging |
A copy of |
Content and settings, staging to production |
Nothing automatic: re-apply by hand, with the API, or with an extension; or freeze content during the deploy |
Because content cannot travel up, features are developed on staging against a copy of production data and released as code; configuration that must change (a new menu item, a module assignment) is then made once on production by an administrator, ideally scripted through the Web Services API, and recorded in the release notes. A schema change is shipped in the extension’s SQL update script so that the update step moves the production database forward (see Installer Scripts and SQL Updates).
A refresh of staging from production should always end with a scrub, so staging neither emails real members nor indexes itself:
#!/usr/bin/env bash
# refresh-staging.sh: copy production data to staging and neutralise it.
set -euo pipefail
: "${PROD_DB_PASSWORD:?set it first}" "${STAGING_DB_PASSWORD:?set it first}"
# 1. Dump production (a consistent snapshot, without locking the site for long).
MYSQL_PWD="${PROD_DB_PASSWORD}" mariadb-dump --single-transaction --quick \
--host prod-db --user tp_club tallpine_club > /tmp/tallpine.sql
# 2. Load it into the staging database.
MYSQL_PWD="${STAGING_DB_PASSWORD}" mariadb --host staging-db --user tp_club tallpine_staging < /tmp/tallpine.sql
# 3. Copy the uploaded media.
rsync -a --delete prod:/srv/joomla/club/images/ /srv/joomla/staging/images/
rsync -a --delete prod:/srv/joomla/club/files/ /srv/joomla/staging/files/
# 4. Neutralise staging.
cd /srv/joomla/staging
php cli/joomla.php config:set offline=false robots=noindex,nofollow mailonline=false
php cli/joomla.php core:autoupdate:unregister
php cli/joomla.php cache:clean
Source: Joomla User Guide: Backup
Three details are easy to forget after a copy. The copied database still holds real member e-mail addresses and the
Mail Templates, so keep outgoing mail on a catcher such as Mailpit (see
Mail Templates and Email Settings) or the staging
mailonline switch off. The Smart Search index, the scheduled tasks and the update sites also came along, so disable
tasks that call outside services (such as the trail-closure sync of plg_task_tallpine_trailsync). And the Site
Offline and robots settings are database-independent configuration values, so they must be set explicitly on each
environment. The backup tooling and restore drills are in
Backups and Recovery.
Scaling beyond the web tier
When Redis, a CDN and two web nodes are not enough, the database becomes the bottleneck, and the options are those of any LAMP application, not Joomla-specific: tune queries and indexes, give the database more memory, add a managed or replicated database, and (carefully) cut the work per request. The core configuration holds a single database host, so a replica is best used for backups and reporting rather than for page requests. MySQL to PostgreSQL migrations and downgrades are not supported by Joomla, so choose the engine for the long term.
On Kubernetes the same design maps to a Deployment of stateless web pods (see
Autoscaling), a CronJob for scheduler:run, a ReadWriteMany volume or an
object-storage adapter for media, and managed Redis and database services, with the configuration injected from
Secrets as environment variables. The requirements and hosting constraints of each Joomla release are in
Requirements and Hosting.
Common errors and pitfalls
-
Login loop after adding a node. The nodes have different
$secretvalues, or sessions are on per-node storage. Compare the cookie names the two nodes issue and the session handler on each. -
Images uploaded in the Media Manager 404 on some requests. The upload went to one node’s disk. Share the directories listed in the Filesystem - Local plugin, or use an object-storage adapter.
-
Clearing the cache seems to do nothing. The File handler clears one node only. Move the cache to Redis.
-
All sessions end at once. The session Redis instance evicted keys or was restarted without persistence, or Shared Sessions was toggled (which invalidates all sessions by design).
-
Blank page after choosing Redis. The PhpRedis extension is missing on a node, or the host name is unreachable. Fix
configuration.phpby hand. -
Two sites share a Redis and show each other’s content. They have the same secret and the same installation path. Give each its own secret and Redis database number or instance.
-
An extension installed on one node is "missing" on another. Installations through the browser change one node. Deploy the same package to every node.
-
A saved Global Configuration wiped the environment-driven setup. Saving rewrites the whole file; keep it read-only and change settings by redeploying.
-
Scheduled tasks run twice or never. The lazy scheduler is still enabled on every node while a CLI runner also exists, or no runner exists in a read-only container.
-
Using one installation for unrelated sites to save effort. One compromised extension or user exposes all of them. Use separate installations unless the sites share an owner and an update window.
-
Relying on the 2012 multiple-domains note or old multisite tutorials. Joomla 6.2 has no multisite; those methods are workarounds and need the same care as an unsupported hack.
Version notes
-
The multisite concept is a 7.x roadmap theme (2027 and beyond) and is absent from Joomla 6.2.
-
The Session Handler list in 6.2 is Database, Filesystem, Redis, Memcached and APC User Cache, and the web installer writes the Database handler into new sites.
-
Automated Core Updates (5.4 and 6.0) are on by default for new installations and off for upgraded sites, which is why a fresh node image usually needs them switched off; core updates have been verified with TUF since 5.1.
-
Joomla 7.0 removes the auto-detection of
$appand$dbproperties of plugins, which matters to any plugin you add to the image for caching or session logic; see Versions, Release Cycle and Support. -
The Developer Manual has no page on scaling or sessions yet; the behaviour above comes from the 6.2 source tree and may change in a minor release, so re-read the cited files before depending on a detail.
Where older books and tutorials are outdated
Joomla! 4 Masterclass (2023) and Developing Extensions for Joomla! 5 (2023) concern a single site on a single server and do not describe scale-out. Both pre-date Joomla 5.1 and 6: they do not mention Automated Core Updates, whose effect on a multi-node deployment is described above, the Backward Compatibility 6 plugin, or the current platform. The User Guide’s cache-handler list (APC, Eaccelerator, Memcache, XCache) is out of date for 6.2, and the legacy wiki and older tutorials describe multisite through shared tables or extensions of Joomla 1.5 to 3.x that are neither supported nor tested on 6.x. For multisite and scaling, the Product Strategy, the User Guide’s system pages and the source tree are the reliable references.