Joomla Command-Line Interface

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.

The Joomla command-line interface (CLI) is a single PHP script, cli/joomla.php, that boots the same framework, plugins and database connection as the web site but without a web server, a session or a login. It is built on the Symfony Console component through the Joomla Framework console package. It lets an administrator update the core, install and remove extensions, rebuild the Smart Search index, run scheduled tasks, switch the site offline for maintenance, export and import tables, and manage users from a terminal, from cron or from a CI job. This page was written against Joomla 6.2.x on PHP 8.3 or newer and checked against the official documentation and the 6.2 source tree on 2026-10-30. Every command in this page was enumerated from the libraries/src/Console directory of the 6.2 source tree and the Joomla Framework database package that supplies two more.

The running example is the Tallpine Hiking Club site: a Joomla 6.2 site served from a container, with a MariaDB database, a staging copy and a production copy, a scheduled task plg_task_tallpine_trailsync that syncs trail closures, and a custom tallpine:import command (see Console command plugins).

What the CLI is, and who uses it

  • Administrators use it for operations that must run unattended: nightly cache clean-up, session garbage collection, Smart Search indexing, core and extension update checks, database exports before an upgrade.

  • DevOps and CI engineers use it to build a site from a container image, install the extensions of a release, switch the site offline during a deploy and run the scheduled tasks from a real scheduler instead of web traffic.

  • Site builders on shared hosting use it from the cron section of the hosting panel when there is no terminal.

  • Extension developers add their own commands as plugins of the console group and test them from the shell.

The CLI never asks for a Joomla login. Whoever can run php cli/joomla.php as a user that can read configuration.php and reach the database can do everything the Super User can do, including creating Super Users and changing the global configuration. Treat shell access to the web root as equivalent to a Super User account (see Security of the CLI).

Running it

From the Joomla root:

cd /var/www/html
php cli/joomla.php list
php cli/joomla.php help cache:clean
php cli/joomla.php cache:clean --help

The guide also shows running the script from inside the cli directory (cd cli; php joomla.php). Both forms work because the script derives the Joomla root from its own location. Three things are worth knowing before the first run.

  • list and help. list prints every registered command, grouped by the part before the colon (cache, config, core, database, extension, finder, maintenance, scheduler, session, site, update, user). It also shows commands contributed by console plugins, so it is the quickest way to check that a plugin is enabled. help <command> and <command> --help print the arguments, options and usage of one command. The help text is hard-coded in the command classes and in third-party plugins, so it is the authority for the exact spelling on the installed version.

  • The PHP binary is not necessarily the web server’s. The CLI runs with the command-line PHP, which has its own php.ini, its own extensions and possibly another version. The script starts with a version guard: it prints that the command-line PHP must be 8.3.0 or newer and exits otherwise. Check it with php -v and php -m, and use the full path to the binary in cron (for example /usr/local/bin/php), because cron runs with a minimal PATH.

  • Who runs it matters. Commands that write files (cache, logs, tmp, configuration.php, installed extensions) create them owned by the invoking user. Running as root and then browsing as www-data produces unwritable cache and log files. Run the CLI as the web server’s user (sudo -u www-data php cli/joomla.php ..., or docker compose exec -u www-data).

The bootstrap sequence

flowchart TD A[php cli/joomla.php command] --> B{PHP is 8.3.0 or newer?} B -- no --> X1[Prints an error and exits] B -- yes --> C{vendor autoloader and media/vendor present?} C -- no --> X2[Prints the git-checkout hint and exits] C -- yes --> D{configuration.php present and not empty?} D -- no --> X3[Prints install hint: php installation/joomla.php install] D -- yes --> E[includes/framework.php: container, services, database] E --> F[ConsoleApplication.execute] F --> G[Load extension namespaces] G --> H[Populate HTTP host from --live-site or config] H --> I[Import plugin groups: behaviour, system, console] I --> J[BEFORE_EXECUTE event: plugins add commands] J --> K[Symfony Console resolves the command] K --> L[Command runs and returns an exit code]

The script cli/joomla.php defines _JEXEC, applies the PHP version guard, loads defines.php (an optional defines.php in the root can relocate paths) and includes/defines.php, checks that the Composer autoloader and media/vendor exist (this check is what a git checkout without composer install and npm ci runs into), checks that configuration.php exists and is larger than a few bytes, and then loads includes/framework.php and starts the console application. Because configuration.php is required, a fresh tree has no list output: the script prints the install hint instead. A fresh site can be installed from the shell with php installation/joomla.php install, which has its own options (see its --help).

The console application is Joomla\CMS\Application\ConsoleApplication. Its execute() method registers the extension namespaces, populates the HTTP host (so plugins that need a site URL do not fail), and imports the behaviour, system and console plugin groups on the console dispatcher before delegating to the Framework application. Core commands are registered lazily by a command loader from the Console service provider; commands from console plugins are added in a BEFORE_EXECUTE listener. Section The console plugin group returns to that.

Global options and exit codes

The built-in Symfony options work on every command.

Option Effect

-h, --help

Print the help of the command.

-q, --quiet

Suppress output. Use it in cron to avoid mail from cron on success. Errors are still reflected in the exit code.

-v, -vv, -vvv

Increase verbosity. remove-old-files prints each checked file at -vv and -vvv.

-n, --no-interaction

Never ask. Prompting commands take the default or, for extension:remove, proceed.

--ansi, --no-ansi

Force or disable coloured output. Disable it in CI logs.

-V, --version

Print the application version.

--live-site=URL

Joomla-specific. The public URL of the site, for example https://www.tallpine.example. Supplies the host and path that a web request would provide.

The --live-site option exists because code run from the shell has no HTTP_HOST or REQUEST_URI. Anything that builds absolute URLs (mail templates, routing from a task, the automated-updates registration) needs it, or needs $live_site in configuration.php. Without it, a command that sends e-mail with links can produce links with the wrong host.

Options accept a space or an equals sign. The exception is the positional option=value arguments of config:set, which always use =:

php cli/joomla.php session:gc --application administrator
php cli/joomla.php session:gc --application=administrator
php cli/joomla.php config:set debug=true

Commands return 0 on success and a non-zero code on failure, which is what shell scripts, cron wrappers and CI steps test. Several commands define their own non-zero codes (for example core:update returns 1 when its checks fail, including when no update is available, and 2 when the update itself fails; config:get returns 1, 2 or 3 for a missing group, a missing option or a failed read; extension:remove has distinct codes for not found, locked, invalid type and aborted). A script should test $? against zero rather than against one specific value, and print the output on failure.

The core commands, one by one

Joomla 6.2 registers about 35 core commands. The table groups them; the sections that follow give arguments, options, behaviour and pitfalls. The "since" column gives the release in which the command appeared where it is newer than 4.0.

Command Purpose Since

cache:clean

Clear the system cache, or only expired entries

4.0

config:get

Show one option or a group of options of configuration.php

4.0

config:set

Set one or more options in configuration.php

4.0

core:update:check (alias core:check-updates)

Refresh and report available Joomla core updates

4.0

core:update

Update the Joomla core

4.0

core:update:channel

Show or set the core update channel

5.1

core:autoupdate:register, core:autoupdate:unregister

Register or unregister the site for the Automated Core Updates service

5.4

database:export, database:import

Export or import tables as XML (optionally zipped)

4.0

maintenance:database

Check the database schema of core and extensions, optionally fix it

5.1

extension:list

List installed extensions

4.0

extension:install, extension:remove

Install from a path or URL, or remove by ID

4.0

extension:enable, extension:disable

Enable or disable an extension by ID

6.1

extension:discover, extension:discover:list, extension:discover:install

Find, list and install extensions copied to the file system

4.0

finder:index

Build, or purge and rebuild, the Smart Search index

4.0

scheduler:list, scheduler:state, scheduler:run

List, enable/disable/trash and run scheduled tasks

4.1

session:gc, session:metadata:gc

Garbage-collect sessions and session metadata

4.0

site:down, site:up

Put the site offline or online

4.0

site:create-public-folder

Create a public folder for the web server’s document root

5.0

update:extensions:check

Refresh and report extension updates

4.0

update:joomla:remove-old-files

Remove files that earlier releases left behind

4.0

user:list, user:add, user:addtogroup, user:removefromgroup, user:reset-password, user:delete

Manage users

4.0

The two database commands live in the Joomla Framework database package rather than in the CMS tree, but they are registered by the same service provider and behave like core commands.

Older books describe the CLI of their own release. Joomla! 4 Masterclass (2023) targets Joomla 4.x and Developing Extensions for Joomla! 5 (2023) targets Joomla 5.0, so neither lists core:update:channel and maintenance:database (5.1), core:autoupdate:* (5.4) or extension:enable/extension:disable (6.1). Run list on the installed version rather than relying on a printed command table. The same applies to any tutorial that uses the Joomla 3 style cli/*.php scripts extending JApplicationCli: those scripts no longer have a base class on Joomla 6 (see Replacing legacy CLI scripts).

cache:clean

php cli/joomla.php cache:clean            # clear all system cache entries
php cli/joomla.php cache:clean expired    # clear only expired entries

The optional argument is the literal word expired. Without it, the command clears every entry of the system cache through the com_cache model; with it, only entries past their lifetime. It reports Cache cleaned or Expired Cache cleaned and returns failure when the model cannot clear the cache. It clears the cache that Joomla’s cache handler manages (file, Redis, memcached, APCu depending on cache_handler); it does not clear the browser, the web server or a CDN. For a Redis cache handler, the cache and its connection settings are covered in Caching and performance and the server itself in Redis. Run cache:clean after deploying a template, an override or a language file when the cache is enabled, and cache:clean expired from cron when you use the file handler, because expired files are otherwise only removed by the web request that notices them.

config:get and config:set

config:get shows options from configuration.php (the JConfig class), and config:set writes them back through the com_config application model, so it validates keys and writes the file the same way the Global Configuration screen does.

php cli/joomla.php config:get sitename
php cli/joomla.php config:get offline
php cli/joomla.php config:get --group session
php cli/joomla.php config:get --group mail
php cli/joomla.php config:set debug=true
php cli/joomla.php config:set sitename="Tallpine Hiking Club" offline_message="Back in ten minutes"
php cli/joomla.php config:set session_handler=redis caching=2 cache_handler=redis
  • config:get <option> prints a one-row table. An unknown name prints Can't find option and returns 2. Booleans are shown as the words true and false.

  • config:get --group <name> (short -g) prints a table for one of three groups: db (dbtype, host, user, password, dbprefix, db, the dbssl* options), session (session_handler, shared_session, session_metadata) and mail (mailonline, mailer, mailfrom, fromname, sendmail, smtpauth, smtpuser, smtppass, smtphost, smtpsecure, smtpport). An unknown group returns 1.

  • config:set takes one or more option=value arguments. Every pair must contain =, and every key must already exist in JConfig, otherwise the command stops with Can't find option and nothing is saved. Values containing spaces need shell quoting. Database settings are validated (type, prefix pattern, name length) before they are saved.

Two security consequences follow. First, config:get --group db and --group mail print the database password and the SMTP password in clear text, which ends up in a terminal scrollback, a CI log or a cron mail. Never run them in a pipeline whose log is shared. Second, values given to config:set are visible in the process list and shell history, so do not pass password=..., smtppass=... or secret=... on the command line of a shared host. Provide secrets by editing configuration.php from a deployment template or from environment variables, as shown in configuration.php and .htaccess.

config:set needs a writable configuration.php. On a locked-down production server (file made read-only on purpose) the command fails to save; change configuration in the deployment instead. After config:set the new values apply immediately to the next request, but a cached page or an opcache that was told not to revalidate may keep serving old values until cache:clean or an opcache reset.

site:down and site:up

php cli/joomla.php site:down
# ... deploy ...
php cli/joomla.php site:up

site:down is a shortcut for config:set offline=true and site:up for offline=false. They run the configuration command internally, print Website is now offline or online, and return 0 on success. The front end then shows the offline page and message configured in Global Configuration; administrators who are logged in can still reach the site and the back end (see Error, offline and component pages). A deploy script should call site:up in a trap so that a failing step does not leave the site offline.

Core updates: core:update:check, core:update, core:update:channel

php cli/joomla.php core:update:check
php cli/joomla.php core:check-updates        # same command, older name
php cli/joomla.php core:update:channel       # show the channel
php cli/joomla.php core:update:channel default
php cli/joomla.php core:update:channel next
php cli/joomla.php core:update:channel custom --url=https://updates.tallpine.example/list.xml
php cli/joomla.php core:update
  • core:update:check purges the cached update information, refreshes it from the update server and prints whether a newer Joomla version exists. Its original name core:check-updates is kept as an alias and is the spelling used in the User Guide; the source marks core:update:check as the preferred name.

  • core:update performs the update of the core. It runs these steps with a progress bar: it starts logging (and stops if the update log cannot be written), reads the update information, checks the database schema and refuses to continue when the schema has errors, runs the extra pre-conditions when the target is a new major version, and then downloads and installs the package. It prints Joomla core updated successfully! and returns 0; it returns 1 when a check fails (including the case You already have the latest Joomla! version) and 2 when the update fails.

  • core:update:channel (Joomla 5.1 and later) shows the current channel, or sets it to default, next or custom. custom requires --url. The default channel is the stable channel; next is the channel used to move to the next major release (for example from 5.4 to 6.0 or from 6.2 to 7.0 when they exist).

The CLI update is the same update that the Joomla Update component performs, minus the web steps. It does not take a backup. Core has no backup feature (see Backups and recovery), so export the database and snapshot the files first (see database:export, database:import and maintenance:database). Automated Core Updates, available from Joomla 5.4, are a separate mechanism that updates minor, patch and security releases without anyone running a command; they are described in Updating Joomla and Automated Core Updates and in the next section.

When a major upgrade is involved (5.4 to 6.x), the pre-condition check inside core:update is not a substitute for the procedure in Upgrading Joomla 5 to 6: update to the latest 5.4.x, disable the old Behaviour backward-compatibility plugin, enable the Behaviour - Backward Compatibility 6 plugin (the two cannot both be on), then switch the channel and update. The pre-update check is not a reliable source of truth on its own.

core:autoupdate:register and core:autoupdate:unregister

Joomla 5.4 introduced Automated Core Updates for minor, patch and security releases: a joomla.org service triggers the site through Joomla Update web-service endpoints, and the site performs the update itself. It is on by default for new installs (the installer offers an opt-out) and off for sites that were upgraded. The service needs a publicly reachable site, the Default update channel, "Stable" as the minimum stability, and Joomla 5.4 or newer; core packages have been verified with TUF (The Update Framework) since 5.1.

php cli/joomla.php core:autoupdate:register --live-site=https://www.tallpine.example
php cli/joomla.php core:autoupdate:unregister

The commands register or unregister the current site for the service. register requires --live-site with a scheme and a host and exits with an error otherwise (Missing --live-site option, Incomplete --live-site provided). Use them to switch the service on or off for a fleet of sites from a provisioning script, or to unregister a staging copy that was cloned from production and would otherwise register a second time under the same address.

database:export, database:import and maintenance:database

php cli/joomla.php database:export --folder=/backups/tallpine/2026-10-30
php cli/joomla.php database:export --folder=/backups/tallpine --zip
php cli/joomla.php database:export --table=jos_content --folder=/tmp
php cli/joomla.php database:import --folder=/backups/tallpine/2026-10-30
php cli/joomla.php database:import --zip=/backups/tallpine/tallpine-db.zip
php cli/joomla.php database:import --table=jos_content --folder=/tmp
php cli/joomla.php maintenance:database
php cli/joomla.php maintenance:database --fix

database:export writes one XML file per table (structure plus data) into a folder (default: the current directory), or a single table with --table, and packs everything into a ZIP archive with --zip. It needs the joomla/filesystem Composer package and, for --zip, joomla/archive; both ship with Joomla 6.2. database:import reads the XML files of a folder, or a ZIP with --zip, or a single table with --table. For each table it drops the existing table, recreates the structure and inserts the data, so importing is destructive for the tables it touches. The User Guide warns against using these commands to import all tables of a multilingual site, because that can break Smart Search (the Finder terms and taxonomy tables are rebuilt by indexing). Import the content tables you need and re-run finder:index purge afterwards. The table names include the prefix of the site (jos_ here is a placeholder; read the real prefix with config:get dbprefix).

These commands are a convenience for moving small sites and single tables, not a replacement for mysqldump, mariadb-dump or pg_dump, which are faster, handle routines, triggers and large data and are what a real backup strategy uses; the portable dump tools are covered in Backups and recovery. The XML format is Joomla-specific and the commands cannot translate between MySQL/MariaDB and PostgreSQL: Joomla does not support migrating between database engines.

maintenance:database (Joomla 5.1 and later) runs the same check as System > Maintenance > Database: for core and each extension it compares the schema version and the update scripts with the database and prints a table of problems. Without --fix it only reports; with --fix it applies the missing changes. Run it after every core or extension update and in the pipeline, because a missing column from an update script is the usual reason for a white page after a deploy (see Installer scripts and SQL updates).

Extensions

php cli/joomla.php extension:list
php cli/joomla.php extension:list --type=plugin
php cli/joomla.php extension:install --path=/tmp/pkg_tallpine_1.4.0.zip
php cli/joomla.php extension:install --url=https://downloads.tallpine.example/pkg_tallpine_1.4.0.zip
php cli/joomla.php extension:disable 10234
php cli/joomla.php extension:enable 10234
php cli/joomla.php extension:remove 10234
php cli/joomla.php extension:remove 10234 --no-interaction
php cli/joomla.php extension:discover
php cli/joomla.php extension:discover:list
php cli/joomla.php extension:discover:install --eid=10234
php cli/joomla.php extension:discover:install
  • extension:list prints a table of installed extensions with their IDs. --type filters by type (component, module, plugin, template, library, package, file, language). The numeric extension ID in the first column is the identifier every other extension command uses, and it differs between sites: the ID of pkg_tallpine on staging is not its ID on production, so look it up by name in the script (for example by parsing extension:list --type=package) instead of hard-coding it.

  • extension:install --path= installs from a ZIP or a folder on the server, --url= downloads a package first. Both go through the same installer as System > Install > Extensions, so manifests, install scripts and SQL run exactly as in the web installer (see Installing and updating extensions). Install from a URL only when you control the address and use HTTPS, as with any package from outside your own build.

  • extension:enable and extension:disable (Joomla 6.1 and later) take the extension ID. Before 6.1 the only way to switch an extension from the shell was a SQL update on #__extensions.

  • extension:remove asks Are you sure you want to remove this extension? and expects yes; with --no-interaction it proceeds without asking. It refuses to remove locked core extensions and returns a different non-zero code for an unknown ID, a locked extension, an invalid type, a failed uninstall and a declined prompt. A script must never pass --no-interaction together with an ID it has not just looked up.

  • The discover family handles extensions whose files were copied onto the server instead of installed (a Git deployment, a Composer-managed plugins directory). extension:discover scans the file system and records new extensions as discovered; extension:discover:list shows them; extension:discover:install installs one (with --eid=, as the command’s option name says) or, without it, all of them. This is how a CI deploy that rsyncs plugins/console/tallpine activates the plugin without opening the back end.

A deployment that ships pkg_tallpine as a ZIP looks like this:

set -euo pipefail
php cli/joomla.php site:down
trap 'php cli/joomla.php site:up' EXIT

php cli/joomla.php database:export --zip --folder="/backups/tallpine/$(date +%F)"
php cli/joomla.php extension:install --path=/deploy/pkg_tallpine_1.4.0.zip
php cli/joomla.php maintenance:database --fix
php cli/joomla.php cache:clean

update:extensions:check and update:joomla:remove-old-files

php cli/joomla.php update:extensions:check
php cli/joomla.php update:joomla:remove-old-files --dry-run
php cli/joomla.php update:joomla:remove-old-files

update:extensions:check refreshes the update sites of every installed extension and prints a table with the columns Extension ID, Name, Location, Type, Installed, Available and Folder, or There are no updates available.. It only reports; there is no command that applies extension updates in 6.2, so installing the update is done in System > Update > Extensions or with extension:install of the downloaded package. From Joomla 6.2 extension updates carry a security-severity flag in the update information, which the back end shows; a monitoring script can combine this command with the web view to alert on security updates. Extension update sites and the JED are covered in Update servers and the JED.

update:joomla:remove-old-files removes the files and folders that the update script lists as obsolete. The core update script keeps a list of files and folders that earlier releases shipped and later ones dropped; this command removes the entries of that list from the shell. Run it with --dry-run first: it reports how many files and folders it checked and how many it would delete, and -vv prints each match. Do not run it on a tree that holds your own files in a core directory.

Smart Search: finder:index

php cli/joomla.php finder:index
php cli/joomla.php finder:index purge
php cli/joomla.php finder:index --minproctime=2 --pause=3 --divisor=5

finder:index builds the Smart Search index for all enabled Smart Search content plugins. The argument purge empties the index (keeping search filters) and then rebuilds it. Indexing a large site in one pass can saturate the database, so the command throttles itself: --minproctime is the minimum processing time in seconds of a batch before a pause is applied (default 1), --pause is either the word division (default) or a fixed number of seconds, and --divisor (default 5) divides the batch time to compute the pause. Use it from cron nightly, because the Smart Search - Content plugin only indexes an item when it is saved in the back end, and items imported by tallpine:import or changed in SQL are missing from the index until it runs. See Smart Search.

Scheduled tasks: scheduler:list, scheduler:state and scheduler:run

php cli/joomla.php scheduler:list
php cli/joomla.php scheduler:run                # run the next due task
php cli/joomla.php scheduler:run --all          # run every due task
php cli/joomla.php scheduler:run --id=7         # run task 7 now
php cli/joomla.php scheduler:state --id=7 --state=disable
php cli/joomla.php scheduler:state -i 7 -s 1
php cli/joomla.php scheduler:state              # interactive

Scheduled tasks (Joomla 4.1 and later) are rows in the Scheduled Tasks component whose routine is supplied by a Task plugin; the Tallpine trail-closure sync is one. The CLI is the way to run them from a real scheduler.

  • scheduler:list prints all tasks with ID, title, type, state and next execution.

  • scheduler:run without options runs the single next due task (the queue limit is one). With --all it runs every due task in sequence. With --id (short -i) it runs that task regardless of whether it is due, and also if it is disabled or already running, which makes --id the right tool for a manual run and a bad one for a schedule. The output per task is Task#07 'Sync trail closures' processed in 0.84 seconds., a notice when a task ran out of time and will resume, a warning when it could not start (Is it already running?), or an error for an orphaned task whose plugin is gone. Exit codes report the same: 0 for success and non-zero otherwise, with a dedicated code for No tasks due!. A cron wrapper that treats every non-zero code as an alert will therefore report No tasks due! as a failure unless the script tolerates that code; run scheduler:run --all -q and check the log instead, or run it every minute and ignore the "nothing due" case.

  • scheduler:state changes the state of one task: enable (or 1), disable (or 0) or trash (or -2). Without options it asks interactively. Disable a task before deploying a new version of its plugin to avoid a run in the middle of an upgrade.

By default Joomla runs tasks lazily: the System - Schedule Runner plugin triggers due tasks from web requests, or the web cron endpoint is called by an external service. For production, set the mode to Web Cron or switch the lazy scheduler off and use the CLI from cron, because lazy scheduling ties task latency to site traffic and runs task code inside a visitor’s request. The scheduler concepts and the task plugin API are in Scheduled tasks and Scheduled task plugins.

Sessions: session:gc and session:metadata:gc

php cli/joomla.php session:gc
php cli/joomla.php session:gc --application=administrator
php cli/joomla.php session:metadata:gc

session:gc runs garbage collection of the session store for one application: site (the default) or administrator; any other value is rejected as not a valid option. The --application option has the single-dash shortcut -app. The command is only useful for session handlers that do not expire sessions by themselves: it reports Garbage collection was not completed when the handler does not support it (a Redis handler expires keys by TTL, for example). session:metadata:gc cleans the session metadata table that tracks who is online when the session_metadata option is on. Typical schedule: once an hour for the database handler.

site:create-public-folder

php cli/joomla.php site:create-public-folder --public-folder=/var/www/tallpine-public

Joomla 5.0 introduced the optional public folder: a document root that contains only the files a browser needs, with configuration.php, libraries and administrator code outside it. The command creates such a folder through the public-folder generator (it needs PHP’s symlink()) and prints Adjust your server configuration to serve from the public folder. It asks for the path when the option is omitted. It does not change the web server; point the document root at the new folder yourself, as described in Installation and the public folder.

Users: user:*

php cli/joomla.php user:list
php cli/joomla.php user:add --username=warden1 --name="Trail Warden One" --email=warden1@tallpine.example --password="$WARDEN_PASSWORD" --usergroup="Trail Warden"
php cli/joomla.php user:addtogroup --username=warden1 --group="Editor"
php cli/joomla.php user:removefromgroup --username=warden1 --group="Editor"
php cli/joomla.php user:reset-password --username=warden1 --password="$WARDEN_PASSWORD"
php cli/joomla.php user:delete --username=warden1
  • user:list prints a table of all users.

  • user:add accepts --username, --name, --password, --email and --usergroup (several groups separated by commas). Any value left out is asked interactively, and the password prompt is hidden. The new user is created through the user model, so password policy and duplicate checks apply.

  • user:addtogroup and user:removefromgroup take --username and --group (a group title, for example Trail Warden or Super Users); if the option is omitted they ask, offering a list of groups.

  • user:reset-password takes --username and --password.

  • user:delete takes --username and asks for confirmation.

This is also the break-glass path when the only Super User has lost the password and the e-mail reset is broken: run user:reset-password for that account, or user:add with --usergroup="Super Users". It does not remove multi-factor authentication (MFA, in core since Joomla 4.2) of the account; a Super User locked out by an MFA method has to follow the procedure in Security hardening, MFA and passkeys. Passing the password with --password places it in the command line, visible to other users through the process list; prefer the hidden prompt on interactive sessions, and on automation read it from an environment variable or secret store as in the example ($WARDEN_PASSWORD). Groups, access levels and the Trail Warden group are modelled in Users, groups, access levels and ACL.

Running the CLI from cron

On a server with a shell:

# /etc/cron.d/tallpine  (runs as the web user; PHP path is absolute)
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
CLI="/usr/local/bin/php /var/www/tallpine/cli/joomla.php"

# Run due scheduled tasks every minute
*    *  * * * www-data  $CLI -q scheduler:run --all
# Clear expired cache entries every 15 minutes
*/15 *  * * * www-data  $CLI -q cache:clean expired
# Garbage-collect sessions hourly
7    *  * * * www-data  $CLI -q session:gc
17   *  * * * www-data  $CLI -q session:gc --application=administrator
# Rebuild the Smart Search index at night
30   2  * * * www-data  $CLI -q finder:index
# Look for core and extension updates, mail the output to the operator
0    6  * * 1 www-data  $CLI core:update:check ; $CLI update:extensions:check

On shared hosting the panel’s cron screen takes one command per job. Use the hosting provider’s PHP binary and the absolute path, including a sub-folder when the site is not in the web root, as the User Guide shows: /usr/local/bin/php /home/username/public_html/cli/joomla.php -q cache:clean. A hosting panel that offers several PHP versions usually names the CLI binary per version (php8.3); use the same major version as the site.

Pitfalls that cause silent failures:

  • Overlap. A job that takes longer than its interval starts a second copy. Wrap long jobs with flock -n /run/lock/tallpine-finder.lock …​, or rely on the scheduler’s own concurrency guard for tasks.

  • Wrong user. A job in root’s crontab creates root-owned cache and log files. Use /etc/cron.d with www-data or the web user’s crontab.

  • -q hides the reason. With -q a failing command prints nothing but exits non-zero. Write a wrapper that logs the command’s output to a file when the exit code is non-zero.

  • Memory and time. The command-line PHP has no max_execution_time but has memory_limit. finder:index on a large site needs more memory than a web request; pass php -d memory_limit=1G.

  • The wrong URL in e-mails. Add --live-site=https://www.tallpine.example to jobs that send mail (tasks that mail reports) or set $live_site in configuration.php.

Docker and Compose

In a containerised site the CLI runs inside the application container, so the PHP version, extensions and the database host are exactly those of the web process. A Compose file for the Tallpine site with the official image, MariaDB and Mailpit (the full local environment is in Local development with Docker):

services:
  joomla:
    image: joomla:6.2-php8.3-apache
    ports:
      - "8080:80"
    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
    depends_on:
      - db
  db:
    image: mariadb:11.4
    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
  mailpit:
    image: axllent/mailpit:latest
    ports:
      - "8025:8025"
volumes:
  joomla_data:
  db_data:

Run commands with docker compose exec, as the web user, in the working directory /var/www/html of the image:

docker compose exec -u www-data joomla php cli/joomla.php list
docker compose exec -u www-data joomla php cli/joomla.php extension:list --type=plugin
docker compose exec -u www-data joomla php cli/joomla.php user:add \
  --username=admin2 --name="Second Admin" --email=admin2@tallpine.example \
  --usergroup="Super Users"
# one-off job container sharing the same volumes and network
docker compose run --rm -u www-data joomla php cli/joomla.php scheduler:run --all

Do not run the scheduler by exec from the host’s cron on a multi-container deployment without thinking about where the job runs: with several replicas behind a load balancer (see Docker Compose in practice and Kubernetes), run exactly one scheduler process, for example a Kubernetes CronJob that starts the same image with the command php cli/joomla.php scheduler:run --all, sharing the database and the writable volumes of the site. Sessions, cache and media must then live in shared storage (database or Redis sessions, a shared images volume), as discussed in Multi-site options and scaling. The official image is built from the release package; installing extensions into the running container writes into the joomla_data volume, not into the image, so rebuild or re-install after a container is recreated without that volume.

Running the CLI in CI

A pipeline job that deploys a release to staging and checks the result:

name: deploy-staging
on:
  push:
    tags: ["v*"]
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Upload the package
        run: scp build/pkg_tallpine.zip "$DEPLOY_USER@$DEPLOY_HOST:/deploy/"
        env:
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
      - name: Install and verify through the CLI
        run: |
          ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<'REMOTE'
          set -euo pipefail
          cd /var/www/tallpine
          CLI="php cli/joomla.php --no-ansi"
          $CLI site:down
          trap '$CLI site:up' EXIT
          $CLI database:export --zip --folder=/backups/tallpine/pre-deploy
          $CLI extension:install --path=/deploy/pkg_tallpine.zip
          $CLI maintenance:database --fix
          $CLI cache:clean
          $CLI extension:list --type=package | grep -q pkg_tallpine
          REMOTE
        env:
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}

Tips for pipelines: use --no-ansi so the log has no escape codes, use --no-interaction for any command that may prompt (and never answer a destructive prompt blindly), keep secrets in the CI secret store and pass them through environment variables, and fail the job on a non-zero exit code. For test jobs, build a throw-away site in a service container with php installation/joomla.php install and run extension:install plus the extension’s tests against it; the extension build and test setup is covered in Build and CI for extensions. A smoke test that runs list and checks that tallpine:import appears proves that the console plugin is installed and enabled.

A maintenance flow in one picture

sequenceDiagram participant Ops as Operator or CI job participant CLI as php cli/joomla.php participant DB as MariaDB participant Site as Tallpine site Ops->>CLI: site:down CLI->>Site: offline=true in configuration.php Ops->>CLI: database:export --zip CLI->>DB: read all tables Ops->>CLI: core:update or extension:install CLI->>Site: replace files, run SQL update scripts Ops->>CLI: maintenance:database --fix CLI->>DB: apply missing schema changes Ops->>CLI: cache:clean and finder:index Ops->>CLI: site:up CLI->>Site: offline=false

The console plugin group

Core commands are registered by a service provider, and everything else is contributed by plugins of the console group. On every CLI start the console application imports the behaviour, system and console groups on its dispatcher, so an enabled console plugin can subscribe to events. A console plugin registers its commands in a listener for the Joomla Framework event ApplicationEvents::BEFORE_EXECUTE, which runs before the Symfony application resolves the command, by calling addCommand() on the application. The application also dispatches Symfony Console events such as BEFORE_COMMAND_EXECUTE and COMMAND_ERROR, which a plugin can use for logging or for blocking commands.

A minimal plugin for the Tallpine site; the full walk-through, the command class with arguments and options, and testing are in Console command plugins.

<?php

namespace Tallpine\Plugin\Console\Tallpine\Extension;

use Joomla\Application\ApplicationEvents;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Tallpine\Plugin\Console\Tallpine\Command\ImportTrailsCommand;

\defined('_JEXEC') or die;

final class Tallpine extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands'];
    }

    public function registerCommands(): void
    {
        $this->getApplication()->addCommand(new ImportTrailsCommand());
    }
}

The command class extends Joomla\Console\Command\AbstractCommand and declares its name with $defaultName:

<?php

namespace Tallpine\Plugin\Console\Tallpine\Command;

use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

\defined('_JEXEC') or die;

final class ImportTrailsCommand extends AbstractCommand
{
    protected static $defaultName = 'tallpine:import';

    protected function configure(): void
    {
        $this->setDescription('Import trails from a GPX folder');
        $this->addOption('folder', null, InputOption::VALUE_REQUIRED, 'Folder with GPX files');
    }

    protected function doExecute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $folder = (string) $input->getOption('folder');

        if ($folder === '' || !is_dir($folder)) {
            $io->error('Pass an existing folder with --folder.');

            return Command::FAILURE;
        }

        // ... create one trail per GPX file ...
        $io->success('Trails imported.');

        return Command::SUCCESS;
    }
}

Concerns specific to console plugins:

  • The plugin must be installed and enabled in the console group; list is the check. A disabled plugin gives Command "tallpine:import" is not defined.

  • The CLI loads only the behaviour, system and console groups. Plugins of the content, user or task groups are not imported automatically, so a command that triggers content events has to import them itself, and a command written for the web cannot assume a session, a logged-in user or HTTP_HOST.

  • Name commands vendor:action (tallpine:import) to avoid clashing with core namespaces. Core command classes are final.

  • Joomla 6.0 removed the CMS CliApplication and BaseApplication; console plugins are the replacement (see the next section).

Replacing legacy CLI scripts

Joomla 3 style CLI scripts placed in cli/ (a class extending JApplicationCli) were replaced by the Symfony Console in Joomla 4. On Joomla 5 the old CMS CliApplication and BaseApplication still exist for compatibility; on Joomla 6.0 they are available only through the Behaviour - Backward Compatibility 6 plugin and are scheduled for removal, together with \Joomla\CMS\Input (use \Joomla\Input). The migration is mechanical: turn the script into a command class, register it from a console plugin, replace $this->out() with SymfonyStyle output and $this->input->get() with InputInterface options. Books and blog posts that show a cli/myscript.php that bootstraps by require and JFactory::getApplication('cli') describe a platform that is end of life; the same applies to uses of the J* class aliases (JFactory, JText), which are about 450 entries of the BC6 class map and are off by default on new 6.x installs. See Deprecations and preparing for Joomla 7 and Error handling and legacy APIs.

Security of the CLI

  • Authorisation is the file system and the shell. The CLI has no login, no ACL and no audit trail of its own. Anyone who can execute PHP as a user that can read configuration.php can create Super Users, change the global configuration and read database credentials. Restrict shell and SSH access, and keep configuration.php unreadable for other users.

  • The web server must not serve cli/. The script refuses to run outside the command line (the application closes when it is not executed from the CLI), but the directory should still not be web-reachable. With the public folder (Joomla 5.0 and later) it is not, because only the public assets are exposed (Installation and the public folder).

  • Do not put secrets on command lines. Arguments appear in ps, in shell history and in CI logs. Use the hidden prompts of user:add and user:reset-password interactively, read values from environment variables or files, and do not run config:get --group db or --group mail in logged pipelines.

  • Destructive commands. database:import drops tables, extension:remove --no-interaction removes without asking, update:joomla:remove-old-files deletes files. Preview with --dry-run where it exists, back up first, and run against staging before production.

  • Keep an eye on the log. core:update writes the update log; the action log of the back end (see Privacy and action logs) records web actions, not CLI-created users, so record CLI operations in the deployment system.

Common errors and how to read them

Message or symptom Cause and remedy

Sorry, your PHP version is not supported.

The command-line PHP is older than 8.3.0. Check php -v and use the full path to the right binary.

It looks like you are trying to run Joomla! from our git repository.

libraries/vendor or media/vendor is missing. Run composer install and npm ci as for a development checkout.

Install Joomla to run cli commands

configuration.php is missing or empty. Install the site first (php installation/joomla.php install).

Command "x" is not defined.

A typo, a command that does not exist in this Joomla version (extension:enable before 6.1) or a disabled console plugin. Check list.

Can't find option *x* in configuration list

config:set or config:get was given a key that is not in configuration.php.

Options and values should be separated by "="

config:set sitename "My site" with a space instead of sitename="My site".

Failed to save properties

configuration.php is not writable by the CLI user, or it is intentionally read-only.

Database Table Structure not Up to Date

core:update refused because the schema has errors. Run maintenance:database, then maintenance:database --fix.

Task#NN is orphaned! Visit the backend to resolve.

The task’s plugin is disabled or uninstalled. Enable the plugin or trash the task with scheduler:state.

Garbage collection was not completed.

The session handler does not support garbage collection or the operation failed.

The "joomla/filesystem" Composer package is not installed

Reported by database:export on a stripped-down install; restore the full Joomla package.

Files owned by root, 403 or cache errors in the web

The CLI was run as root. Re-own the tree (chown -R www-data:www-data) and run as the web user.

Links to localhost or http:// in e-mails sent by a task

No --live-site and no $live_site. Set one of them.

Version notes

  • Joomla 4.0 introduced the console application and replaced the Joomla 3 cli/*.php scripts.

  • 4.1 added the scheduler commands, 5.0 added site:create-public-folder, 5.1 added core:update:channel and maintenance:database, 5.4 added core:autoupdate:register and core:autoupdate:unregister, and 6.1 added extension:enable and extension:disable.

  • The --live-site global option was added in 4.2.1.

  • Joomla 6.0 removed the CMS-level CLI application classes from the default class map (BC6 plugin only); commands are written as console plugins.

  • Joomla 6.x requires PHP 8.3 or newer on the command line as well as on the web. The release cycle (a major every two years with four minors, 5.x security-only, 4.x end of life) is in Versions, release cycle and support.

  • The CLI help is the primary documentation for options and may change between minors: pin the Joomla version in scripts and re-read help <command> after each upgrade.

Where to go next