A forum stack is the box and the pipes, not a shopping list of host brands. XenForo 2.3 wants current PHP, a MySQL or MariaDB that InnoDB can live in, a cache that is not “disk plus hope,” HTTPS, mail that actually leaves, and a backup you have restored once. Discourse official production is a Docker stack (or an official hosted plan) with the same honesty about TLS, SMTP, and offsite copies. This article is that recommended runtime. It is a recipe. It is not a ranking of companies.
It is not the admin toolkit — that piece already named the desk (ACP, uptime, vault, calendar). It is not the Core Web Vitals guide (Gzip, debug off, icon sprite, the first 60 minutes). It is not scaling architecture (PHP-FPM math, U×N locks, when search is the next lever). It is not the Discourse hosting cookbook (RAM, rebuilds, hosted Free/Pro/Business). Read those when the rung they own is the one that hurts. Stay here for what to put on the machine in 2026, and what not to invent.
We will not pick a VPS vendor. We will not invent a “best host for XenForo” table. If a paragraph starts to sound like an affiliate page, it does not belong.
Two products, two shapes
Do not tune Unicorn like PHP-FPM. Do not expect a cPanel “one-click Discourse” to be the official install.
| Shape | Application | Database you actually run | Cache you actually run | Who owns the pager |
|---|---|---|---|---|
| XenForo self-host | PHP-FPM behind nginx, Apache, or LiteSpeed | MySQL 8 or MariaDB (5.7 floor) | Redis (official Symfony provider; community Xon) | You |
| XenForo Cloud | Their PHP | Theirs | Theirs | They do, inside the plan’s caps |
| Discourse official Docker | Unicorn in a container | PostgreSQL in the container | Redis in the container | You |
| Discourse official hosted | Theirs | Theirs | Theirs | They do, inside the plan |
XenForo Cloud and Discourse hosted are valid stacks. They remove this article’s SSH sections. They do not remove HTTPS, a mail story, or a backup you can take off the vendor. Self-host is the rest of the page.
A third shape — “Discourse on Plesk / Bitnami / a random compose file” — is how people spend a week on TLS and then blame the software. Meta support expects the standard install. Do not start there.
PHP 8.x for XenForo 2.3
XenForo 2.3 raised the floor. Current 2.3 release notes print:
- Minimum: PHP 7.2 or newer
- Recommended: PHP 8.3 (developer docs also name 8.4 as the recommended line)
The 2.3 era answer for a new or newly rebuilt box is PHP 8.3 or 8.4, OPcache on, php -v matching the FPM pool the vhost actually uses. PHP 7.4 “because it works” is how you inherit last year’s advisory and a slower opcode story. The upgrade article is the decision to move the application. This article is the runtime you land on.
Required extensions official materials name: mysqli, gd with JPEG, pcre, curl, spl, simplexml, dom, json, iconv, ctype. Add gmp / other extras only when an add-on or PWA checklist actually asks. The purchase-site requirements zip is the test you run before you upload files.
config.php hygiene that is part of the stack, not a tweak hobby:
$config['debug'] = false;
$config['development']['fullJs'] = false;
$config['enableGzip'] = true;
Official: debug on production “severely hurts performance” and exposes internal SQL. fullJs serves the unbundled development JavaScript. Gzip compresses generated HTML/CSS unless the web server or CDN already does that job — do not double-wrap blindly, do not disable it “to see raw HTML” on a live board.
Cookie path and domain must cover the XenForo root. A wrong $config['cookie']['path'] makes every request look like a guest. You then enable a guest cache and serve a cached footer to a moderator. That is a configuration mistake, and it is also a stack bug.
Discourse does not want your PHP. If you are on Discourse, skip this heading and run Ubuntu LTS plus the official installer. Mixing “we already have PHP 8.3 for WordPress, let’s add Discourse” on ports 80/443 is how both sites lose.
MariaDB or MySQL (XenForo)
Official 2.3 database line: MySQL 5.7 minimum, MySQL 8.0 or MariaDB recommended, Percona listed as compatible. Required PHP extension: mysqli. Either MySQL 8 or a current MariaDB 10.x/11.x with InnoDB is a 2026 default. We will not pick a winner between those two names. Community threads call it personal preference. InnoDB for the tables that take writes is not a preference.
Stack habits that are community operations (the official cache manual does not publish my.cnf):
- InnoDB buffer pool sized for the working set — or you will blame “XenForo” for disk.
- Query cache off. A busy forum is writes.
innodb_flush_log_at_trx_commit = 1for durability. Lower it only during a known import window, then put it back.- Do not invent read replicas as a XenForo feature. Official manuals do not ship a primary/replica adapter for forum reads.
Stock leftovers that can stay MyISAM on a small board (xf_search_index and session tables appear in community engine notes) are a search conversation, not a reason to run the whole site on MyISAM.
Put the database on fast local disk. Remote MySQL on a cheap network is how every page waits. Official importer docs make the same point in another context.
Discourse: you do not choose MariaDB. PostgreSQL ships in the container. Do not “improve” it onto a shared MySQL host.
Redis: official cache, plus Xon
XenForo 2.3 cache is Symfony Cache. Official public cache manual still names config.php providers: ApcCache, Filesystem, Memcached, Redis, WinCache, XCache. Redis is first-class. It is not a third-party idea.
A minimal official-shaped block (manual defaults: port 6379, optional password, database 0, persistent false):
$config['cache']['enabled'] = true;
$config['cache']['provider'] = 'Redis';
$config['cache']['config'] = [
'host' => '127.0.0.1',
'password' => 'password',
];
What that holds: data registry, compiled templates, CSS cache, permission combinations. Without it, 2.3 still runs and reads xf_data_registry from MySQL on every page.
Guest page cache is a different context. Official 2.1+ rule: $config['pageCache']['enabled'] = true only works if you also define $config['cache']['context']['page'] with its own provider / instance. Huge guest HTML must not evict sessions and the registry. Cached responses send X-XF-Cache-Status: HIT. Page cache is off by default because it eats RAM.
$config['pageCache']['enabled'] = true;
$config['cache']['context']['page'] = [
'provider' => 'Redis',
'config' => [
'host' => '127.0.0.1',
'database' => 1,
],
];
Treat that as a shape. Separate Redis databases are weak isolation if they still share one maxmemory. Separate instances, or a memory budget you can explain, are the real split. Sessions in cache ($config['cache']['sessions'] = true) plus allkeys-lru on a tiny box is how you log people out to store a homepage.
Redis Cache by Xon (community)
Official 2.3 docs in the notebook do not walk Redis operations. Community sources do, via Redis Cache by Xon (also seen as SV Redis Cache) and the phpredis extension. Claimed wins in those write-ups: compiled CSS in RAM instead of locking xf_css_cache; the data registry stops hitting MySQL every request. HA notes from the same sources: Redis Sentinel (primary/replica) is supported; multi-master clustering is not. If Redis flaps, they tell you to disable persistence so a BGSAVE does not block PHP workers.
Install path that stays honest:
- Install Redis locally. Install
phpredis. Confirmphp -m. - Configure the official provider and/or Xon’s add-on as that add-on’s current README specifies. Restart PHP-FPM.
- Hit the homepage as a guest twice. Change a style property. Confirm the front-end updates after a cache rebuild.
- Do not enable Redis for the first time during a permission rebuild. One variable.
If you already offload guest HTML at LiteSpeed or nginx, you may leave XenForo page cache off. One guest-cache owner. Two layers both believing they own / is a Saturday of purges.
Discourse: Redis is already in the container. Do not add Xon’s XenForo add-on to a Discourse box. Different animal.
Elasticsearch only if XFES
Stock XenForo search is MySQL full-text. That is fine until it is not.
XenForo Enhanced Search (XFES) is an official add-on. It replaces MySQL full-text with Elasticsearch or OpenSearch. After install: Setup → Enhanced Search, connect, rebuild the search index. That rebuild is a job. Window it.
Version floors, combined so we do not invent:
- Older official XFES language named Elasticsearch 2.0+.
- Current XenForo 2.3 release notes print Elasticsearch 7.2 as the Enhanced Search requirement; our speed/scaling articles treat 7.2+ Elasticsearch or OpenSearch as the runtime you actually run.
Typical shared hosting has neither. If the panel cannot give you a process with its own heap, you do not have XFES. Stay on MySQL search. Do not buy the add-on to look prepared.
Go / no-go, copied from the architecture article because it is still the truth:
-
SHOW PROCESSLISTduring slowness names search, not a permission rebuild or a dead Redis - Members have actually complained that search misses or times out
- You have a box or a sized slice for ES/OS, not a leftover GB on the web node
XFES does not replace application cache, page cache, or InnoDB. It replaces search. If the guest homepage is slow, XFES will not help. Third-party “Elasticsearch Essentials” style add-ons require XFES — do not install the satellite first.
Do not put ES on an 8 GB all-in-one “just to try.” Heap plus page cache plus InnoDB is a 3 a.m. OOM.
Discourse search is not XFES. Do not install Elasticsearch next to Docker because this heading exists.
HTTPS is the floor
TLS is not a style option.
- XenForo PWA and push require HTTPS. Official Setup → PWA setup will not enable without it.
- OAuth (Google mail, SSO you add later) expects
https://Board URL. - Discourse official install provisions Let’s Encrypt when DNS and ports 80/443 work.
--skip-connection-testdoes not invent a certificate.
Board URL, canonical host, and the certificate name must match. www vs apex is how people debug “members cannot stay logged in” for a day. The security baseline already owns 2FA, login limits, src/ and internal_data/ not public. This stack article only insists: terminate TLS, redirect HTTP, do not ship mixed content.
nginx (or equivalent) must mark internal_data, src, install/data, install/templates as not public. Official friendly-URL notes. That is a stack control, not an add-on.
Mail: Symfony Mailer under three transports
XenForo 2.3 uses Symfony Mailer, not SwiftMailer. That is the library. It is why a 2.2 add-on that still binds Swift can fail on upgrade. It is not a fourth radio button in the ACP.
What you still pick, under Setup → Options → Email options:
| Transport | When it is the stack default |
|---|---|
| PHP built-in mail | VPS with a working local MTA; official preferred because it offloads send to a dedicated program |
| SMTP | Shared host that blocks mail(), or you already have a transactional provider; official slower (each mail sent by XenForo) |
| Google OAuth | The mailbox is Google and refuses SMTP passwords |
Set Default email address, Bounced email address, Default email sender name the same day. Prove the pipe with a test or a lost-password mail before you mandate email 2FA.
$config['enableMail'] and $config['enableMailQueue'] exist. Turning mail off is a staging tool, not a production personality.
Discourse: SMTP is optional at install (Discourse ID can log people in). No SMTP means no digest, no mailing-list mode, no reply-by-email. A support community that lives in the inbox cannot skip it. Hosted plans have email caps — read the live grid; do not assume infinite mail.
We will not invent a 2026 transactional-vendor ranking. SPF/DKIM on a domain you control matter more than the logo on the SMTP host.
Offsite backups
A backup that lives on the same disk as the origin is a rumour.
XenForo — official restore doctrine:
- Database +
data/+internal_data/ - Restore onto the same XenForo version
- Merge
data/andinternal_data/on restore; never replace those folders with an empty tree internal_data/install-lock.php(empty file is enough) re-enables the upgrade wizard if the installer thinks it is a fresh site
The XF backup article is the loop. This stack only adds: copy the dump and the two directories off the box (another volume, an object bucket, a machine that is not the origin). Nightly that never finishes because data/ is 400 GB is an architecture ticket: snapshot the volume, or exclude with a documented restore story.
Discourse — Meta 14855:
backup_frequencydefault 7 days (0 = off, max 30)backup_time_of_daydefault 3:30 UTCbackup_with_uploadsdefault onmaximum_backupsdefault 5- Local path
/var/discourse/shared/standalone/backups/default - S3:
backup_location= S3, separates3_backup_bucket(not the same folder as uploads)
Download one backup offsite anyway. Restore onto a scratch site once. Provider snapshots are a disk picture; application backups are an application picture. Keep both until you have restored both.
Put the restore drill on the calendar class.
Object storage (uploads are a volume)
Attachments are bytes. At some size they stop being “a folder on the web node.”
XenForo: official public manuals do not describe a built-in “put data/ on S3” checkbox as core. Community add-ons and object-storage patterns exist. If you use one, name it in the runbook and test restore. Conceptual advice that is fair:
- Treat attachment bytes as a volume you can snapshot.
- Do not put
internal_dataon a public bucket. Raw attachments can live outside the web root; they are not a CDN toy. data/is public (avatars, thumbs). A public bucket for that class of object is a conversation you have after you can still restore.- Client-side HTML5 resize (official reason: less server CPU) before you shop for object storage.
- Image proxy (
$config['proxyUrlFormat']) is a second tree of other people’s bytes. Watch it.
Discourse: uploads-on-S3 is an official-ish ops project (Meta’s S3-compatible provider topic). Backups and uploads are not the same bucket and folder — not supported, will not work. DISCOURSE_S3_CDN_URL is not DISCOURSE_CDN_URL. Do not copy a full uploads app.yml because you wanted offsite backups.
Do this when local disk or guest image weight is the problem. Do not do it on day two because a blog said “real sites use S3.”
LiteSpeed + LSCache for guests (XenForo, community)
Community practice (SF-2) offloads guest HTML at the web server: LiteSpeed Cache (LSCache) with the XF2 add-on, nginx FastCGI cache, or a carefully configured edge. The model: serve a flat file, skip PHP-FPM for guests, bypass when a cookie means “this person is logged in.”
That is the right model. Official $config['pageCache'] is the in-app version of the same idea.
Rules:
- Guests only. Logged-in members must miss. Test a moderator session after you enable it.
- One owner. LiteSpeed or XenForo page cache or an edge HTML cache. Not a stack of three.
- Purge on thread create / post, or accept that guests will see a stale first page for the cache lifetime.
- LiteSpeed as the web server plus LSCache as the guest HTML layer is a community pairing, not an official XenForo requirement. Apache + official page cache is a valid stack. nginx + FastCGI cache is a valid stack. We will not rank them.
UI.X rebuilds can hit too many open files — raise open_files_limit (community example: 50000) or rebuild via CLI. That is a style-framework note, not a reason to skip cache.
Do not enable LSCache the same afternoon you enable Redis and a Cloudflare “speed” tickbox. One variable.
Cloudflare, and the Discourse JS rule
Cloudflare (or any similar edge) is optional. It is a DNS + TLS + DDoS + cache class. We will not rank it against other CDNs.
If you use it:
XenForo
- Do not Auto Minify or otherwise rewrite the JavaScript 2.3 already deferred. Community and the CWV article already called this an INP and “switcher broke” class of outage.
- Do not cache logged-in HTML at the edge unless you truly know the cookie bypass.
- Polish / Rocket Loader / “JS modifications” are how a style variation gadget dies.
Discourse (hard rule from the notebook)
- Disable JS minify / JS modifications or Discourse breaks.
DISCOURSE_CDN_URLmust include the protocol if you set an app CDN.- Do not treat Cloudflare as a substitute for the official container owning 80/443 on the standard install path, unless you are deliberately doing reverse proxy (advanced, not the one-liner).
Edge cache of static /uploads or /data/ avatars is usually fine. Edge cache of the application HTML is how you serve a guest page to a staff session. When in doubt, cache static, bypass the app.
A recommended XenForo box (no vendor names)
One honest machine, years of life:
- OS: a current Linux LTS you know how to patch.
- Web: LiteSpeed or nginx or Apache — one of them. TLS there.
internal/ equivalent on the private directories. Friendly URLs (htaccess.txt→.htaccesson Apache;try_fileson nginx). - PHP: 8.3 or 8.4, dedicated FPM pool, OPcache on. Size
pm.max_childrenfrom RAM, not from a WordPress blog. - Database: MySQL 8 or MariaDB, InnoDB, buffer pool you can explain.
- Redis: application cache; optional isolated page-cache context or LSCache for guests, not both.
- Mail: Symfony Mailer via PHP mail to a local MTA, or SMTP you have tested.
- Backups: dump +
data/+internal_data/off-box. - XFES / ES: absent until search is the incident and the process has a home.
- Object storage: absent until disk or backup windows say so.
Horizontal PHP (two app boxes) is possible in the abstract. Official docs do not hand you a clustering manual. Shared data/ and internal_data/, sessions in Redis, identical config.php. Do that only after this single box is honestly maxed. Most “we need a cluster” tickets are an unsized buffer pool or a permission rebuild.
XenForo Cloud is the same application without this SSH list. Compare it to your time, not to a fantasy $0 VPS — the cost comparison already has the public license/Cloud grid. We will not reprint dollar rows here as a host ranking.
A recommended Discourse box (still no vendor names)
- Official Docker on Ubuntu LTS, 2 GB+ RAM (1 GB only with swap, as a lab), 20 GB+ disk, ports 80/443 free.
- Official one-liner. Unattended upgrades.
fail2banon SSH. - SMTP decided: skip only if Discourse ID is enough; add SMTP before you promise digests.
- Backups weekly (default) or daily; one copy off-box; S3 backups in a separate bucket/prefix from uploads.
- Cloudflare: JS minify / JS modifications off.
- Plugins via
app.yml+ rebuild, not random installers. - Or: official hosted, and this list collapses to “read the plan’s email cap and download a backup.”
Import-sized droplets (community chatter around 4 vCPU / 8 GB) are import boxes. Do not buy 8 GB because an import thread exists.
Ladder (so you do not skip rungs)
XenForo, in order:
- PHP 8.3/8.4, debug/
fullJsoff, Gzip, HTTPS, cookie path correct. - Mail delivers. 2FA can use email if you enabled that method.
- Offsite backup you have restored.
- Redis application cache. Confirm style changes still appear.
- One guest-cache owner (official page cache or LSCache / FastCGI).
- InnoDB buffer pool honest. Process list understood.
- Attachment plan (WebP for new files, client-side resize, backup window).
- Permission matrix small. Rebuilds in a calendar window.
- XFES + ES/OS on their own resources when search is the incident.
- Only then object-storage add-ons, a second app box, or a DBA replica.
Discourse, in order:
- Official Docker or official hosted. Not Bitnami.
- HTTPS and a hostname you control (or a temporary
discourse.diyyou will leave). - Mail story you can explain.
- Backup frequency set; one file off-box; restore once.
- JS minify off at the edge.
- S3 for backups if the VPS is not the vault. Uploads-on-S3 later, different bucket.
If you are about to migrate because “the stack is hard,” check which rung you skipped. Stay or migrate is the decision framework.
What we will not rank
- Host brands, “best XenForo VPS 2026,” “best Discourse host.”
- LiteSpeed vs nginx vs Apache as a religion. Pick one, document the guest-cache owner.
- MySQL vs MariaDB as a morality play.
- Transactional email vendors.
- Cloudflare vs “no Cloudflare.”
- Cloud vs self-host as a personality test. Cloud is a pager you are buying. Self-host is a pager you already have.
If a comparison needs a public price, the cost article and the live vendor grids are the sources. This recipe does not reprint them as a podium.
Checklist
XenForo
- PHP 8.3 or 8.4 on the FPM pool the vhost uses; OPcache on
- MySQL 8 or MariaDB; InnoDB;
mysqli -
$config['debug']andfullJsfalse; Gzip intentional - Cookie path covers the board; Board URL is
https:// -
src/,internal_data/not fetchable - Redis up;
phpredisloaded; style change still appears after rebuild - One guest-cache owner (page cache or LSCache / FastCGI)
- Logged-in moderator is not served guest HTML
- Email: Symfony Mailer transport chosen; test arrived
- Backup = DB +
data/+internal_data/, off-box, same-version restore tested - XFES absent unless ES/OS 7.2+ exists and search is the incident
- Object storage named in the runbook, or absent
- Edge does not minify / rewrite XF JavaScript
Discourse
- Official Docker or official hosted
- 2 GB+ (swap if you insisted on 1 GB for a lab)
- Let’s Encrypt actually issued; 80/443 free on the standard path
- SMTP decided; digest implications understood
-
backup_frequencyset; one download off-box - S3 backups ≠ S3 uploads folder
- Cloudflare JS minify / JS modifications disabled
Both
- Someone who is not “whoever notices Discord” can restore
- No host-brand sentence in the runbook pretending to be architecture
What this article will not do
It will not publish pm.max_children magic. It will not invent official core S3 for XenForo. It will not tell you LiteSpeed plus Cloudflare plus native pageCache is “more faster.” It will not rank hosts. It will not retell the CWV hour or the scaling topology drawings.
It will not configure analytics tags or auto-mod add-ons. Those ride on this stack. They are not this stack.
Takeaways
- XenForo 2.3: PHP 8.x (8.3/8.4), MySQL 8 or MariaDB, Redis, HTTPS, Symfony Mailer, offsite DB +
data/+internal_data/. - Redis is official. Xon is the community operator’s add-on. Page cache is a separate context — or LSCache for guests, not both.
- Elasticsearch only with XFES, current 2.3 floor 7.2+, not on typical shared hosting, not as a speed charm.
- Discourse: official Docker or official hosted. JS minify off at Cloudflare. Backups off-box; S3 backups ≠ uploads.
- Object storage is a volume decision. XenForo has no core S3 checkbox in the public manuals we used.
- One guest-cache owner. One mail transport you have tested. One restore you have actually done.
- No host podium. The box you can restore is the recommended stack.
Patch the runtime. Name the owner. Then go back to the queue — the toolkit is what you open every day. This is what it stands on.

