SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

Install Flarum on a VPS: PHP forum setup

Install Flarum on a VPS with PHP, Composer and MariaDB, serve only /public over TLS, and fix the mail and extension upgrade traps that break new forums.

Install Flarum on a VPS with PHP and MariaDB

Install Flarum on a VPS and you get a forum that runs on PHP and MariaDB, with no Docker, no Ruby and no separate job runner. That is the reason to pick it. Installing Discourse on a VPS means a Docker stack holding Ruby, PostgreSQL, Redis and a background worker, and 2 GB of RAM is the practical floor before you add backups or a mail service. Discourse earns that footprint on a busy community with thousands of posts a day. On a forum of a few hundred people it is machinery you pay for every month and never use. Flarum fits inside a PHP stack you may already be running, so a 1 GB VPS with nginx and MariaDB can host it next to whatever else is there. If the choice is still open, the wider comparison of self-hosted forum software covers the rest of the field.

This guide pins the install to Flarum 1.x, currently the 1.8 series. As of August 2026 Flarum 2.0 is still a release candidate, so 1.8 is the version to run for a forum that other people depend on. The extensions matter more here than the core version does, and most extension authors have not moved to 2.0 yet.

What Flarum 1.8 needs on the server

Flarum's documented requirements are PHP 7.3 or newer, MySQL 5.6+ or MariaDB 10.0.5+, and either nginx or Apache with mod_rewrite. Ubuntu 24.04 ships PHP 8.3 and MariaDB 10.11, so the distribution packages satisfy both with no third-party repository. The PHP extensions Flarum requires are curl, dom, fileinfo, gd, json, mbstring, openssl, pdo_mysql, tokenizer and zip. You also need shell access, because Composer is not optional here: every Flarum extension is a Composer package, and there is no way to add one without running Composer.

Flarum is an ordinary PHP application. If you already run a LAMP stack on Ubuntu 24.04, most of the next two sections is installed and you can skim to the database.

Install PHP and the extensions Flarum requires

sudo apt update
sudo apt install -y nginx mariadb-server composer
sudo apt install -y php8.3-fpm php8.3-cli php8.3-curl php8.3-gd php8.3-mbstring php8.3-mysql php8.3-xml php8.3-zip
php -m | grep -E 'curl|dom|gd|mbstring|pdo_mysql|zip'
composer --version

The package names do not match Flarum's list one for one. The dom extension lives inside php8.3-xml, and fileinfo, json, openssl and tokenizer are compiled into PHP 8.3 on Ubuntu, so there is no package to install for those. The grep should print six lines and composer --version should report a 2.x release. A missing extension is worth fixing now, because Composer refuses to install Flarum without it and the error names the extension for you:

  Problem 1
    - flarum/core[v1.8.0, ..., v1.8.17] require ext-gd * -> it is missing from your system.
      Install or enable PHP's gd extension.

Check that the command line PHP and the FastCGI process manager (PHP-FPM) are the same version. Composer runs under the command line binary while your forum runs under FPM, so php -v and sudo php-fpm8.3 -v must agree. They drift apart on a machine that has collected several PHP versions from a third-party repository, and then an extension you installed for one version is invisible to the other.

Create the database and a dedicated database user

sudo mariadb-secure-installation
sudo mariadb
CREATE DATABASE flarum CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'flarum'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON flarum.* TO 'flarum'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Give Flarum its own database user instead of root. The grant covers flarum.* and nothing else, so a SQL injection bug in an extension you did not write cannot read your other databases. The character set matters too: utf8mb4 stores emoji and non-Latin scripts correctly, while MySQL's older utf8 is a three-byte encoding that truncates a post at the first four-byte character. Confirm the user works before you go further.

mariadb -u flarum -p flarum -e 'SELECT DATABASE();'

That should print flarum. ERROR 1045 (28000): Access denied for user 'flarum'@'localhost' means the password does not match, or the user was created for a different host than localhost.

Install Flarum on a VPS with Composer, pinned to 1.8

First create a system user that owns the code. Running Composer as root leaves root-owned files in vendor/ and storage/, so PHP cannot write its own cache later and the forum answers every request with a 500 error.

sudo useradd --system --home-dir /srv/flarum --shell /bin/bash flarum
sudo install -d -o flarum -g flarum -m 755 /srv/flarum
sudo -iu flarum

That last command drops you into a shell as the flarum user, in /srv/flarum. Every command from here to the end of the guide runs there unless it starts with sudo.

composer create-project flarum/flarum:^1.8.0 .

The ^1.8.0 constraint is the version pin. It accepts 1.8.x patch releases and refuses 2.0, which is what you want while 2.0 is a release candidate. Composer downloads the skeleton, resolves flarum/core and its dependencies, and leaves you with public/, storage/, vendor/, composer.json and a flarum command line script.

On a small VPS this step is where things break, and two different failures look alike. PHP Fatal error: Allowed memory size of 134217728 bytes exhausted is PHP's own memory_limit, and putting COMPOSER_MEMORY_LIMIT=-1 in front of the command removes that ceiling for one run. A bare Killed on its own line with no PHP error is the kernel out-of-memory killer, which you can confirm with dmesg | tail. That case means the machine really has run out of RAM, so add swap: raising the PHP limit makes it worse, because PHP then asks for even more memory before the kernel steps in.

Why only /public should be web facing

The project root holds config.php with your database password in plain text, vendor/ with every dependency, and storage/ with logs and cached sessions. None of that belongs on the public internet. Flarum keeps everything web accessible in one subdirectory, public/, which contains index.php, an assets/ folder and little else. Point the web server root at /srv/flarum/public and the rest of the tree is unreachable over HTTP by construction, not by a rule you have to remember.

Point the root at /srv/flarum instead and the damage is real. Requesting /config.php returns nothing useful, because PHP executes the file and it only returns an array. But /storage/logs/flarum.log hands a stranger your stack traces and database errors, and /composer.lock tells them the exact version of every package you run, which is a list of known vulnerabilities to try.

Give PHP-FPM its own pool

Flarum needs write access to three paths: the project root, so the installer can create config.php; storage/, for logs and cache; and assets/, for uploaded avatars and logos. The files belong to the flarum user, so the clean answer is a PHP-FPM pool that runs as flarum. nginx keeps running as www-data and only ever reads.

Write /etc/php/8.3/fpm/pool.d/flarum.conf:

[flarum]
user = flarum
group = flarum
listen = /run/php/php8.3-fpm-flarum.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 16M
php_admin_value[post_max_size] = 17M
sudo systemctl restart php8.3-fpm
ls -l /run/php/php8.3-fpm-flarum.sock

The socket should be listed as srw-rw---- 1 www-data www-data. The master process runs as root and creates the socket, which is why it can hand ownership to www-data while the workers run as flarum. If the socket is not there, sudo journalctl -u php8.3-fpm -n 30 prints the line FPM rejected. pm = ondemand starts worker processes only when a request arrives, so a quiet forum costs almost no memory between visitors.

The nginx server block

Flarum ships an nginx snippet in the project root called .nginx.conf. It holds the rewrite rule, the cache headers and the compression settings. Include it rather than copying it, so a Flarum upgrade that changes the snippet reaches your server without you editing anything.

server {
    listen 80;
    listen [::]:80;
    server_name forum.example.com;

    root /srv/flarum/public;
    index index.php;
    client_max_body_size 16M;

    include /srv/flarum/.nginx.conf;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm-flarum.sock;
    }
}

Save that as /etc/nginx/sites-available/flarum, then enable it.

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

nginx -t should answer syntax is ok and test is successful. The include line is the one people leave out, and leaving it out gives one very specific symptom: the front page loads perfectly and every discussion link returns a 404 from nginx. Flarum routes a URL like /d/1-hello-world inside PHP. That path is not a file on disk, so without the snippet's try_files $uri $uri/ /index.php?$query_string rule nginx looks for a directory named d, does not find one, and answers 404 before PHP is ever asked. Apache gets the same routing from the .htaccess file already sitting in public/, which only takes effect if the matching <Directory> block sets AllowOverride All.

Add TLS before you run the installer

Do the certificate now, not afterwards. Flarum writes its own base URL into config.php during installation, and it takes that URL from the address in your browser. Install over http:// and every asset URL Flarum generates afterwards starts with http://, so once you add TLS (transport layer security) the browser blocks those requests as mixed content and the forum loads as unstyled text with no JavaScript. Work through the certbot setup for Let's Encrypt on nginx first, confirm that https://forum.example.com serves a page, then come back.

If you already installed over plain HTTP, the repair is one line. Edit /srv/flarum/config.php so the url key reads 'url' => 'https://forum.example.com',, then run php flarum cache:clear as the flarum user.

Run the installer and lock down config.php

Open https://forum.example.com in a browser. Flarum's web installer asks for the database name, the database user and password you created, the forum title and your admin account. It writes config.php and creates the tables. When it finishes, tighten the file that now holds your database password.

sudo chmod 640 /srv/flarum/config.php

The file stays readable and writable by flarum, which is the user FPM runs as, and unreadable by every other account on the box. Then check the install from the shell.

php flarum info

That prints the core version, the PHP version, the enabled extensions, the mail driver and the paths in use. It is the first thing to run when something misbehaves, and the first thing anyone helping you will ask for.

Why signup and password reset email fails silently

This section decides whether your forum survives its first week. The first two emails a new forum ever sends are the signup confirmation and the password reset, and both go out at the exact moment a stranger is deciding whether your community is real. Both fail quietly. The person who never received the mail cannot tell you, and nothing on your admin dashboard turns red.

Flarum has four mail drivers, set under Administration then Email: smtp, mail, mailgun and log. Choose smtp and point it at a mail service that will actually deliver. The mail driver hands the message to a local sendmail binary, which does not exist on a fresh Ubuntu VPS, so the message goes nowhere and no error is raised. The log driver writes the message body into storage/logs/ and sends nothing at all, because it exists for development.

Save the settings, then press the Send Test Mail button on that same page. That button is the only feedback loop you get, so use it before you invite anybody. If the message does not arrive, read the log.

tail -n 50 /srv/flarum/storage/logs/flarum.log

A refused connection, a rejected login and a TLS negotiation failure all land there with the host and port that were tried. A line reading Connection could not be established with host smtp.example.com means the port is blocked or wrong, and the usual cause is that the provider blocks outbound port 25 by default. Port 587 with the encryption field set to tls is the combination that works with almost every mail service.

When mail is broken, a visitor who signs up sees Flarum's generic failure banner, Oops! Something went wrong. Please reload the page and try again., because the request throws while sending the confirmation. Either way that person cannot get in, and they will not write to you about it.

Getting the mail accepted is a separate problem from getting it sent. Mail from a VPS address with no SPF or DKIM records published for your domain lands in the spam folder, or is dropped with no bounce message at all, so your log shows a successful send and the reader still sees nothing. Sending mail reliably from self-hosted apps covers which DNS records to publish and which relay services are worth using.

Extensions are Composer packages, so upgrades are a command

In Flarum an extension is a Composer package. There is no upload button and no zip file to drop in a folder. That is a deliberate design decision: versions resolve against each other, the exact set is recorded in composer.lock, and an extension that depends on a PHP library gets that library installed properly. The cost is that adding one means three commands as the flarum user.

composer require 'fof/upload:*'
php flarum migrate
php flarum cache:clear

Then enable it under Administration then Extensions. Each command does a distinct job. composer require puts the code on disk. php flarum migrate runs any database migrations the extension carries, which is how it creates its own tables. php flarum cache:clear rebuilds the compiled JavaScript and CSS bundles that the browser loads. Skipping the last one produces the classic confusion: the extension is listed, the toggle is on, and nothing at all changes in the browser however many times you reload.

Removing an extension runs the other way. Disable it in the admin panel first, then remove the package. Flarum leaves the extension's tables in place on purpose, so re-enabling it later does not lose data. To drop them as well, roll its migrations back before you remove the code.

php flarum migrate:reset --extension fof-upload
composer remove fof/upload
php flarum cache:clear

The extension id in that command is the package name with the slash replaced by a hyphen. If you would rather click than type, composer require 'flarum/extension-manager:*' adds an official admin page that wraps Composer. It runs the same operations as the web user, needs the same memory and the same write permissions, and fails in the same ways, so the command line stays the reliable path when something goes wrong.

Why composer update refuses to upgrade Flarum

Upgrading Flarum and every extension is one Composer command followed by two Flarum commands. Back up the database first.

composer update --prefer-dist --no-plugins --no-dev -a --with-all-dependencies
php flarum migrate
php flarum cache:clear

Read the output rather than assuming it worked, because the interesting case is when Composer declines to do anything:

Your requirements could not be resolved to an installable set of packages.

That message means one installed extension declares a flarum/core constraint that excludes the release you are moving to. Composer will not violate the constraint, so it holds the entire install at the old version rather than assemble a combination the extension author has said will not work. Find the package responsible.

composer why-not flarum/core 1.8.17

The output lists every package blocking that version and the constraint each one declares. From there the options are to wait for the author to publish a compatible release, or to remove the extension and go without it. This is the price of the Composer model, and it is worth understanding before you install fifteen extensions on a forum other people rely on. Prefer extensions that have shipped a release recently, and keep the list short enough that you can read it.

What to back up

Back up the database and the project directory. Everything else can be rebuilt.

mariadb-dump -u flarum -p --single-transaction flarum > flarum-$(date +%F).sql
sudo tar czf flarum-files.tgz -C /srv flarum

On older systems mariadb-dump is called mysqldump; on Ubuntu 24.04 both names work. The database holds every post, every user and every setting. The project directory holds config.php, composer.json (which is the real record of which extensions you run) and assets/ with uploaded avatars and logos. You can skip vendor/, because composer install rebuilds it from composer.lock, and storage/ regenerates itself. Copy both files off the server, because a backup living on the machine it protects is not a backup. Restoring means a fresh Flarum of the same version, composer install, the saved files back in place, and the SQL file loaded.

FAQ

Can Flarum run on a 1 GB VPS?

Yes, for a small community. Flarum in normal operation is PHP-FPM answering requests plus MariaDB, and with pm = ondemand the PHP workers only exist while someone is reading. The peak is the install itself: composer create-project resolves the whole dependency graph in memory and is the step most likely to be stopped by the out-of-memory killer. Add a swap file before you install, or run Composer once on a larger machine and copy the tree across.

Your nginx server block is missing include /srv/flarum/.nginx.conf;. Flarum handles URLs such as /d/1-hello-world inside PHP, and that path is not a file on disk. Without the snippet's try_files $uri $uri/ /index.php?$query_string rule, nginx searches for a directory called d, fails, and returns 404 itself without ever calling PHP. On Apache the same routing comes from public/.htaccess, which is ignored unless the <Directory> block sets AllowOverride All.

Why did my forum lose all its styling after I enabled HTTPS?

Flarum stores its base URL in config.php, captured from whatever address you used when running the installer. If you installed over plain HTTP, Flarum keeps generating http:// asset URLs, and the browser blocks them as mixed content on an HTTPS page, which leaves unstyled text. Edit the url key in /srv/flarum/config.php to the https:// address and run php flarum cache:clear as the user that owns the files.

Why does nobody receive the signup confirmation email?

Check the mail driver first. The mail driver needs a local sendmail binary that a fresh Ubuntu VPS does not have, and the log driver writes to storage/logs/ and sends nothing. Switch to smtp, use port 587 with encryption set to tls because most providers block outbound port 25, and press Send Test Mail. If the log shows a successful send and the mail still never arrives, the problem is deliverability rather than configuration, so publish SPF and DKIM records for your sending domain.

Should I install Flarum 1.8 or 2.0?

Install 1.8. As of August 2026 Flarum 2.0 is a release candidate, and more importantly many extensions still declare a flarum/core constraint that stops at 1.x. Installing 2.0 today means running a forum whose extensions cannot be updated together, which is the exact failure composer why-not exists to diagnose. Pin with flarum/flarum:^1.8.0 and revisit once the extensions you depend on have published 2.0 releases.