How to Install Craft CMS with LEMP on Rocky Linux 8

Install Craft CMS with LEMP Stack on Rocky Linux 8 using PHP 8.3, Nginx, MySQL 8.4, Composer, SELinux-safe permissions, and HTTPS on a fresh VPS.
A polished Rocky Linux Craft CMS server scene with a web window, database cylinder, and green-blue light trails.

Install Craft CMS with LEMP Stack on Rocky Linux 8 by combining Nginx, PHP-FPM, MySQL 8.4, and Composer on a clean VPS. You will finish with a TLS-protected Craft control panel and a public web/ document root in about 15–20 minutes, assuming DNS already points to the server.

Prerequisites

  • Rocky Linux 8.10, fully updated, with 2 vCPUs, at least 2 GB of RAM, and 10 GB of free disk space.
  • A root account or sudo-enabled user connected over SSH. The commands below use sudo.
  • A domain such as YOUR_DOMAIN with an A record pointing to the VPS. Ports 80 and 443 must be reachable during certificate issuance.
  • A unique MySQL password, a Craft administrator password, and an email address for Let’s Encrypt notices.
  • A VPSLake Linux VPS with a public IPv4 address is suitable for this LEMP stack.
  • Do not run Apache or another service on ports 80 and 443; Nginx will own those ports.

Craft 5 needs PHP 8.2 or newer, Composer 2, and MySQL 8.0.17 or newer. This guide uses PHP 8.3 from Remi and MySQL 8.4 LTS; review Craft’s official requirements if you plan to add demanding plugins or a large content library.

Step 1: Install the LEMP packages

Rocky Linux 8’s default PHP stream can be too old for a new Craft project, so enable the Remi PHP 8.3 stream and MySQL’s EL8 repository before installing the services.

sudo dnf update -y
sudo dnf install -y epel-release firewalld nginx curl unzip tar policycoreutils-python-utils
sudo dnf module disable -y mysql
sudo dnf install -y https://dev.mysql.com/get/mysql84-community-release-el8-3.noarch.rpm
sudo dnf install -y mysql-community-server
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo dnf module reset -y php
sudo dnf module enable -y php:remi-8.3
sudo dnf install -y php php-bcmath php-cli php-common php-curl php-fpm php-gd php-intl php-mbstring php-mysqlnd php-opcache php-process php-xml php-zip
sudo sed -ri 's/^[; ]*user = .*/user = nginx/; s/^[; ]*group = .*/group = nginx/; s/^[; ]*listen.owner = .*/listen.owner = nginx/; s/^[; ]*listen.group = .*/listen.group = nginx/' /etc/php-fpm.d/www.conf
sudo sed -ri 's/^memory_limit = .*/memory_limit = 512M/; s/^upload_max_filesize = .*/upload_max_filesize = 64M/; s/^post_max_size = .*/post_max_size = 64M/; s/^max_execution_time = .*/max_execution_time = 120/' /etc/php.ini
sudo systemctl enable --now firewalld mysqld nginx php-fpm

The PHP-FPM pool runs as nginx, while MySQL remains bound for local application use. Check the versions before continuing:

php -v
mysql --version
nginx -v
PHP 8.3.x (cli)
mysql  Ver 8.4.x
nginx version: nginx/1.x

Run the interactive hardening wizard once, because a fresh MySQL installation has a temporary root password and default settings that should not remain on an internet-facing VPS.

sudo mysql_secure_installation

Choose a strong root password, remove anonymous users, disable remote root login, remove the test database, and reload the privilege tables when prompted. You will use a separate application account in the next step.

Step 2: Create a private MySQL database

Craft needs a database account with schema privileges, but it does not need MySQL exposed to the public network.

sudo mysql -u root -p

At the mysql> prompt, run the following SQL. Replace YOUR_DB_PASSWORD with a long, unique password before submitting it.

CREATE DATABASE craftcms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'craft_user'@'localhost' IDENTIFIED BY 'YOUR_DB_PASSWORD';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES, LOCK TABLES ON craftcms.* TO 'craft_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Confirm that the schema exists without opening port 3306:

sudo mysql -u root -p -e "SHOW DATABASES LIKE 'craftcms';"
Database (craftcms)
craftcms

Step 3: Install Composer and create the Craft project

The Composer starter creates the current Craft application layout. Keeping the project above Nginx’s public root prevents visitors from requesting .env, config, or vendor files.

Install Composer 2 with its published checksum, then create the project at /var/www/craft:

set -eu
sudo curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php
EXPECTED_CHECKSUM="$(curl -fsSL https://composer.github.io/installer.sig)"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', '/tmp/composer-setup.php');")"
test "$EXPECTED_CHECKSUM" = "$ACTUAL_CHECKSUM"
sudo php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm -f /tmp/composer-setup.php
sudo env COMPOSER_ALLOW_SUPERUSER=1 composer create-project craftcms/craft /var/www/craft --no-scripts

Create the environment file and the directories Craft must write during installation. Replace both placeholders in this block with the values you chose earlier.

sudo install -d -o root -g nginx -m 0770 /var/www/craft/storage /var/www/craft/config/project /var/www/craft/web/cpresources
sudo install -o root -g nginx -m 0660 /dev/null /var/www/craft/.env
sudo tee /var/www/craft/.env > /dev/null <<'ENV'
CRAFT_APP_ID=
CRAFT_SECURITY_KEY=
CRAFT_ENVIRONMENT=production
PRIMARY_SITE_URL="https://YOUR_DOMAIN/"
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=localhost
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=craftcms
CRAFT_DB_USER=craft_user
CRAFT_DB_PASSWORD="YOUR_DB_PASSWORD"
CRAFT_DB_SCHEMA=
CRAFT_DB_TABLE_PREFIX=
ENV
sudo chown -R root:nginx /var/www/craft
sudo find /var/www/craft -type d -exec chmod 0750 {} \;
sudo find /var/www/craft -type f -exec chmod 0640 {} \;
sudo chmod 0660 /var/www/craft/.env
sudo chmod 0770 /var/www/craft/config /var/www/craft/config/project /var/www/craft/storage /var/www/craft/web/cpresources

Replace YOUR_DOMAIN and YOUR_DB_PASSWORD; do not leave either placeholder in the live file. Generate Craft’s application ID and security key as the same account that PHP-FPM uses:

cd /var/www/craft
sudo -u nginx env HOME=/var/lib/nginx php craft setup/app-id
sudo -u nginx env HOME=/var/lib/nginx php craft setup/security-key

Each command should finish successfully and write its value into .env. These permissions avoid world-writable files and leave SELinux enabled.

Step 4: Apply SELinux labels for Craft

SELinux permits Nginx to read the project by default, but Craft needs write access to its storage, project configuration, and control-panel resources.

sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/craft(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/craft/(config|storage|web/cpresources)(/.*)?'
sudo restorecon -Rv /var/www/craft

The final command should list restored paths without AVC denial errors. The writable context applies only to the directories Craft documents as runtime storage.

Step 5: Configure Nginx for the Craft web root

Craft’s front controller handles clean URLs, so Nginx must serve /var/www/craft/web and route unknown paths to index.php.

sudo tee /etc/nginx/conf.d/craft.conf > /dev/null <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name YOUR_DOMAIN;

    root /var/www/craft/web;
    index index.php;
    client_max_body_size 64m;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param HTTP_PROXY "";
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}
NGINX
sudo nginx -t
sudo systemctl reload nginx

Change YOUR_DOMAIN before running the block. The configuration test should report syntax is ok and test is successful; Nginx will not reload an invalid file.

Step 6: Open web ports and enable HTTPS

Allow SSH plus the two web services through Rocky’s firewalld, then let Certbot add the TLS configuration and redirect HTTP traffic.

sudo firewall-cmd --permanent --zone=public --add-service=ssh
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx --redirect --agree-tos --no-eff-email -m YOUR_EMAIL -d YOUR_DOMAIN

Replace YOUR_EMAIL with an address you monitor. The domain must already resolve to this server, and port 80 must be reachable while Certbot validates it. A successful run reports the certificate location and an HTTPS redirect.

Step 7: Run the Craft web installer

The browser installer completes the database migrations and creates the first administrator after the server-side project is ready.

Open https://YOUR_DOMAIN/admin/install and enter these database values if Craft asks for them:

  • Database: craftcms
  • User: craft_user
  • Server: localhost
  • Port: 3306
  • Password: the value used for YOUR_DB_PASSWORD

Create a separate administrator password and store it in a password manager. For the reasoning behind a separate web/ document root and the Nginx pattern above, see Craft’s server setup guidance.

Verify the Craft installation

Check the service states, PHP extensions, Nginx syntax, Craft installation status, and HTTPS response from SSH. Then sign in at https://YOUR_DOMAIN/admin and load the site’s front page.

sudo systemctl is-active nginx php-fpm mysqld firewalld
sudo nginx -t
php -m | grep -E 'bcmath|curl|gd|intl|mbstring|mysqli|PDO|xml|zip'
cd /var/www/craft
sudo -u nginx env HOME=/var/lib/nginx php craft install/check
curl -fsSI https://YOUR_DOMAIN/
active
active
active
active
syntax is ok
test is successful
HTTP/2 200

The extension list can appear in a different order, and the first request may redirect to the control panel. A successful Craft login and a rendered front page confirm the complete stack.

Troubleshooting

502 Bad Gateway

Nginx cannot reach PHP-FPM when the service is stopped or the pool socket is not readable by Nginx. Check the socket and restart both services:

sudo systemctl status php-fpm --no-pager
sudo ls -l /run/php-fpm/www.sock
sudo systemctl restart php-fpm nginx

If the socket path differs, use the listen = value in /etc/php-fpm.d/www.conf in the fastcgi_pass directive.

SQLSTATE[HY000] [1045] Access denied

The password in .env does not match the craft_user account, or the account was created for a different host. Run sudo mysql -u root -p -e "SELECT User,Host FROM mysql.user WHERE User='craft_user';"; the host should be localhost, then update the password in .env to match.

403 Forbidden or permission denied during setup

Check the Unix group and SELinux labels instead of loosening permissions. Run sudo restorecon -Rv /var/www/craft, confirm that storage, config/project, and web/cpresources are group-writable by nginx, and verify that Nginx’s root is exactly /var/www/craft/web.

Certbot says the challenge failed

The DNS record may still point elsewhere, or an upstream VPS firewall may block port 80. Confirm dig +short YOUR_DOMAIN returns the VPS address, allow HTTP in the VPS control panel, and run Certbot again after DNS propagation.

Hardening and maintenance

  • Keep MySQL private: do not add a firewalld rule for port 3306, and use an SSH tunnel for remote database administration.
  • Back up the database, storage, and uploaded assets before Craft or Composer updates.
  • Test renewal with sudo certbot renew --dry-run and keep the renewal timer enabled.
  • Update the host with sudo dnf update -y; review Craft and plugin changes before applying them to production.

FAQ

Can I use an IP address instead of a domain?

You can use the VPS IP for an initial test, but a domain is the right choice for a public Craft site. The HTTPS step relies on DNS validation, and PRIMARY_SITE_URL should match the hostname visitors will use.

Why does this guide use MySQL instead of MariaDB?

Both database families meet Craft’s minimum requirements, but Craft currently recommends MySQL for sites with many users or a large amount of content. MySQL 8.4 also gives this Rocky Linux 8 setup a maintained LTS series with an official EL8 repository.

Which Craft files need write access?

At runtime, PHP needs to write to storage, config/license.key, and web/cpresources. The guide grants the Nginx group access to those locations while keeping the rest of the project outside the public root.

How should I update Craft later?

Take a database and file backup, run Composer from /var/www/craft, and use php craft up to apply migrations and project configuration changes. Perform updates in a maintenance window and check plugin compatibility before upgrading the CMS core.

Samrat Ghosh
Written by

Samrat Ghosh

Founder & Infrastructure Engineer

Samrat Ghosh is the founder of VPSLake, where he builds and runs the remote desktop and VPS hosting infrastructure the company is built on. He writes hands-on guides about RDP, Windows Server, VPS management and secure remote access - the practical documentation he wishes had…

Previous Article

5 Best QloudRDP Alternatives in 2026

Next Article

5 Best FireVPS Alternatives in 2026

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨