Install Craft CMS with LEMP Stack on AlmaLinux 8 by placing the current Craft starter project behind Nginx, PHP-FPM, MariaDB, and HTTPS. With DNS already pointed to the VPS, you can reach the Craft control panel in about 15–20 minutes.
Prerequisites
- AlmaLinux 8.10 or another fully updated AlmaLinux 8 system, with 2 vCPUs, at least 2 GB RAM, and 10 GB of free disk space.
- Root access or a sudo-enabled account over SSH. The commands below use
sudo. - A domain such as
YOUR_DOMAINwith an A record pointing to the VPS. Ports 80 and 443 must be reachable for the certificate request. - A unique database password, a separate Craft administrator password, and an email address for certificate notices.
- A VPSLake Linux VPS with a public IPv4 address is a suitable host for this stack.
- Do not run Apache on ports 80 or 443; this guide uses Nginx as the web server.
Craft 5 requires PHP 8.2 or newer, Composer 2, and MySQL 8.0.17+ or MariaDB 10.4.6+. This guide uses PHP 8.3 and MariaDB 10.11, which are available on AlmaLinux 8 through maintained upstream repositories. Review Craft’s official requirements before deploying a high-traffic site.
Step 1: Install PHP, Nginx, MariaDB, and Composer
AlmaLinux 8’s built-in PHP and MariaDB streams can be older than Craft’s minimums, so enable the Remi PHP stream and MariaDB’s 10.11 repository before installing the stack.
sudo dnf update -y
sudo dnf install -y epel-release firewalld nginx curl unzip tar policycoreutils-python-utils
sudo curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version="mariadb-10.11"
sudo dnf install -y MariaDB-server MariaDB-client
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo dnf module reset php -y
sudo dnf module enable php:remi-8.3 -y
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 mariadb nginx php-fpm
Install Composer 2 from its official installer, then check that the command-line PHP version and required extensions are visible. The installer checksum check prevents a damaged download from being executed.
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
php -v
composer --version
mariadb --version
You should see PHP 8.3.x, Composer 2.x, and MariaDB 10.11.x. Patch numbers change as the repositories publish updates.
Step 2: Create the Craft database
Craft stores content, users, and project configuration in MariaDB, so give this site a local-only account with privileges limited to its own database.
sudo mariadb <<'SQL'
CREATE DATABASE craftcms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'craft_user'@'localhost' IDENTIFIED BY 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON craftcms.* TO 'craft_user'@'localhost';
FLUSH PRIVILEGES;
SQL
sudo mariadb -e "SHOW DATABASES LIKE 'craftcms';"
Replace YOUR_DB_PASSWORD with a long, unique password before running the block. The final command should print a row named craftcms; MariaDB does not need a public firewall rule because PHP connects through localhost.
Step 3: Create the Craft project and apply SELinux-safe permissions
Craft’s Composer starter project provides the current application structure, while using web/ as the document root keeps .env, config, and vendor outside the public URL space.
Create the project without running its interactive post-install setup. This lets you prepare the database and permissions first.
sudo composer create-project craftcms/craft /var/www/craft --no-scripts
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'
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/storage /var/www/craft/config/project /var/www/craft/web/cpresources
sudo find /var/www/craft/storage /var/www/craft/config/project /var/www/craft/web/cpresources -type d -exec chmod 0770 {} \;
sudo find /var/www/craft/storage /var/www/craft/config/project /var/www/craft/web/cpresources -type f -exec chmod 0660 {} \;
Replace YOUR_DOMAIN and YOUR_DB_PASSWORD in the .env block with the same values used earlier. Craft needs PHP to write storage, config/license.key, and web/cpresources during installation; these group-writable directories provide that access without making the whole tree world-writable.
Persist the SELinux labels so the permissions survive relabeling or a reboot. SELinux stays enforcing throughout this setup.
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
sudo -u nginx env HOME=/var/lib/nginx php /var/www/craft/craft setup/app-id
sudo -u nginx env HOME=/var/lib/nginx php /var/www/craft/craft setup/security-key
The last two commands generate the application ID and security key in .env. Their output should end with a success message rather than a permission error.
Step 4: Configure Nginx for Craft
Craft routes requests through its front controller, so Nginx must serve the web/ directory and send unknown paths to web/index.php.
Create /etc/nginx/conf.d/craft.conf with the following server block:
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 $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_pass unix:/run/php-fpm/www.sock;
}
location ~ /\.(?!well-known) {
deny all;
}
}
NGINX
sudo nginx -t
sudo systemctl reload nginx
Replace YOUR_DOMAIN before running the block. The test should return syntax is ok and test is successful; if it does not, Nginx will keep the previous working configuration.
Step 5: Open the firewall and enable HTTPS
Allow only the services needed for administration and web traffic, then let Certbot add the TLS server block and HTTP-to-HTTPS redirect.
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 firewall-cmd --list-services
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. Certbot must resolve YOUR_DOMAIN to this VPS and reach port 80 from the internet. Its success output includes the certificate path and a message that HTTPS was enabled.
Step 6: Finish the Craft installation in the browser
The browser wizard creates the first administrator and runs Craft’s database migrations after the server-side project is ready.
Open https://YOUR_DOMAIN/admin/install and follow the setup screens. If the database form appears, use craftcms for the database, craft_user for the username, localhost for the server, port 3306, and the password from Step 2. Create a long, unique administrator password and keep it in a password manager.
After the wizard reports a successful installation, check the application from SSH:
cd /var/www/craft
sudo -u nginx env HOME=/var/lib/nginx php craft install/check
sudo systemctl reload nginx
The check should report that Craft is installed. Craft’s server setup guidance explains why the public root must remain the project’s web/ directory.
Verify the Craft installation
Run these checks, then sign in to the control panel and load the site’s front page. Together they confirm the services, PHP extensions, Nginx configuration, database connection, and public certificate.
sudo systemctl is-active nginx php-fpm mariadb 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 PHP module order can differ, and the first page may return a redirect to the control panel. A successful login at https://YOUR_DOMAIN/admin and a working front page are the final confirmation.
Troubleshooting
502 Bad Gateway
Nginx cannot reach PHP-FPM, usually because the service is stopped or the pool socket is owned by the wrong account. Confirm the socket exists and restart the services after applying the pool changes from Step 1.
sudo systemctl status php-fpm --no-pager
sudo ls -l /run/php-fpm/www.sock
sudo systemctl restart php-fpm nginx
Composer reports a missing PHP extension
Composer may be using a different PHP binary than PHP-FPM, or an extension was omitted from the Remi stream. Run php -v and php --ini, install the missing php-* package with DNF, then restart PHP-FPM and repeat the Composer command.
SQLSTATE[HY000] [1045] Access denied
The database password in .env does not match the MariaDB account, or the account was created for a different host. Re-run sudo mariadb -e "SELECT User,Host FROM mysql.user WHERE User='craft_user';", make sure the row is craft_user | localhost, and update the password in .env if necessary.
The installer returns a permission denied or 403 error
Check both Unix ownership and SELinux labels. Run sudo restorecon -Rv /var/www/craft, verify that storage, config, and web/cpresources are group-writable by nginx, and confirm that Nginx’s root points to /var/www/craft/web.
Certbot cannot validate YOUR_DOMAIN
DNS may still point to another address, or an upstream VPS firewall may block port 80. Confirm dig +short YOUR_DOMAIN returns the server IP, allow HTTP in the VPS control panel, and run Certbot again after DNS has updated.
Hardening and maintenance
- Keep the host updated with
sudo dnf update -yand back up the MariaDB database plus Craft’sstorageand uploaded assets before upgrades. - Do not expose MariaDB on the public interface. Keep port 3306 closed and use SSH tunneling for administrative database access.
- Test certificate renewal with
sudo certbot renew --dry-runand keep the certificate renewal timer enabled. - Run Craft and Composer updates during a maintenance window, keep
composer.lockunder version control, and usephp craft upafter deploying dependency changes.
FAQ
Can I install Craft CMS without a domain?
You can test the site over the VPS IP address, but a domain is the practical choice for a production installation. Let’s Encrypt validation in this guide requires a DNS name, and Craft’s generated site URL should match the hostname visitors use.
Why does Nginx point to the web directory?
Craft keeps application code, Composer dependencies, and environment secrets above the public web root. Serving only /var/www/craft/web prevents visitors from requesting files such as .env or composer.json directly.
Is MariaDB supported by Craft CMS?
Yes. Craft 5 lists MariaDB 10.4.6 or newer as a minimum, and this guide installs the maintained 10.11 series. Craft’s documentation notes that MySQL is the better choice for sites with many users or a large content volume because MariaDB and MySQL no longer have complete feature parity.
How do I update Craft after this installation?
Back up the database and project first, then run Composer in /var/www/craft to install the versions recorded in your project files. Finish with php craft up so Craft applies database migrations and project configuration changes.