VPS App installer

How to Install Joomla on a VPS with Docker

Somewhere between WordPress’s simplicity and Drupal’s complexity sits Joomla, a CMS that has been powering government portals, e-commerce storefronts, and community forums since 2005. With over 2 million active installations worldwide, it’s a proven platform backed by a rich extension ecosystem and a fiercely loyal developer community. If you’ve decided to take the self-hosting route and run Joomla on your own VPS, this guide covers everything from a freshly provisioned server to a production-ready, SSL-secured deployment.

The setup uses Docker and Docker Compose to containerize Joomla and its database, with Nginx acting as the reverse proxy and Let’s Encrypt providing free SSL certificates. This approach keeps your installation portable, straightforward to update, and cleanly isolated from the underlying operating system. No manual PHP version juggling. No tangled web of system packages to maintain.

What Is Joomla?

Joomla is a free, open-source content management system that lets you build and manage websites without writing application code from scratch. It ships with a powerful extension framework, multilingual support out of the box, and a granular access control system that rivals what most enterprise platforms charge a premium for. Think of it as the middle path between a beginner-friendly website builder and a full-blown custom application framework.

What sets Joomla apart is its flexibility. A single installation can power a simple blog, a multilingual news portal, a membership community, or a lightweight storefront. The official Joomla Extensions Directory lists over 7,000 add-ons covering everything from payment gateways to social networking features. You’re unlikely to outgrow it quickly.

Running Joomla on your own VPS puts you in complete control of the server environment. You choose the PHP version, tune database configuration, set your own resource limits, and never compete for compute power with hundreds of other tenants on a shared host. For high-traffic sites or projects with specific compliance requirements, that level of control is worth a great deal.

Prerequisites

Before you start, confirm your VPS meets the following specifications. You’ll also need a registered domain name with its DNS A record already pointing to your server’s public IP address.

ResourceMinimumRecommended
CPU1 vCPU2 vCPUs
RAM1 GB2 GB
Storage20 GB SSD40 GB SSD
Operating SystemUbuntu 22.04 LTSUbuntu 24.04 LTS
Root / Sudo AccessRequiredRequired
Domain NameRequiredRequired

You’ll also need basic familiarity with the Linux command line. The steps ahead are laid out in detail so you don’t need to be a systems administrator, but comfort with running commands in a terminal will make things go smoothly.

Installing Joomla on Your VPS

Step 1: Update Your Server

A freshly provisioned server is like a new apartment. Before you move any furniture in, do a thorough clean. Update all packages to their latest versions and close any known vulnerabilities before installing anything else.

sudo apt update && sudo apt upgrade -y

If the upgrade included a kernel update, reboot now so the changes take effect:

sudo reboot

Reconnect via SSH once the server is back up and move on to the next step.

Step 2: Install Docker Engine

Always install Docker Engine from the official Docker repository rather than the Ubuntu package manager. The Ubuntu-bundled version lags behind and can cause compatibility issues with modern Compose files.

Add Docker’s GPG key and official repository to your system’s package sources:

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Now install Docker Engine and the Compose plugin:

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Enable Docker so it starts automatically on boot and confirm both tools are working:

sudo systemctl enable docker
sudo systemctl start docker
docker --version
docker compose version

Step 3: Install Nginx and Certbot

Nginx will serve as the public-facing reverse proxy, handling all incoming web traffic and forwarding it to the Joomla container. Certbot automates SSL certificate provisioning from Let’s Encrypt so you don’t have to renew certificates manually.

sudo apt install -y nginx certbot python3-certbot-nginx
sudo systemctl enable nginx
sudo systemctl start nginx

Step 4: Configure the UFW Firewall

Lock your server down to only the ports it genuinely needs. SSH keeps you connected and “Nginx Full” covers both HTTP and HTTPS traffic:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

The status output should list OpenSSH and Nginx Full as allowed. Everything else stays blocked by default, which is exactly where you want it.

Step 5: Set Up the Project Directory

Keeping all Joomla-related configuration in one dedicated directory makes the deployment far easier to manage, back up, and troubleshoot later. Create it and move into it:

sudo mkdir -p /opt/joomla
cd /opt/joomla

Step 6: Configure Docker Compose

This is the core of your deployment. The Compose file defines two services: a MySQL database and the Joomla application itself. Create the file:

sudo nano /opt/joomla/docker-compose.yml

Paste in the following configuration. Replace both password placeholders with strong, unique values before saving.

services:
  db:
    image: mysql:8.0
    container_name: joomla_db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: replace_with_strong_root_password
      MYSQL_DATABASE: joomla
      MYSQL_USER: joomla
      MYSQL_PASSWORD: replace_with_strong_db_password
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  joomla:
    image: joomla:5
    container_name: joomla_app
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    environment:
      JOOMLA_DB_HOST: db
      JOOMLA_DB_USER: joomla
      JOOMLA_DB_PASSWORD: replace_with_strong_db_password
      JOOMLA_DB_NAME: joomla
    volumes:
      - joomla_data:/var/www/html
    depends_on:
      db:
        condition: service_healthy

volumes:
  db_data:
  joomla_data:

A few deliberate design decisions are worth understanding here. The database port isn’t exposed to the outside world at all; only the Joomla container communicates with MySQL directly over Docker’s internal network. The Joomla container binds to 127.0.0.1:8080 rather than 0.0.0.0:8080, which means Nginx is the only way in from the public internet. The depends_on block with condition: service_healthy prevents a common race condition where Joomla attempts to connect to MySQL before the database has finished its initialization sequence.

Before deploying, verify the joomla:5 tag on Docker Hub to confirm you’re pulling the latest release in that major version line.

Step 7: Start the Containers

With the configuration in place, bring the stack up in detached mode:

cd /opt/joomla
sudo docker compose up -d

Docker pulls the MySQL and Joomla images on first run so expect a short wait depending on your connection speed. Once complete, confirm both containers are running:

sudo docker compose ps

Both joomla_db and joomla_app should show a status of running. If the database still shows starting, it hasn’t finished initializing yet. Give it 30 seconds and check again. You can also watch the startup sequence unfold in real time:

sudo docker compose logs -f

Press Ctrl+C to exit the log stream once you’re satisfied both services started cleanly.

Step 8: Set Up Nginx as a Reverse Proxy

Create a new Nginx server block for your domain:

sudo nano /etc/nginx/sites-available/joomla

Paste in the following configuration, replacing your-domain.com with your actual domain name:

server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    client_max_body_size 50M;

    location / {
        proxy_pass          http://127.0.0.1:8080;
        proxy_http_version  1.1;
        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;
        proxy_read_timeout  300;
        proxy_connect_timeout 300;
    }
}

Enable the configuration, test Nginx’s syntax, and reload the service:

sudo ln -s /etc/nginx/sites-available/joomla /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If nginx -t reports a syntax error, double-check your domain spelling and confirm the proxy_pass port matches the port binding defined in your Compose file.

Step 9: Obtain an SSL Certificate

With Nginx configured and your domain’s DNS resolving to the server, Certbot can now request and install a certificate automatically:

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Certbot will ask for an email address for renewal alerts and prompt you to accept the Let’s Encrypt terms of service. It then verifies domain ownership, issues the certificate, and rewrites your Nginx configuration to redirect all HTTP traffic to HTTPS. The whole process takes under a minute.

Confirm the automatic renewal timer is active so your certificate never expires silently:

sudo systemctl status certbot.timer

An active (waiting) status means the timer is running and Certbot will handle all future renewals without any intervention from you.

Step 10: Complete the Joomla Web Installer

Open your browser and navigate to https://your-domain.com. The Joomla installation wizard loads automatically. Work through the setup screens:

  • Configuration: Enter your site name, a brief description, and your admin account credentials. Choose a strong, unique admin password. Joomla’s administrator panel is a frequent target for brute-force attempts so this isn’t the place to cut corners.
  • Database: Set the database type to MySQLi. Use db as the hostname (that’s the Docker service name, not localhost), joomla for both the database name and username, and the password you defined in docker-compose.yml.
  • Overview: Review your settings and click Install Joomla. The installer runs all database migrations and sets up the initial site structure.

Once installation finishes, Joomla prompts you to remove the installation folder. Do this immediately. Leaving it in place exposes your site to a trivial reconfiguration attack that any script kiddie can run. After removal, your site is live at https://your-domain.com and the admin panel is at https://your-domain.com/administrator.

If Joomla generates HTTP links instead of HTTPS after setup, go to System → Global Configuration → Server and set Force HTTPS to Entire Site. This tells Joomla to build all internal URLs using HTTPS regardless of what the container itself sees.

How to Update Joomla

Keeping Joomla current is one of the most impactful ongoing maintenance tasks for site security. New releases regularly patch vulnerabilities so don’t let your installation drift too far behind. With Docker, the update process is refreshingly simple.

Pull the latest image for your pinned major version and restart the containers:

cd /opt/joomla
sudo docker compose pull
sudo docker compose up -d

Docker replaces the running containers with updated ones while your data in the named volumes remains completely untouched. Clean up any old images afterward to reclaim disk space:

sudo docker image prune -f

Before pulling a new major version (for example, moving from Joomla 5 to a future Joomla 6), review the official Joomla changelog and extension compatibility notes. Major upgrades sometimes involve database schema changes or deprecated APIs that require checking your installed extensions first.

Troubleshooting

Even a clean deployment can hit unexpected snags. Here are the five most common problems and how to resolve them.

The Joomla Container Keeps Restarting

If docker compose ps shows joomla_app in a restarting or exited state, Joomla is almost certainly failing to reach the database. Pull up the container logs to see the exact error:

sudo docker compose logs joomla

The most common culprit is a password mismatch between the two services. Confirm that JOOMLA_DB_PASSWORD under the joomla service exactly matches MYSQL_PASSWORD under the db service in your Compose file. A single character difference causes a connection failure every time. If the passwords match and the error persists, try bringing the stack down and back up to force a clean initialization:

sudo docker compose down
sudo docker compose up -d

Nginx Returns a 502 Bad Gateway Error

A 502 means Nginx received the request but couldn’t hand it off to Joomla. Start by confirming the container is actually running:

sudo docker compose ps

If the container is up and healthy, check your Nginx proxy_pass directive. It must point to http://127.0.0.1:8080 and the Compose port entry for the Joomla service must read 127.0.0.1:8080:80. A mismatch between those two values is the most frequent cause of this error. Double-check both and reload Nginx after any corrections.

SSL Certificate Issuance Fails

Certbot uses an HTTP-01 challenge to verify domain ownership, which means it needs to reach your server on port 80 from the public internet. Two things commonly block this. First, confirm your firewall allows HTTP traffic:

sudo ufw status

Second, verify that your domain’s DNS resolves to your server’s actual IP:

dig +short your-domain.com

If the returned IP doesn’t match your VPS, the certificate will fail every time. DNS propagation can take anywhere from a few minutes to 48 hours. Wait it out and try again once the records resolve correctly.

The Web Installer Shows a Blank Page

A blank page during the web installer almost always points to a PHP memory limit issue inside the container. Check the Joomla logs for PHP fatal errors:

sudo docker compose logs joomla --tail=50

If you see Allowed memory size exhausted in the output, add a memory limit environment variable to the joomla service in your Compose file and restart the stack:

    environment:
      JOOMLA_DB_HOST: db
      JOOMLA_DB_USER: joomla
      JOOMLA_DB_PASSWORD: replace_with_strong_db_password
      JOOMLA_DB_NAME: joomla
      PHP_MEMORY_LIMIT: 256M
sudo docker compose up -d

Database Credentials Fail During the Web Installer

If the web installer can’t connect to the database, verify three things. First, confirm you’re using db as the database hostname and not localhost or 127.0.0.1. Docker’s internal networking resolves service names so the Joomla container reaches MySQL through the service name db specifically. Second, make sure the username and password entered in the installer match the MYSQL_USER and MYSQL_PASSWORD values from your Compose file exactly. Third, confirm you actually replaced the placeholder text before starting the containers. An unreplaced replace_with_strong_db_password value is a surprisingly easy thing to miss on a first run.

What to Build Next

Your Joomla installation is live and secured. Here are some natural next steps to turn it into a fully production-ready site:

  • Install a template: Swap out the default look with something that fits your brand. The Joomla Extensions Directory and third-party marketplaces offer hundreds of free and premium templates across every niche and industry.
  • Set up automated backups: Install Akeeba Backup, the gold standard for Joomla backups, and schedule regular snapshots of both your files and database. A site without a tested, off-server backup isn’t really protected.
  • Configure transactional email: Go to System → Global Configuration → Server and set up SMTP with a provider like Mailgun, Postmark, or Amazon SES. PHP’s built-in mail() function on a fresh VPS lands in spam almost immediately so this is worth doing before you send a single notification.
  • Enable page caching: Turn on Joomla’s built-in caching under System → Cache to reduce database load and speed up page delivery. On a moderate-traffic site, caching is one of the highest-leverage performance improvements you can make for almost no effort.
  • Restrict admin access by IP: Add an IP allowlist to the /administrator location block in your Nginx configuration. If your IP address is static, this single change dramatically reduces your attack surface against brute-force login attempts.

You now have a fully functional, SSL-secured Joomla installation running inside Docker on your own VPS. This setup gives you something shared hosting never can: complete ownership of the stack. When you need more resources you scale the server. When an update drops you pull a new image. The infrastructure bends to your needs rather than the other way around and that’s exactly how it should be.