Local Development with Docker

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.

A Joomla site needs PHP, a web server and a database, and a throwaway local copy needs them to be easy to create, to reset and to match production. The official joomla image on Docker Hub provides exactly that: Apache (or PHP-FPM), PHP and the Joomla files, with an entrypoint that can install the site without opening a browser. This page, checked on 2026-10-30, runs the Tallpine Hiking Club site (package pkg_tallpine, component com_tallpine, child template cassiopeia_tallpine) with Joomla 6.2 on PHP 8.4, MariaDB and Mailpit, then compares the alternatives (XAMPP, MAMP, DDEV) and explains how to build Joomla core from its Git repository. It pins Joomla 6.2.x and PHP 8.3 or later, the platform of the current release. For the container tooling itself see Compose for local development; for requirements and hosting see Requirements and Hosting.

The official image and its tags

The image is the Docker Official Image joomla (https://hub.docker.com/_/joomla). Its tags combine three choices.

Choice Values (Joomla 6.2 line)

Joomla version

6.2.0 (exact), 6.2 (follows patch releases), 6 (follows minors), latest. The 5.4 line (5.4.10, PHP 8.2 and 8.3) is published alongside it for sites that are still on the security-only branch.

PHP version

php8.3, php8.4, php8.5. The unqualified 6.2, 6 and latest tags are PHP 8.4 on Apache.

Server variant

apache (Apache with PHP, the default), fpm (PHP-FPM only, needs a separate web server such as nginx) and fpm-alpine (PHP-FPM on Alpine Linux with musl libc, the smallest image).

So 6.2.0-php8.4-apache, 6.2-php8.5-fpm and 6.2.0-php8.3-fpm-alpine are all valid. Pin at least the minor and the PHP version (6.2-php8.4-apache) in a project, and an exact patch (6.2.0-php8.4-apache) when reproducing a bug: latest moves to the next major without warning. Match the PHP version to the one production will run; Joomla 6 needs PHP 8.3 or later, which is why PHP 8.2 appears only on the 5.4 tags. The Alpine variant is attractive for size but its musl libc differs from the Debian build that most hosts use, so prefer apache or fpm when the goal is production parity.

Older books and tutorials build on Joomla 3 or 4 images and tags such as joomla:3.9-php7.4-apache. Those lines are end of life (4.x since 2025-10). Developing Extensions for Joomla! 5 (2023) and Joomla! 4 Masterclass (2023) do not cover Docker, and the platform they target is security-only or end of life.

Configuration variables

The entrypoint reads environment variables on first start. Database variables tell it where the database is; the auto-install variables let it complete the installer itself.

Database

Variable Meaning

JOOMLA_DB_HOST

Host and optional port of the database server. Defaults to the linked mysql container; in Compose use the service name (db).

JOOMLA_DB_USER

Database user. Defaults to root; always use a dedicated user instead.

JOOMLA_DB_PASSWORD

Database password. Defaults to the linked container’s MYSQL_ROOT_PASSWORD.

JOOMLA_DB_PASSWORD_FILE

Path of a file that contains the password (a Docker or Compose secret), as an alternative to the variable.

JOOMLA_DB_NAME

Database name, joomla by default. It is created at start-up when the user is allowed to.

JOOMLA_DB_TYPE

mysqli (default, also used for MariaDB) or pgsql for PostgreSQL.

Automatic installation

When the site variables are set the container installs Joomla on first start and the browser installer is skipped.

Variable Meaning

JOOMLA_SITE_NAME

The site name.

JOOMLA_ADMIN_USER

The full name of the first Super User.

JOOMLA_ADMIN_USERNAME

The login name of the Super User.

JOOMLA_ADMIN_PASSWORD

The Super User password. It must satisfy the installer’s password rules.

JOOMLA_ADMIN_EMAIL

The Super User e-mail address.

JOOMLA_EXTENSIONS_URLS

Semicolon-separated URLs of extension packages to install during the set-up.

JOOMLA_EXTENSIONS_PATHS

Semicolon-separated paths, inside the container, of extension packages to install.

JOOMLA_SMTP_HOST, JOOMLA_SMTP_HOST_PORT

Host and port of the outgoing mail server. These are the only two SMTP variables the image documents.

JOOMLA_EXTENSIONS_PATHS is what makes the container useful for extension work: build pkg_tallpine.zip on the host, mount the folder that contains it and name the file in the variable, and a fresh site starts with the package already installed. The variables apply only when the site is first created; once the joomla_data volume holds a configuration.php, changing them does nothing, and the site is reset by removing the volumes (see Resetting and persistence).

The password variables are the weak spot. Environment variables are visible with docker inspect and in CI logs, so a Compose file never contains a literal password: the values below come from an untracked .env file, and the Compose interpolation form ${NAME:?message} makes docker compose stop with a message when one is missing. JOOMLA_DB_PASSWORD_FILE with a Compose secret is the stronger option when the file is shared with other developers.

A Compose file with MariaDB and Mailpit

The stack has three services. joomla is the site, db is MariaDB and mailpit catches every e-mail the site sends, so registration, password reset and notification mails can be tested without reaching a real mailbox.

flowchart LR B[Browser] -->|8080| J[joomla service<br/>Apache and PHP 8.4] B -->|8025 web UI| M[mailpit service] J -->|3306| D[(db service<br/>MariaDB)] J -->|SMTP 1025| M J --- V1[(joomla_data volume<br/>/var/www/html)] D --- V2[(db_data volume)] H[packages folder on the host] -.->|bind mount, read-only| J

First create the untracked .env file next to compose.yaml. The values are placeholders; generate your own, for instance with openssl rand -base64 24, and add .env to .gitignore.

cat > .env <<'ENV'
TALLPINE_DB_PASSWORD=CHANGE_ME_DB
TALLPINE_DB_ROOT_PASSWORD=CHANGE_ME_ROOT
TALLPINE_ADMIN_PASSWORD=CHANGE_ME_ADMIN_AT_LEAST_12_CHARS
TALLPINE_ADMIN_EMAIL=admin@tallpine.test
ENV
echo ".env" >> .gitignore

Then the Compose file.

name: tallpine

services:
  joomla:
    image: joomla:6.2-php8.4-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    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
      JOOMLA_SITE_NAME: Tallpine Hiking Club
      JOOMLA_ADMIN_USER: Tallpine Admin
      JOOMLA_ADMIN_USERNAME: tallpine-admin
      JOOMLA_ADMIN_PASSWORD: ${TALLPINE_ADMIN_PASSWORD:?set it in .env}
      JOOMLA_ADMIN_EMAIL: ${TALLPINE_ADMIN_EMAIL:?set it in .env}
      JOOMLA_SMTP_HOST: mailpit
      JOOMLA_SMTP_HOST_PORT: "1025"
      JOOMLA_EXTENSIONS_PATHS: /packages/pkg_tallpine.zip
    volumes:
      - joomla_data:/var/www/html
      - ./packages:/packages:ro

  db:
    image: mariadb:11.8
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: tallpine
      MARIADB_USER: tallpine
      MARIADB_PASSWORD: ${TALLPINE_DB_PASSWORD:?set it in .env}
      MARIADB_ROOT_PASSWORD: ${TALLPINE_DB_ROOT_PASSWORD:?set it in .env}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  mailpit:
    image: axllent/mailpit:v1.27
    restart: unless-stopped
    ports:
      - "8025:8025"
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: "1"
      MP_SMTP_AUTH_ALLOW_INSECURE: "1"

volumes:
  joomla_data:
  db_data:

Start it and watch the first start, which creates the database tables and the Super User:

mkdir -p packages
docker compose up -d
docker compose logs -f joomla

When the log settles, the site is at http://localhost:8080, the administrator at http://localhost:8080/administrator (log in with tallpine-admin and the password from .env) and every outgoing mail in the Mailpit inbox at http://localhost:8025.

Points worth understanding in this file:

  • No published port for the database or for SMTP. Services reach each other by name on the Compose network, so only the two browser ports are published. Publishing 3306 for a desktop database client is a convenience to add deliberately and to bind to loopback ("127.0.0.1:3306:3306").

  • depends_on with a health check. The db service reports healthy only when MariaDB accepts connections, so the joomla entrypoint does not race the database on a cold start. The official MariaDB image has no default health check; its healthcheck.sh script is provided for this purpose.

  • MariaDB, not MySQL. The Docker Hub example uses MySQL 8.0; both work, and the image variable is the same (JOOMLA_DB_TYPE stays mysqli). Use the database engine and major version that production runs, because Joomla does not support migrating between MySQL or MariaDB and PostgreSQL after the fact.

  • Mailpit accepts anything. MP_SMTP_AUTH_ACCEPT_ANY lets Joomla send with any or no credentials. Mailpit keeps the messages instead of delivering them, which is the point; it must not be reachable from a network you do not trust.

  • Bind-mounted packages. The package that JOOMLA_EXTENSIONS_PATHS installs is built on the host and mounted read-only; the read-write Joomla files stay in the named volume.

After the first start, open System > Global Configuration > Server > Mail Settings and confirm that the mailer is SMTP with host mailpit, port 1025, no authentication and no encryption, then use Send Test Mail. The mail templates that produce the messages are covered in Mail Templates and Email Settings.

PHP-FPM variants

With fpm or fpm-alpine the container runs PHP-FPM and serves no HTTP. A second container, for instance nginx, must share the Joomla files (the same volume) and pass .php requests to the joomla service on the FastCGI port. The extra moving parts only pay off when the goal is to rehearse a production nginx set-up, which is described in Web Server, Reverse Proxy and HTTPS; for everyday development the apache variant is simpler.

Working with the running container

# Joomla's command line interface inside the container
docker compose exec --user www-data joomla php cli/joomla.php list
docker compose exec --user www-data joomla php cli/joomla.php cache:clean

# A shell, and the container log
docker compose exec joomla bash
docker compose logs --tail=100 joomla

# A database shell (uses the container's own variables)
docker compose exec db sh -c 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"'

Run commands as the web server user (--user www-data) when they write files such as the cache, logs or configuration.php; otherwise the files end up owned by root and the site can no longer write them. The command line interface is covered in Command Line Interface.

Resetting and persistence

The Joomla files live in joomla_data and the database in db_data, so docker compose down keeps the site and docker compose down -v deletes both volumes and returns to a clean first start, where the auto-install variables run again. That is the fastest way to test an installer script or an install of pkg_tallpine from scratch. Because the whole of /var/www/html is in the volume, the volume also holds the code of Joomla itself: pulling a newer image tag does not upgrade an existing site. Update the site as in production, from the administrator, as described in Updating Joomla and Automated Core Updates, or remove the volumes and start again.

Developing an extension in the container

Two approaches work. The first, shown above, installs a built package. The second bind-mounts the source of a single extension over the installed one so edits show immediately:

services:
  joomla:
    volumes:
      - joomla_data:/var/www/html
      - ./com_tallpine/administrator:/var/www/html/administrator/components/com_tallpine
      - ./com_tallpine/site:/var/www/html/components/com_tallpine
      - ./com_tallpine/media:/var/www/html/media/com_tallpine

The component must already be installed once so that its database records and namespace map exist. If new classes or a new extension are not picked up, delete administrator/cache/autoload_psr4.php in the container; Joomla regenerates it on the next request. On macOS and Windows bind mounts are slower than a named volume, which is the reason to mount only the extension and not the whole site.

Alternatives

Tool Notes

XAMPP, WampServer, Bearsampp (Windows), MAMP (macOS)

Bundled Apache, MariaDB or MySQL and PHP installed on the host. Quick for a single site, but with little isolation from the rest of the machine and, usually, one PHP version at a time. Check that the bundled PHP is 8.3 or later and enable the extensions the installer reports as missing. The Joomla Manual’s get-started page names WAMP, Bearsampp and XAMPP; the User Guide’s local-setup page adds Laragon, a Linux LAMP stack, Docker, a build from source and a Raspberry Pi set-up.

Laragon (Windows)

An all-in-one local server presented by the User Guide as a lightweight alternative to WAMP and XAMPP.

DDEV

A Docker-based tool with per-project PHP, database and web-server versions, HTTPS on .ddev.site names and project commands. It has a dedicated joomla project type (the quickstart downloads the latest release and runs the CLI installer), and a generic php type, ddev config --project-type=php, for any other PHP code base, with ddev config --docroot=public or another folder when the web root is a sub-folder.

The official image, as above

The closest match to a container-based production deployment and the smallest set-up when only Joomla is needed.

DDEV is the better choice when several projects with different PHP versions share a machine, or when the team already uses it for Drupal and WordPress. A DDEV set-up for a downloaded Joomla looks like this:

mkdir tallpine && cd tallpine
curl -fsSL -o joomla.zip "https://www.joomla.org/latest"
unzip joomla.zip && rm joomla.zip
ddev config --project-type=joomla
mkdir -p .ddev/php
printf 'display_errors = off\noutput_buffering = off\n' > .ddev/php/joomla.ini
ddev start
ddev launch /administrator

Finish the installation in the browser, or with the installation/joomla.php install command from the quickstart, using the database host db with the user, password and database name db that DDEV provides for local use. Those credentials are a local convention of DDEV and exist only inside the developer’s machine.

Working on Joomla core itself

Everything above installs a release package. To change Joomla itself, test a pull request or build the front-end assets you need the Git repository. Composer and Node.js are required only for this (the Developer Manual says so on its Composer and npm pages, which are still stubs; the source tree’s README and package.json are the working reference).

git clone https://github.com/joomla/joomla-cms.git
cd joomla-cms
git checkout 6.2-dev
composer install
npm ci
npm run build:dev
  • composer install installs the PHP dependencies.

  • npm ci installs the exact versions in package-lock.json. The package.json requires Node.js 20.0.0 or later and npm 10.1.0 or later, and npm ci deletes and recreates node_modules, so it is also the fix for a broken one.

  • npm run build:dev compiles the JavaScript and Sass sources with the development settings; npm run build uses the production settings, and npm run watch:dev rebuilds on every change. The scripts lint:js and lint:css run the linters and cypress:run the browser tests.

  • The repository is not installable out of the box: its README points to the stable package or a nightly build for a working site. For development, serve the checkout with any of the set-ups above (the container with the checkout bind-mounted as /var/www/html is one way) and run the installer.

  • After pulling changes that add, delete or rename extensions, delete administrator/cache/autoload_psr4.php.

Testing of extensions in this environment continues in Testing Extensions and the CI builds in Build and CI for Extensions.

Common errors and pitfalls

  • The installer starts although the variables are set. The auto-install applies to a new site. An existing joomla_data volume with an older site, or a half-finished install, keeps its state; reset with docker compose down -v.

  • "Database connection failed" on the first start. Usually the database was not ready, or the user and database were created with different credentials in the db service than in the JOOMLA_DB_* variables. Both services read the same .env variable here for that reason. Changing MARIADB_PASSWORD after the data directory exists has no effect.

  • Mail is not arriving. Check the mailer under Global Configuration, use the Mailpit address mailpit (not localhost) from inside the container, and make sure the mail is not waiting for a scheduled task that has not run.

  • Files owned by root or not writable. Run commands with --user www-data, and fix bind-mounted folders on Linux with a matching UID.

  • Slow pages on macOS and Windows. Bind-mounting the whole site is slow; keep the site in a named volume.

  • Using latest. It moves to the next major release. Pin the tag.

  • Treating the local stack as production. The Compose file here publishes plain HTTP and uses throwaway credentials; production needs HTTPS, secrets, backups and separate database hosting, as covered in configuration.php and .htaccess and Security Hardening, MFA and Passkeys.

  • Following Joomla 3 or 4 material. Images, PHP versions, the JPATH_PLATFORM guard and the legacy J* classes in older tutorials are out of date for 6.x; see Where Older Books and Tutorials Are Outdated.