{"id":1017,"date":"2026-11-06T09:00:00","date_gmt":"2026-11-06T09:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1017"},"modified":"2026-11-06T09:00:00","modified_gmt":"2026-11-06T09:00:00","slug":"install-backdrop-cms-lemp-almalinux-8","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/06\/install-backdrop-cms-lemp-almalinux-8\/","title":{"rendered":"How to Install Backdrop CMS with LEMP on AlmaLinux 8"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Backdrop CMS with LEMP Stack on AlmaLinux 8 by connecting Nginx, MariaDB, and PHP-FPM, then finish the web installer over HTTPS. The complete setup takes about 10\u201315 minutes on a fresh VPS with a domain already pointed at its IP address.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>AlmaLinux 8.x with at least 1 GB RAM, 10 GB 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 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. A second DNS record is not required for this single-site setup.<\/li>\n\n\n<li>A strong database password and a separate strong Backdrop administrator password. Use different passwords for both accounts.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> with AlmaLinux 8 is a suitable starting point for this workload.<\/li>\n\n\n<li>The LEMP components will be installed in this guide; do not install Apache on the same ports.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Update AlmaLinux and install the LEMP packages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Updating first applies the latest AlmaLinux 8 fixes, while the Remi PHP stream supplies a modern PHP version for a long-lived Backdrop installation.<\/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. The exact patch numbers can change as the repositories publish updates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PHP-FPM normally starts workers as <code>apache<\/code> on AlmaLinux. Nginx needs access to its Unix socket, so change the default pool to the Nginx account and restart PHP-FPM.<\/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<p class=\"wp-block-paragraph\">Expected output is <code>active<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Create the MariaDB database<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Backdrop stores pages, users, configuration, and content in MariaDB, so give it a dedicated database and account with access limited to that database.<\/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 final command should return a row named <code>backdrop<\/code>; you will enter the same database name, username, and password in the Backdrop installer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Download Backdrop CMS and set the webroot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The release archive is downloaded from Backdrop\u2019s GitHub releases rather than from a mutable development branch, which keeps this installation reproducible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The commands below use Backdrop 1.35.1, the current stable release at the time of writing. The archive can contain a top-level directory, so <code>find<\/code> locates the real release root before copying its files.<\/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\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Set read access for Nginx, but leave only the upload directory writable by the web process. Backdrop needs that directory for images, modules, and other managed files.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/var\/www\/backdrop\/files\nsudo 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\">On an SELinux-enabled server, label the code as read-only web content and the <code>files<\/code> directory as writable web content. This keeps SELinux enforcing while allowing Backdrop to manage uploads.<\/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 front-controller routing for clean URLs, so Nginx must send unknown paths to <code>index.php<\/code> instead of returning a plain 404.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>\/etc\/nginx\/conf.d\/backdrop.conf<\/code> with this server block:<\/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 $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 ~ \\.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\n    location = \/settings.php {\n        deny all;\n    }\n\n    location ~ ^\/files\/.*\\.php$ {\n        deny all;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_DOMAIN<\/code> with the hostname in your DNS record. The explicit <code>settings.php<\/code> rule blocks direct access to database credentials, and the upload rule prevents an uploaded PHP file from being executed.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nginx -t\nsudo systemctl reload nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output includes <code>syntax is ok<\/code> and <code>test is successful<\/code>.<\/p>\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\">HTTP is needed for the initial certificate challenge, while HTTPS protects the Backdrop login and administration pages.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo firewall-cmd --permanent --zone=public --add-service=http\nsudo firewall-cmd --permanent --zone=public --add-service=https\nsudo firewall-cmd --reload\nsudo 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 for certificate renewal notices. Certbot must be able to resolve <code>YOUR_DOMAIN<\/code> to this server and reach port 80 from the internet. On success, it updates the Nginx server block and redirects HTTP requests to HTTPS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Finish the Backdrop installation in your browser<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The browser installer creates the site configuration and administrator account, so no CMS password needs to appear in your shell history.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>https:\/\/YOUR_DOMAIN<\/code> in a browser. If the installer does not appear, open <code>https:\/\/YOUR_DOMAIN\/core\/install.php<\/code> directly. Choose the standard installation profile, enter <code>backdrop<\/code> as the database name, <code>backdrop_user<\/code> as the database user, <code>localhost<\/code> as the host, and the database password from Step 2.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the site information screen, enter the site name and create a new administrator account with a long, unique password. When setup completes, sign in through the HTTPS address and confirm that the dashboard loads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the installer finishes, make the settings file read-only to the web process while retaining group read access for Nginx:<\/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<p class=\"wp-block-paragraph\">For Nginx routing details, compare this server block with Backdrop\u2019s <a href=\"https:\/\/docs.backdropcms.org\/documentation\/system-requirements\" target=\"_blank\" rel=\"noopener\">official system requirements<\/a>. The <a href=\"https:\/\/docs.backdropcms.org\/documentation\/installation-instructions\" target=\"_blank\" rel=\"noopener\">official installation instructions<\/a> also explain the browser steps and the writable <code>files<\/code> directory.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify the Backdrop installation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run these checks from SSH, then open the site in a private browser window to confirm that the certificate and login session work together.<\/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<p class=\"wp-block-paragraph\">Expected results are <code>active<\/code> for each service, a successful Nginx test, an HTTP response such as <code>HTTP\/2 200<\/code>, and the PHP extensions listed on separate lines. A successful page request proves Nginx can reach PHP-FPM and Backdrop can serve its front controller.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Deploy Backdrop CMS\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">AlmaLinux VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Deploy Backdrop CMS<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Launch a secure Backdrop site on an AlmaLinux VPS.<\/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 reach the PHP-FPM socket, usually because PHP-FPM is stopped or the pool still uses the wrong socket owner. Check <code>sudo systemctl status php-fpm<\/code>, confirm that <code>\/run\/php-fpm\/www.sock<\/code> exists, and restart both services after reapplying the pool changes from Step 1.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The installer shows missing PDO MySQL or PHP extensions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The PHP modules were not installed in the active stream, or PHP-FPM has not been restarted. 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, but the home page loads<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Backdrop rewrite location is missing, the domain is using a different server block, or Nginx was not reloaded. Run <code>sudo nginx -T | grep -A5 -B3 'server_name YOUR_DOMAIN'<\/code>, verify the <code>@backdrop<\/code> block, and reload Nginx after correcting it.<\/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 A record may still point elsewhere, or an upstream firewall may block port 80. Confirm <code>dig +short YOUR_DOMAIN<\/code> returns the VPS IP, allow HTTP in the VPS control panel and firewalld, then run Certbot again after DNS propagation.<\/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>Keep AlmaLinux, Nginx, PHP, and MariaDB updated with <code>sudo dnf update -y<\/code>; take a database and <code>\/var\/www\/backdrop\/files<\/code> backup before major CMS updates.<\/li>\n\n\n<li>Leave SELinux enforcing and do not make the whole document root writable. Only the <code>files<\/code> directory needs web-server write access.<\/li>\n\n\n<li>Test renewal before you rely on the certificate: <code>sudo certbot renew --dry-run<\/code>.<\/li>\n\n\n<li>Backdrop\u2019s update manager may need outbound HTTPS access. If SELinux logs deny that specific operation, review the denial and then enable only the documented <code>httpd_can_network_connect<\/code> boolean if required.<\/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 CMS without a domain?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can test the site over the VPS IP address, but a real domain is strongly preferred for HTTPS and reliable administrator access. Let\u2019s Encrypt certificates are issued for domain names, not bare IP addresses in this workflow.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why use PHP 8.3 on AlmaLinux 8?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AlmaLinux 8\u2019s default application streams can be older than the PHP release recommended for current Backdrop deployments. The Remi 8.3 stream provides a maintained PHP version while preserving the AlmaLinux 8 package layout and PHP-FPM service name.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does Backdrop require MariaDB instead of MySQL?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Backdrop supports MySQL-compatible databases, including MariaDB, and the <code>php-mysqlnd<\/code> extension provides the PHP database driver used here. A separate database and user per site keeps permissions easier to audit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where are uploaded images stored?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Backdrop stores managed uploads below <code>\/var\/www\/backdrop\/files<\/code>. That directory is intentionally writable by Nginx and should be included in backups, while the rest of the code remains read-only to the web process.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Can I install Backdrop CMS without a domain?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"You can test the site over the VPS IP address, but a real domain is strongly preferred for HTTPS and reliable administrator access. Let\u2019s Encrypt certificates are issued for domain names, not bare IP addresses in this workflow.\"}}, {\"@type\": \"Question\", \"name\": \"Why use PHP 8.3 on AlmaLinux 8?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"AlmaLinux 8\u2019s default application streams can be older than the PHP release recommended for current Backdrop deployments. The Remi 8.3 stream provides a maintained PHP version while preserving the AlmaLinux 8 package layout and PHP-FPM service name.\"}}, {\"@type\": \"Question\", \"name\": \"Does Backdrop require MariaDB instead of MySQL?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Backdrop supports MySQL-compatible databases, including MariaDB, and the `php-mysqlnd` extension provides the PHP database driver used here. A separate database and user per site keeps permissions easier to audit.\"}}, {\"@type\": \"Question\", \"name\": \"Where are uploaded images stored?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Backdrop stores managed uploads below `\/var\/www\/backdrop\/files`. That directory is intentionally writable by Nginx and should be included in backups, while the rest of the code remains read-only to the web process.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Backdrop CMS with LEMP Stack on AlmaLinux 8 using Nginx, MariaDB, PHP-FPM, SELinux, firewalld, and HTTPS in minutes on a fresh VPS today.","protected":false},"author":1,"featured_media":1018,"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":[94,123,124,125],"class_list":["post-1017","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-almalinux-8","tag-backdrop-cms","tag-lemp","tag-nginx","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1017","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=1017"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1017\/revisions"}],"predecessor-version":[{"id":3911,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1017\/revisions\/3911"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1018"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1017"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1017"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1017"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}