Real-time Forum
    Node.js + Socket.IO

    Deploy NodeBB on a VPS

    Deploy NodeBB 4.x on a RamNode KVM VPS with MongoDB, nginx WebSocket-aware reverse proxy, Let's Encrypt, systemd, external SMTP, and ActivityPub federation.

    NodeBB sits between Flarum and Discourse. It is a Node.js application using Socket.IO for real-time updates, it runs on your choice of MongoDB, Redis, or PostgreSQL, and it ships with a genuinely useful set of bundled plugins including 2FA, spam filtering, and Markdown composition. It also has a native ActivityPub implementation, which means forum categories can federate with Mastodon, Lemmy, and other Fediverse platforms without a third-party plugin. That is a real differentiator if it matters to your community.

    This guide deploys NodeBB 4.x on a RamNode KVM VPS with MongoDB, nginx as a reverse proxy with WebSocket support, Let's Encrypt, a systemd unit, an external SMTP relay, and a backup process.


    1. Choose the right RamNode instance

    NodeBB's resident footprint is modest once running. The pain point is npm install, which is memory-hungry and will get OOM-killed on an undersized box without swap.

    Community sizeRAMvCPUDisk
    Small forum, under 100 active users1 GB + swap125 GB
    Under 500 active users2 GB240 GB
    Larger, or clustered4 GB+460 GB+

    Notes for RamNode specifically:

    • KVM, Ubuntu 24.04 LTS x86_64. MongoDB needs a real kernel and AVX support on modern versions. RamNode's KVM plans are fine here.
    • Swap is not optional on 1 GB. NodeBB's own docs say installing dependencies can need more than 512 MB.
    • Location matters more than usual. NodeBB's whole value proposition is real-time. Pick the RamNode location closest to your users.

    2. Database choice

    NodeBB supports three backends through a database abstraction layer. They are not equivalent for your purposes.

    BackendWhen to pick it
    MongoDBThe default and the best-documented path. Pick this unless you have a specific reason not to.
    RedisFastest, but everything lives in memory. On a 1 GB RamNode plan this is a bad trade. Required if you want clustering.
    PostgreSQLFine if you already run Postgres and want one less daemon. Less community mileage than Mongo.

    This guide uses MongoDB 8.0. Note that Redis is also required if you ever want horizontal clustering, since it doubles as the session store in that configuration. That is out of scope for a single-node RamNode deployment.

    3. Prerequisites

    1. A domain or subdomain with an A record on your RamNode IPv4, propagated before you request certificates.
    2. An external SMTP relay.
    3. SSH access as a sudo user.

    The SMTP situation on RamNode

    RamNode does not permit mail services on its VPS platform, and outbound port 25 is not a path available to you. NodeBB needs email for registration confirmation, password resets, and digests. Configure it against a transactional provider over 587 or 465: Postmark, SendGrid, Mailgun, Amazon SES, Brevo, or similar.

    NodeBB will install and run without email configured. It will also quietly fail to confirm any account. Set it up before you invite anyone, and configure SPF, DKIM, and DMARC at your DNS provider.

    4. Base server preparation

    shell
    apt update && apt upgrade -y
    hostnamectl set-hostname forum.example.com
    timedatectl set-timezone UTC

    Reboot if the kernel changed.

    Create a dedicated unprivileged user. Do not run NodeBB as root.

    shell
    adduser --system --group --home /opt/nodebb --shell /bin/bash nodebb

    Swap

    shell
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab
    echo 'vm.swappiness=10' >> /etc/sysctl.d/99-nodebb.conf
    sysctl --system
    free -h

    Firewall

    NodeBB listens on 4567 behind nginx. That port must never be exposed.

    shell
    apt install -y ufw
    ufw allow 22/tcp
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw --force enable
    ufw status verbose

    Deliberately absent: 4567 and 27017. If you can reach either from outside, something is wrong.

    Automatic security updates and fail2ban

    shell
    apt install -y unattended-upgrades fail2ban
    dpkg-reconfigure -plow unattended-upgrades
    systemctl enable --now fail2ban

    5. Install Node.js 22

    NodeBB 4.11 and later require Node.js 22 or greater. Node 20 is at or near end of life and NodeBB has moved its floor. Install from NodeSource rather than Ubuntu's repo, which lags.

    shell
    apt install -y ca-certificates curl gnupg git build-essential
    curl -fsSL https://deb.nodesource.com/setup_lts.x -o /tmp/nodesource_setup.sh
    less /tmp/nodesource_setup.sh
    bash /tmp/nodesource_setup.sh
    apt install -y nodejs
    
    node -v    # expect v22.x or higher
    npm -v

    build-essential is there because a few native modules compile during install. Leaving it out produces a confusing wall of node-gyp errors.

    6. Install and secure MongoDB

    Add the MongoDB 8.0 repository:

    shell
    curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
      gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg
    
    echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" \
      > /etc/apt/sources.list.d/mongodb-org-8.0.list
    
    apt update
    apt install -y mongodb-org
    systemctl enable --now mongod
    systemctl status mongod

    Create the database and users

    shell
    mongosh
    shell
    use admin
    db.createUser({
      user: "admin",
      pwd: "a-long-random-admin-password",
      roles: [ { role: "root", db: "admin" } ]
    })
    
    use nodebb
    db.createUser({
      user: "nodebb",
      pwd: "a-long-random-nodebb-password",
      roles: [
        { role: "readWrite", db: "nodebb" },
        { role: "clusterMonitor", db: "admin" }
      ]
    })
    quit()

    The clusterMonitor role is what lets NodeBB show database statistics in the admin control panel. It is read-only. Skip it if you would rather not grant it, and accept that the ACP database page will be blank.

    Enable authentication and bind locally

    Edit /etc/mongod.conf:

    shell
    net:
      port: 27017
      bindIp: 127.0.0.1
    
    security:
      authorization: enabled
    shell
    systemctl restart mongod
    ss -lntp | grep 27017

    Confirm the bind is on 127.0.0.1 only. An internet-reachable Mongo with weak auth is a well-known way to lose a database.

    Test:

    shell
    mongosh "mongodb://127.0.0.1:27017/nodebb" --username nodebb --authenticationDatabase nodebb

    7. Install NodeBB

    Clone the versioned branch. Only versioned branches are stable; do not deploy from develop or master.

    shell
    mkdir -p /opt/nodebb
    chown nodebb:nodebb /opt/nodebb
    sudo -u nodebb -H bash
    cd /opt/nodebb
    git clone -b v4.x https://github.com/NodeBB/NodeBB.git .

    Run the setup CLI. NodeBB does not start with npm start; it uses its own CLI wrapper.

    shell
    ./nodebb setup

    You will be prompted. The answers that matter:

    shell
    URL used to access this NodeBB    https://forum.example.com
    Please enter a NodeBB secret      (accept the generated default)
    Which database to use             mongo
    MongoDB connection URI            (leave blank)
    Host IP or address of your MongoDB instance   127.0.0.1
    Host port of your MongoDB instance            27017
    MongoDB username                              nodebb
    Password of your MongoDB database             <your nodebb password>
    MongoDB database name                         nodebb

    Get the URL exactly right. If your forum will be served at https://forum.example.com, enter exactly that, with the scheme and no trailing slash. A wrong value here produces broken asset paths and a forum that half-works in ways that are tedious to diagnose. You can fix it later in config.json, but fix it properly rather than working around it.

    Setup then prompts for the administrator account. That is your forum login, separate from the database credentials.

    The result is /opt/nodebb/config.json. It holds your database credentials, so:

    shell
    chmod 600 /opt/nodebb/config.json

    Exit back to your sudo user.

    8. systemd unit

    NodeBB has a built-in ./nodebb start daemon mode. Use systemd instead. It gives you proper restart behavior, log integration, and boot ordering.

    /etc/systemd/system/nodebb.service:

    shell
    [Unit]
    Description=NodeBB
    Documentation=https://docs.nodebb.org
    After=network.target mongod.service
    Requires=mongod.service
    
    [Service]
    Type=simple
    User=nodebb
    Group=nodebb
    WorkingDirectory=/opt/nodebb
    ExecStart=/usr/bin/node loader.js --no-daemon --no-silent
    Restart=always
    RestartSec=5
    StandardOutput=journal
    StandardError=journal
    SyslogIdentifier=nodebb
    Environment=NODE_ENV=production
    
    # Hardening
    NoNewPrivileges=true
    PrivateTmp=true
    ProtectSystem=strict
    ProtectHome=true
    ReadWritePaths=/opt/nodebb
    
    [Install]
    WantedBy=multi-user.target
    shell
    systemctl daemon-reload
    systemctl enable --now nodebb
    systemctl status nodebb
    journalctl -u nodebb -f

    If ProtectSystem=strict causes trouble with a plugin that wants to write outside /opt/nodebb, add the path to ReadWritePaths rather than dropping the hardening.

    9. nginx reverse proxy with WebSocket support

    NodeBB listens on 4567. Without nginx you would be serving http://forum.example.com:4567, and the Socket.IO connection is the part people get wrong.

    /etc/nginx/sites-available/nodebb:

    shell
    upstream nodebb {
        server 127.0.0.1:4567;
        keepalive 32;
    }
    
    server {
        listen 80;
        listen [::]:80;
        server_name forum.example.com;
    
        client_max_body_size 32M;
    
        location / {
            proxy_pass http://nodebb;
            proxy_http_version 1.1;
    
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host       $host;
            proxy_set_header X-Real-IP  $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            proxy_redirect off;
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    
        access_log /var/log/nginx/nodebb.access.log;
        error_log  /var/log/nginx/nodebb.error.log;
    }

    The three lines that make real-time work are proxy_http_version 1.1, the Upgrade header, and Connection "upgrade". Miss any of them and the forum loads but never updates live, and your browser console fills with failed Socket.IO polling.

    shell
    ln -s /etc/nginx/sites-available/nodebb /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    nginx -t
    systemctl reload nginx

    Tell NodeBB it is behind a proxy

    In /opt/nodebb/config.json:

    shell
    {
      "url": "https://forum.example.com",
      "secret": "...",
      "database": "mongo",
      "port": 4567,
      "bind_address": "127.0.0.1",
      "trust_proxy": true
    }

    bind_address on 127.0.0.1 means 4567 is unreachable from outside regardless of what ufw is doing. trust_proxy makes NodeBB honor X-Forwarded-For, which matters for rate limiting and IP-based moderation. Restart after editing:

    shell
    systemctl restart nodebb

    10. TLS with Let's Encrypt

    shell
    apt install -y certbot python3-certbot-nginx
    certbot --nginx -d forum.example.com --agree-tos -m you@example.com --redirect
    certbot renew --dry-run

    Confirm url in config.json is https://, not http://. Mismatched scheme produces mixed-content failures and broken Socket.IO handshakes.

    Behind Cloudflare, use DNS-only for the initial issuance, then enable the proxy with SSL mode Full (strict). Also enable WebSockets in the Cloudflare network settings, or real-time will silently degrade.

    11. Configure email

    Admin control panel, Settings, Email. Set:

    shell
    From address:    forum@example.com
    From name:       Your Forum
    Email service:   Custom
    SMTP host:       smtp.postmarkapp.com
    SMTP port:       587
    Use TLS:         yes (or 465 with implicit TLS)
    Username:        <provider token>
    Password:        <provider token>

    Use the "Send test email" control. Failures show up in journalctl -u nodebb.

    Then turn on requireEmailConfirmation under Settings, User, so bots do not fill your user table.

    12. Post-install hardening

    In the ACP:

    • Enable 2FA for administrators. nodebb-plugin-2factor is bundled. Turn it on under Extend, Plugins, then enroll from your user settings.
    • Configure the spam-be-gone plugin with Akismet or similar. Bundled, disabled by default. A public forum without it will be full of casino posts within a week.
    • Review Settings, Advanced, Rate limiting.
    • Set maximumFileSize against your RamNode disk allocation.
    • Review the privilege matrix under Manage, Privileges. NodeBB's default privileges are permissive for registered users.

    ActivityPub federation

    NodeBB 4.x ships native ActivityPub. Categories can publish to and be followed from Mastodon, Lemmy, and Misskey. It is off by default. Before turning it on, understand that federation means your posts leave your server and are cached elsewhere, permanently and outside your control. That is a moderation and privacy decision, not a technical one. Enable it under Settings, ActivityPub once you have made it deliberately.

    13. Backups

    Three things to back up: the Mongo database, /opt/nodebb/public/uploads/, and config.json.

    /usr/local/bin/nodebb-backup.sh:

    shell
    #!/bin/bash
    set -euo pipefail
    
    BACKUP_DIR=/srv/backups/nodebb
    STAMP=$(date +%Y%m%d-%H%M%S)
    KEEP_DAYS=14
    
    mkdir -p "$BACKUP_DIR/db-$STAMP"
    
    mongodump \
      --uri="mongodb://nodebb:${NODEBB_DB_PASS}@127.0.0.1:27017/nodebb?authSource=nodebb" \
      --out="$BACKUP_DIR/db-$STAMP"
    
    tar czf "$BACKUP_DIR/db-$STAMP.tar.gz" -C "$BACKUP_DIR" "db-$STAMP"
    rm -rf "$BACKUP_DIR/db-$STAMP"
    
    tar czf "$BACKUP_DIR/files-$STAMP.tar.gz" \
      -C /opt/nodebb public/uploads config.json
    
    find "$BACKUP_DIR" -type f -mtime +$KEEP_DAYS -delete
    shell
    chmod +x /usr/local/bin/nodebb-backup.sh
    echo 'NODEBB_DB_PASS=your-nodebb-password' > /root/.nodebb-backup.env
    chmod 600 /root/.nodebb-backup.env

    Cron:

    shell
    20 3 * * * . /root/.nodebb-backup.env && /usr/local/bin/nodebb-backup.sh >> /var/log/nodebb-backup.log 2>&1
    45 3 * * * rsync -az --delete /srv/backups/nodebb/ user@backup-host:/srv/backups/nodebb/

    A RamNode VPS is a single node. Backups on the same disk as the forum are not backups.

    Restore

    shell
    systemctl stop nodebb
    tar xzf db-20260716-032001.tar.gz -C /tmp
    mongorestore --drop \
      --uri="mongodb://nodebb:PASS@127.0.0.1:27017/nodebb?authSource=nodebb" \
      /tmp/db-20260716-032001/nodebb
    tar xzf files-20260716-032001.tar.gz -C /opt/nodebb
    chown -R nodebb:nodebb /opt/nodebb
    systemctl start nodebb

    Test this on a scratch VPS before you need it.

    14. Upgrades

    NodeBB ships releases at a high cadence. It has a built-in migration runner that discovers and executes upgrade scripts in semver order, which makes upgrades less frightening than they could be. It does not make backups optional.

    Within the 4.x line

    shell
    systemctl stop nodebb
    sudo -u nodebb -H bash
    cd /opt/nodebb
    git fetch
    git checkout v4.x
    git pull
    ./nodebb upgrade
    exit
    systemctl start nodebb
    journalctl -u nodebb -f

    ./nodebb upgrade runs npm install, rebuilds assets, and runs pending migrations in one pass. On a 1 GB plan this is the step most likely to get OOM-killed, which is why you configured swap.

    If a plugin breaks the build after an upgrade, start with plugins disabled:

    shell
    ./nodebb reset -p

    Then re-enable them one at a time.

    Across major versions

    Read the upstream upgrade notes first. Node.js floor changes between majors, and 4.11 already moved to Node 22. Check your Node version before starting:

    shell
    node -v

    15. Troubleshooting

    Forum loads but nothing updates in real time. WebSocket proxying. Check proxy_http_version 1.1 and the Upgrade/Connection headers in your nginx block. If you are behind Cloudflare, check that WebSockets are enabled there too.

    Assets 404 or the page renders unstyled. url in config.json does not match how you are actually reaching the site. Fix it, then ./nodebb build and restart.

    ./nodebb upgrade gets killed. OOM. dmesg | grep -i oom. Add swap or size up. Consider --max-old-space-size tuning as a last resort.

    Cannot connect to MongoDB after enabling auth. Confirm authSource. NodeBB's user was created in the nodebb database, so authSource=nodebb, not admin.

    Service will not start after adding systemd hardening. ProtectSystem=strict is blocking a write. Check journalctl -u nodebb for the path and add it to ReadWritePaths.

    Emails never arrive. Check journalctl -u nodebb around the send. Confirm 587 or 465 with a real provider. Port 25 is not available to you on RamNode.

    High memory after weeks of uptime. Node processes grow. systemctl restart nodebb on a weekly timer is a legitimate mitigation on small plans, not a defeat.

    16. Where NodeBB fits

    NodeBB is the reasonable middle. It is lighter than Discourse and more capable out of the box than Flarum, the bundled plugin set covers most of what a real forum needs without hunting through a catalog, and the ActivityPub story is genuinely unique among the three. The cost is a Node.js and MongoDB stack to maintain, which is more surface area than Flarum's PHP and MariaDB, and a smaller community than Discourse's when you go looking for answers.

    Pick Discourse if moderation tooling and trust levels are the point. Pick Flarum if you want the smallest possible footprint on shared infrastructure. Pick NodeBB if you want real-time, federation, or both. All three have companion guides.


    Quick reference

    shell
    Install root:   /opt/nodebb
    Config:         /opt/nodebb/config.json
    Uploads:        /opt/nodebb/public/uploads
    Listens on:     127.0.0.1:4567
    Service:        systemctl {start,stop,restart,status} nodebb
    Logs:           journalctl -u nodebb -f
    
    ./nodebb setup      # initial configuration
    ./nodebb upgrade    # pull deps, rebuild, migrate
    ./nodebb build      # rebuild assets only
    ./nodebb reset -p   # disable all plugins
    ./nodebb reset -t   # reset to default theme