VPS App installer

How to Install Drupal on a VPS with Docker

Drupal has been quietly powering government portals, university systems, and enterprise platforms for over two decades. NASA runs on it. The White House has run on it. Hundreds of national health agencies and financial institutions trust it for content that simply can’t fail. If you need a CMS that handles serious traffic, complex content hierarchies, and tightly controlled editorial workflows, Drupal belongs on your shortlist.

Getting it right on a VPS, though, takes more than a quick download. You need PHP, a database, a web server, and a deployment strategy that holds together months after launch. This guide solves all of that. Using Docker and Docker Compose, you’ll get Drupal 11 running on Ubuntu with Nginx handling public traffic and Let’s Encrypt covering your SSL. Every component is containerized so updates stay predictable and rollbacks stay painless.

What Is Drupal?

Drupal is an open-source content management system written in PHP. First released in 2001, it has grown from a simple discussion board into one of the most extensible CMS platforms on the internet. Where many systems give you templates and plugins, Drupal gives you something more fundamental: a structured data modeling engine that lets you define exactly what your content is before you decide how to display it.

Think of WordPress as a Swiss Army knife. Comfortable, familiar, and excellent for most jobs. Drupal is closer to a machinist’s lathe. The learning curve is steeper but the ceiling is dramatically higher. Complex multi-site networks, granular user permissions, deep API integrations, and sophisticated editorial workflows are all native territory for Drupal.

Drupal 11, the current major release, requires PHP 8.3 and ships with improved performance defaults, an overhauled administrative interface, and native support for decoupled (headless) architectures. A global contributor community maintains it with regular security releases and long-term support commitments. Before publishing this guide, verify that drupal:11 is the current stable tag on Docker Hub.

Prerequisites

Before you begin, make sure your VPS meets these requirements and your domain’s DNS A record is already pointed at your server’s IP address.

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

How to Install Drupal on a VPS with Docker

Step 1: Update Your Server

Always start with a fully updated system. Connect to your VPS over SSH and run:

sudo apt update && sudo apt upgrade -y

If the kernel updated, reboot before continuing:

sudo reboot

Reconnect over SSH once the server comes back online.

Step 2: Install Docker Engine

Skip the Docker package in Ubuntu’s default repository. It lags several major releases behind. Pull from the official Docker repository instead:

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

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

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

Add your user to the docker group so you don’t need sudo for every Docker command:

sudo usermod -aG docker $USER
newgrp docker

Confirm the installation succeeded:

docker --version
docker compose version

Step 3: Create the Project Directory

Keep your Drupal stack organized under a single dedicated directory:

mkdir -p ~/drupal && cd ~/drupal

Step 4: Configure Environment Variables

Database credentials belong in an environment file, not hardcoded inside your Compose configuration. Create the file:

nano .env

Add the following lines, replacing both placeholder values with strong, unique passwords:

DRUPAL_DB_PASSWORD=replace_with_strong_password
MARIADB_ROOT_PASSWORD=replace_with_another_strong_password

Lock down the file so only your user can read it:

chmod 600 .env

Step 5: Write the Docker Compose File

Create the Compose file that defines your two services, Drupal and MariaDB:

nano docker-compose.yml

Paste in the following configuration:

services:
  drupal:
    image: drupal:11
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - drupal_modules:/var/www/html/modules
      - drupal_profiles:/var/www/html/profiles
      - drupal_themes:/var/www/html/themes
      - drupal_sites:/var/www/html/sites
    depends_on:
      mariadb:
        condition: service_healthy
    networks:
      - drupal_network

  mariadb:
    image: mariadb:11
    restart: unless-stopped
    volumes:
      - mariadb_data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=drupal
      - MYSQL_USER=drupal
      - MYSQL_PASSWORD=${DRUPAL_DB_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - drupal_network

volumes:
  drupal_modules:
  drupal_profiles:
  drupal_themes:
  drupal_sites:
  mariadb_data:

networks:
  drupal_network:
    driver: bridge

A few design decisions are worth understanding here. The Drupal service binds to 127.0.0.1:8080 only so the container isn’t directly reachable from the public internet. Nginx handles all incoming traffic and forwards it internally. The depends_on directive with a service_healthy condition ensures Drupal waits for MariaDB to finish initializing before it attempts a connection — a common source of cryptic first-boot failures. Four named volumes persist your installed modules, uploaded files, themes, and site configuration across container restarts and image upgrades.

Step 6: Start the Containers

Launch both services in detached mode:

docker compose up -d

Watch the logs to confirm a clean startup:

docker compose logs -f

Press Ctrl+C to stop following the output. Check the status of both containers:

docker compose ps

Both should report a status of running. Once MariaDB shows as healthy and Drupal shows as running, you’re ready to set up Nginx.

Step 7: Install and Configure Nginx

Nginx sits between the public internet and your Drupal container, handling SSL termination and forwarding requests to port 8080. Install it:

sudo apt install -y nginx

Create a new server block for your Drupal site:

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

Paste in the configuration below, replacing yourdomain.com with your actual domain name:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    client_max_body_size 128M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

The client_max_body_size 128M directive allows file uploads up to 128 MB. Bump this higher if your editorial team regularly works with large media files. The X-Forwarded-Proto header tells Drupal whether the original request arrived over HTTP or HTTPS, which is essential for generating correct internal URLs.

Enable the configuration, test it, and reload Nginx:

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

Step 8: Configure the UFW Firewall

Open only the ports your server actually needs:

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

Confirm the active rules:

sudo ufw status

The Nginx Full profile opens both port 80 and port 443. Port 80 stays open temporarily for the Certbot domain verification challenge in the next step.

Step 9: Obtain a Let’s Encrypt SSL Certificate

Install Certbot and its Nginx plugin:

sudo apt install -y certbot python3-certbot-nginx

Request your certificate, making sure your domain’s DNS records already point to this server:

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot handles everything automatically. It verifies domain ownership, fetches the certificate, and modifies your Nginx configuration to add SSL directives and redirect all HTTP traffic to HTTPS. Verify that automatic renewal is working:

sudo certbot renew --dry-run

Step 10: Complete the Drupal Web Installer

Open your browser and navigate to https://yourdomain.com. Drupal’s installer walks you through setup in six straightforward screens.

  • Choose language. Select your preferred language and click Save and continue.
  • Choose profile. Select Standard. It pre-enables the most commonly needed modules including taxonomy, menus, and user account management out of the box.
  • Verify requirements. Drupal checks your PHP environment. The official Docker image ships with all required extensions so this screen passes without errors.
  • Set up database. This step requires specific values. Set the database type to MySQL, MariaDB, Percona Server, or equivalent. Enter drupal as both the database name and the username. Enter the value you set for DRUPAL_DB_PASSWORD as the password. Expand Advanced Options and set Host to mariadb (the Docker service name, not localhost). Leave the port at 3306.
  • Install site. Drupal creates the required database tables and installs the default module set. Give it one to two minutes.
  • Configure site. Set your site name, admin email address, admin username, and admin password. Use a strong, unique password for the admin account. Click Save and continue.

You’ll land on your new Drupal homepage when the installer finishes.

Step 11: Configure Trusted Hosts and Reverse Proxy Settings

Drupal running behind a reverse proxy needs two additional configuration entries to generate correct URLs and pass its internal security checks. Without them, you’ll see warnings in the status report and likely run into redirect problems.

After the web installer completes, Drupal sets settings.php to read-only. Make it temporarily writable and copy it to your host machine for editing:

docker compose exec drupal chmod 644 /var/www/html/sites/default/settings.php
docker cp "$(docker compose ps -q drupal)":/var/www/html/sites/default/settings.php ./settings.php

Open the file:

nano ./settings.php

Scroll to the very end and add the following block, replacing yourdomain.com with your actual domain:

$settings['trusted_host_patterns'] = [
  '^yourdomain\.com$',
  '^www\.yourdomain\.com$',
];

$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = ['127.0.0.1'];

Save and close the file. Copy it back into the container and restore the original read-only permissions:

docker cp ./settings.php "$(docker compose ps -q drupal)":/var/www/html/sites/default/settings.php
docker compose exec drupal chmod 444 /var/www/html/sites/default/settings.php
rm ./settings.php

Clear Drupal’s cache to apply the changes. Log into your admin panel, navigate to Administration > Configuration > Performance, and click Clear all caches.

How to Update Drupal

Keeping Drupal current with Docker is refreshingly simple. Navigate to your project directory and pull the latest image:

cd ~/drupal
docker compose pull

Recreate the container with the updated image:

docker compose up -d

After minor version updates, Drupal may need to apply database schema changes. Check by visiting https://yourdomain.com/update.php in your browser. If pending updates exist, follow the on-screen prompts to apply them. Finish by clearing the cache via Administration > Configuration > Performance.

Your uploaded files, installed modules, themes, and site configuration all live in named volumes so nothing is lost during the image swap.

Troubleshooting

502 Bad Gateway

This error means Nginx is running but can’t reach the Drupal container. Check whether the container is actually up:

docker compose ps

If Drupal shows as stopped or in a restart loop, inspect the logs for the underlying cause:

docker compose logs drupal

A startup failure often means MariaDB wasn’t ready when Drupal tried to connect on a previous boot. Run docker compose up -d to restart the stack. The healthcheck on the MariaDB service prevents this on subsequent starts.

Database Connection Error During Installation

If the web installer throws a connection error, the most likely culprit is entering localhost as the database host. Drupal runs inside a container and can’t reach the host’s loopback address. Return to the database setup screen, expand Advanced Options, and set Host to mariadb (the Docker service name). Also verify that the password you entered matches the DRUPAL_DB_PASSWORD value in your .env file exactly.

Redirect Loops or Mixed Content Warnings

A redirect loop or a browser console full of mixed content warnings usually means Drupal doesn’t know it’s running behind HTTPS. Confirm you’ve completed Step 11 and added both the reverse_proxy and reverse_proxy_addresses settings to settings.php. Also verify that your Nginx configuration includes the proxy_set_header X-Forwarded-Proto $scheme; directive inside the location block.

“Trusted Host Settings Not Configured” Warning

Drupal’s status report page (Administration > Reports > Status report) displays this security warning when trusted_host_patterns isn’t defined in settings.php. Follow Step 11 to add the patterns. Note that the values are PHP regular expressions so the dots in your domain name must be escaped with a backslash. A pattern like ^example\.com$ matches example.com exactly and rejects anything else.

File Uploads Failing or Returning a 413 Error

A 413 error means the uploaded file exceeds Nginx’s size limit. This guide sets client_max_body_size to 128 MB. If your team uploads large video files or high-resolution image sets, increase this value in /etc/nginx/sites-available/drupal and also raise Drupal’s own upload limit under Administration > Configuration > Media. Reload Nginx after any configuration change:

sudo systemctl reload nginx

What to Build Next

Your Drupal site is running but you’ve only scratched the surface of what the platform can do. Here are a few high-value next steps worth exploring.

  • Install Drush. Drush is the command-line companion to Drupal. Adding it to your workflow unlocks cache rebuilds, database updates, user management, and module administration from the terminal without touching a browser. The Drush documentation covers installation via Composer inside a running container.
  • Add Redis caching. For production traffic, a Redis container dramatically reduces database load. Add a Redis service to your Docker Compose file and install the Drupal Redis module to connect them. Sites with meaningful traffic feel the difference quickly.
  • Set up automated database backups. Your MariaDB data lives in a named Docker volume. Schedule a cron job to dump the database regularly using docker compose exec mariadb mysqldump and ship the output to off-server storage.
  • Explore Drupal Commerce. Building an e-commerce site? Drupal Commerce is one of the most capable open-source commerce platforms available. It integrates natively with Drupal’s content modeling and permissions systems so your product catalog is just another content type.
  • Add a CDN. Static assets like images, CSS, and JavaScript don’t need to hit your VPS on every request. Putting Cloudflare or a similar CDN in front of your Drupal site cuts load times and reduces server strain considerably during traffic spikes.

Wrapping Up

You now have Drupal 11 running on a VPS with a containerized MariaDB backend, Nginx managing public traffic, and Let’s Encrypt providing valid SSL. The setup is straightforward to update, easy to back up, and built on the same containerization principles used in production Drupal deployments worldwide. From here the platform scales well: add Redis caching, bolt on Drupal Commerce, build a decoupled frontend with Next.js, or simply start publishing. Drupal’s reputation for handling whatever you throw at it is well earned.