{"id":1019,"date":"2026-11-06T13:00:00","date_gmt":"2026-11-06T13:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1019"},"modified":"2026-11-06T13:00:00","modified_gmt":"2026-11-06T13:00:00","slug":"install-backdrop-cms-lemp-rocky-linux-8","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/06\/install-backdrop-cms-lemp-rocky-linux-8\/","title":{"rendered":"How to Install Backdrop CMS with LEMP on Rocky Linux 8"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Backdrop CMS with LEMP Stack on Rocky Linux 8 by putting Nginx, MariaDB, and PHP-FPM behind SELinux and HTTPS. On a fresh VPS with DNS already pointed at the server, you can reach the Backdrop installer in about 10\u201315 minutes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Rocky Linux 8.x with at least 1 GB of RAM, 10 GB of free disk space, and a public IPv4 address.<\/li>\n\n\n<li>Root access or a sudo-enabled user over SSH. The commands below assume a non-root user and use <code>sudo<\/code>.<\/li>\n\n\n<li>A domain such as <code>YOUR_DOMAIN<\/code> with an A record pointing to the VPS. Certbot needs this name to resolve publicly before it can issue a certificate.<\/li>\n\n\n<li>A strong, unique database password and a separate strong password for the Backdrop administrator.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> running Rocky Linux 8 is suitable for a small Backdrop site.<\/li>\n\n\n<li>Do not run Apache on ports 80 or 443 on this server; this guide installs Nginx as the web server.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Install the LEMP packages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky Linux 8 uses <code>dnf<\/code>, and the Remi PHP stream provides a current PHP release while retaining Rocky\u2019s PHP-FPM service layout.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf update -y\nsudo dnf install -y epel-release firewalld mariadb-server unzip curl bind-utils policycoreutils-python-utils\nsudo dnf module enable nginx:1.20 -y\nsudo dnf install -y nginx\nsudo dnf install -y https:\/\/rpms.remirepo.net\/enterprise\/remi-release-8.rpm\nsudo dnf module reset php -y\nsudo dnf module enable php:remi-8.3 -y\nsudo dnf install -y php php-fpm php-cli php-mysqlnd php-gd php-mbstring php-opcache php-xml php-curl php-zip php-intl\nsudo systemctl enable --now firewalld mariadb nginx php-fpm\nphp -v\nnginx -v\nmariadb --version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see PHP 8.3.x, an Nginx version, and a MariaDB client version. Patch numbers change as Rocky, Remi, and EPEL publish updates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The package command also installs the tools used later for archive extraction, DNS checks, and SELinux policy management. Starting the services now makes it easier to identify a package or socket problem before Backdrop is added.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On Rocky Linux, PHP-FPM may initially run its pool as <code>apache<\/code>. Nginx needs to read the PHP-FPM socket, so assign the pool and socket to the <code>nginx<\/code> account before testing the site.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>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\nsudo systemctl restart php-fpm\nsudo systemctl is-active php-fpm<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>active<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Create a MariaDB database and user<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Backdrop keeps content, accounts, and configuration data in MariaDB, so give this site its own database and a local-only database account.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mariadb &lt;&lt;'SQL'\nCREATE DATABASE backdrop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\nCREATE USER 'backdrop_user'@'localhost' IDENTIFIED BY 'YOUR_DATABASE_PASSWORD';\nGRANT ALL PRIVILEGES ON backdrop.* TO 'backdrop_user'@'localhost';\nFLUSH PRIVILEGES;\nSQL\nsudo mariadb -e \"SHOW DATABASES LIKE 'backdrop';\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_DATABASE_PASSWORD<\/code> with a unique password before running the block. The check should return one row named <code>backdrop<\/code>; keep the database name, username, and password for the browser installer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The account is restricted to <code>localhost<\/code>, so a remote host cannot use these credentials to connect to MariaDB. The <code>utf8mb4<\/code> character set also gives Backdrop room for full Unicode content instead of relying on an older default encoding.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Download Backdrop and prepare the document root<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Using a numbered release archive avoids pulling an unstable development branch and makes future upgrades easier to identify. This guide uses Backdrop 1.35.1, the current release listed by the project.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BACKDROP_VERSION=1.35.1\nsudo curl -fL -o \/tmp\/backdrop.zip \"https:\/\/github.com\/backdrop\/backdrop\/releases\/download\/${BACKDROP_VERSION}\/backdrop.zip\"\nsudo mkdir -p \/tmp\/backdrop-release \/var\/www\/backdrop\nsudo unzip -q \/tmp\/backdrop.zip -d \/tmp\/backdrop-release\nBACKDROP_ROOT=\"$(find \/tmp\/backdrop-release -type f -name index.php -printf '%h\\n' | head -n 1)\"\nsudo cp -a \"$BACKDROP_ROOT\"\/. \/var\/www\/backdrop\/\nsudo mkdir -p \/var\/www\/backdrop\/files<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The web server only needs to write uploads and generated files. Keep the application code owned by root, make it readable by Nginx, and leave <code>settings.php<\/code> temporarily group-writable so the installer can save the database connection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This split is useful during upgrades: an attacker who gains a web process should not be able to rewrite the CMS core, while Backdrop can still create media and generated image styles under <code>files<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chown -R root:nginx \/var\/www\/backdrop\nsudo find \/var\/www\/backdrop -type d -exec chmod 750 {} \\;\nsudo find \/var\/www\/backdrop -type f -exec chmod 640 {} \\;\nsudo chown -R nginx:nginx \/var\/www\/backdrop\/files\nsudo chmod -R 770 \/var\/www\/backdrop\/files\nsudo chmod 660 \/var\/www\/backdrop\/settings.php<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apply SELinux labels that distinguish read-only application code from the writable upload directory. This leaves SELinux enforcing while allowing PHP-FPM to manage uploaded assets.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo semanage fcontext -a -t httpd_sys_content_t '\/var\/www\/backdrop(\/.*)?'\nsudo semanage fcontext -a -t httpd_sys_rw_content_t '\/var\/www\/backdrop\/files(\/.*)?'\nsudo restorecon -Rv \/var\/www\/backdrop<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Configure Nginx for Backdrop<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Backdrop uses a front controller for clean URLs, so Nginx must pass unknown paths to <code>index.php<\/code> while refusing direct access to the site settings and executable files in uploads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>\/etc\/nginx\/conf.d\/backdrop.conf<\/code> with this content:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server {\n    listen 80;\n    listen [::]:80;\n    server_name YOUR_DOMAIN;\n\n    root \/var\/www\/backdrop;\n    index index.php;\n    client_max_body_size 32m;\n\n    location \/ {\n        try_files $uri @backdrop;\n    }\n\n    location @backdrop {\n        rewrite ^\/(.*)$ \/index.php?q=$1 last;\n    }\n\n    location ^~ \/files\/styles\/ {\n        try_files $uri @backdrop;\n    }\n\n    location = \/settings.php {\n        deny all;\n    }\n\n    location ~ ^\/files\/.*\\.php$ {\n        deny all;\n    }\n\n    location ~ \\.php$ {\n        try_files $uri =404;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_param HTTP_PROXY \"\";\n        fastcgi_pass unix:\/run\/php-fpm\/www.sock;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_DOMAIN<\/code> with the DNS name that points to this VPS. Test the syntax before reloading Nginx.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nginx -t\nsudo systemctl reload nginx<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>syntax is ok\ntest is successful<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Open the firewall and enable HTTPS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Keep SSH available for administration, allow HTTP for the initial certificate challenge, and allow HTTPS for the finished site.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo firewall-cmd --permanent --zone=public --add-service=ssh\nsudo firewall-cmd --permanent --zone=public --add-service=http\nsudo firewall-cmd --permanent --zone=public --add-service=https\nsudo firewall-cmd --reload\nsudo firewall-cmd --list-services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The service list should include <code>ssh<\/code>, <code>http<\/code>, and <code>https<\/code>. Install Certbot and let it update the Nginx server block with a redirect from HTTP to HTTPS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y certbot python3-certbot-nginx\nsudo certbot --nginx --redirect --agree-tos --no-eff-email -m YOUR_EMAIL -d YOUR_DOMAIN<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_EMAIL<\/code> with an address that can receive renewal notices. Certbot must resolve <code>YOUR_DOMAIN<\/code> to this VPS and reach port 80 from the public internet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Nginx plugin edits the active server block and installs the redirect, so do not replace the file with a second HTTPS block afterward. Keep the DNS name and certificate name identical to avoid a browser certificate warning.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Run the Backdrop installer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The browser installer writes the site settings and creates the first administrator account without putting that password into shell history.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>https:\/\/YOUR_DOMAIN\/core\/install.php<\/code> in a browser. Select the standard installation profile if prompted, then enter these database values:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Database name: <code>backdrop<\/code><\/li>\n\n\n<li>Database username: <code>backdrop_user<\/code><\/li>\n\n\n<li>Database password: the value used for <code>YOUR_DATABASE_PASSWORD<\/code><\/li>\n\n\n<li>Database host: <code>localhost<\/code><\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Enter a site name and create a unique administrator password. When the installation finishes, visit the home page and sign in over HTTPS. If the installer opens at the domain root instead, follow its normal setup flow; the direct path is useful when the home page has already been customized.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the installer completes, remove write access from the settings file. Backdrop\u2019s <a href=\"https:\/\/docs.backdropcms.org\/documentation\/installation-instructions\" target=\"_blank\" rel=\"noopener\">official installation instructions<\/a> specifically recommend protecting this file after setup.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chown root:nginx \/var\/www\/backdrop\/settings.php\nsudo chmod 640 \/var\/www\/backdrop\/settings.php<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify the Backdrop installation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These checks confirm that the services are running, Nginx accepts the configuration, PHP has the database and image modules, and the public site answers over TLS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl is-active nginx php-fpm mariadb firewalld\nsudo nginx -t\ncurl -fsSI https:\/\/YOUR_DOMAIN\nsudo php -m | grep -E 'curl|gd|mbstring|mysqli|mysqlnd|PDO|xml|zip'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nactive\nactive\nactive\nHTTP\/2 200<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The exact extension order can differ. A successful HTTPS response and a loaded <code>PDO<\/code>\/MySQL driver show that Nginx can hand requests to PHP-FPM; the final proof is a working Backdrop dashboard and an upload test from the administration area. Backdrop\u2019s <a href=\"https:\/\/docs.backdropcms.org\/documentation\/system-requirements\" target=\"_blank\" rel=\"noopener\">system requirements<\/a> describe the supported PHP, MariaDB, and Nginx requirements behind this setup.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Launch Backdrop CMS\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">Rocky Linux VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Launch Backdrop CMS<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Run a secure Backdrop site on Rocky Linux 8 with Nginx and HTTPS.<\/p>\n\t<\/div>\n\t<a class=\"vl-cta-banner__button\" href=\"\/buy-vps\" aria-label=\"View Linux VPS plans\">View Linux VPS plans<\/a>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">502 Bad Gateway<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nginx cannot open the PHP-FPM socket, usually because PHP-FPM is stopped or its pool still uses the <code>apache<\/code> account. Run <code>sudo systemctl status php-fpm<\/code>, check that <code>\/run\/php-fpm\/www.sock<\/code> exists, reapply the pool changes from Step 1, and restart PHP-FPM.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The installer reports missing PHP extensions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The active PHP module stream may not contain the required modules, or PHP-FPM may still have old workers. Run <code>sudo dnf install -y php-mysqlnd php-gd php-mbstring php-xml php-curl php-zip php-intl<\/code>, then run <code>sudo systemctl restart php-fpm<\/code> and refresh the installer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Clean URLs return 404 while the front page works<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Backdrop named location is absent, the request is reaching a different Nginx server block, or the configuration was not reloaded. Check <code>sudo nginx -T | grep -A6 -B3 'server_name YOUR_DOMAIN'<\/code>, confirm the <code>@backdrop<\/code> rewrite, then run <code>sudo nginx -t &amp;&amp; sudo systemctl reload nginx<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Certbot cannot validate the domain<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The DNS record may point to another address, or port 80 may be blocked by the VPS control panel or an upstream firewall. Run <code>dig +short YOUR_DOMAIN<\/code>, compare the result with the VPS public IP, and allow HTTP before retrying Certbot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardening and maintenance<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Apply updates with <code>sudo dnf update -y<\/code>, and back up both the MariaDB database and <code>\/var\/www\/backdrop\/files<\/code> before changing the CMS.<\/li>\n\n\n<li>Keep SELinux enforcing and do not make the entire webroot writable. Only <code>files<\/code> needs write access for normal uploads.<\/li>\n\n\n<li>Test renewal with <code>sudo certbot renew --dry-run<\/code> after the certificate is active.<\/li>\n\n\n<li>If Backdrop\u2019s update manager needs outbound HTTPS and SELinux logs a denial, inspect that specific denial before enabling the narrowly relevant web-server network boolean.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Can I install Backdrop without a domain?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can test the application over the VPS IP address, but a domain is the practical choice for a production site. This guide uses Let\u2019s Encrypt, which requires a publicly resolvable domain for its HTTP validation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does this guide use PHP 8.3 on Rocky Linux 8?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rocky\u2019s base PHP streams may not provide the newest practical PHP branch for a fresh CMS deployment. The Remi 8.3 stream supplies a maintained PHP version while keeping the standard Rocky PHP-FPM service and socket arrangement.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does Backdrop need MariaDB, or can it use MySQL?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Backdrop supports MySQL-compatible databases, including MariaDB. The dedicated database and local-only user in this guide keep the site\u2019s privileges separate from other applications on the VPS.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Which directory must be included in backups?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Back up the MariaDB database and <code>\/var\/www\/backdrop\/files<\/code>, which contains uploaded media and generated assets. The application code and <code>settings.php<\/code> should also be retained, but never expose the settings file through the web server.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Can I install Backdrop without a domain?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"You can test the application over the VPS IP address, but a domain is the practical choice for a production site. This guide uses Let\u2019s Encrypt, which requires a publicly resolvable domain for its HTTP validation.\"}}, {\"@type\": \"Question\", \"name\": \"Why does this guide use PHP 8.3 on Rocky Linux 8?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Rocky\u2019s base PHP streams may not provide the newest practical PHP branch for a fresh CMS deployment. The Remi 8.3 stream supplies a maintained PHP version while keeping the standard Rocky PHP-FPM service and socket arrangement.\"}}, {\"@type\": \"Question\", \"name\": \"Does Backdrop need MariaDB, or can it use MySQL?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Backdrop supports MySQL-compatible databases, including MariaDB. The dedicated database and local-only user in this guide keep the site\u2019s privileges separate from other applications on the VPS.\"}}, {\"@type\": \"Question\", \"name\": \"Which directory must be included in backups?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Back up the MariaDB database and `\/var\/www\/backdrop\/files`, which contains uploaded media and generated assets. The application code and `settings.php` should also be retained, but never expose the settings file through the web server.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Backdrop CMS with LEMP Stack on Rocky Linux 8 using Nginx, MariaDB, PHP-FPM, SELinux, firewalld, and HTTPS on a fresh VPS.","protected":false},"author":1,"featured_media":1020,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"csco_singular_sidebar":"","csco_page_header_type":"","csco_page_load_nextpost":"","footnotes":""},"categories":[5,9],"tags":[123,124,125,105],"class_list":["post-1019","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-backdrop-cms","tag-lemp","tag-nginx","tag-rocky-linux-8","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1019","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/comments?post=1019"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1019\/revisions"}],"predecessor-version":[{"id":3912,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1019\/revisions\/3912"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1020"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1019"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1019"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1019"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}