Deploying NodeBB on Docker with MongoDB
My friends and I needed an Internet forum, to be able to have deep, complex conversations with people I trust, permanent, archived, and accessible. What suggested itself was an Internet forum.
This post starts with an inordinate amount of personal and philosophical musings, so if you want, you can skip them to get down to business.
Choosing Internet Forum Software
A little while ago I was discussing with a friend the things I am looking for in online interaction that I'm not getting from traditional or federated social media. A lot of different things came up, but the most important and central is this:
the need to be able to have deep, complex conversations with people I trust, in a way that those conversations are not ephemeral, lost in the endlessly scrolling feed, like an infinite dark plain in which attention is but a circle of candlelight—but permanent, archived, accessible.
This is because I want to be able to collaborate with people to create ideas, with complexity and nuance, for which it's necessary to be able to build on the past and plan for the future.
So, my friend and I have identified some key needs:
- To build a trusted community,
- To build and enforce community norms,
- To keep some discussions private, and some public,
- To keep past conversations easily findable and accessible.
A few other important pieces are:
- A good mobile experience,
- Inexpensive to operate,
- Extensible with new features that might be needed.
And, finally, it would be nice, but not mandatory, to be able to achieve some level of federation with the Fediverse, so long as it's strictly limited and controlled.
Given those requirements, what suggested itself was an Internet forum.
What is an Internet forum, you may ask? Well, my child, let me tell you. When the world and I were young, before high-speed Internet, YouTube, social media, Elon Musk's physiognomy, and placental mammals, though not before the Mythic Age of 2400 baud modems and BBSs (I'm not that old), Internet forums reigned supreme.
According to Wikipedia, "Early Internet forums could be described as a web version of an electronic mailing list or newsgroup (such as those that exist on Usenet), allowing people to post messages and comment on other messages." I remember as a young fellow, on a forum whose name I won't mention, we used to give each other homework whilst pretending to be Jedi Knights and calling each other names like Jom Terra and Master Olórin (thank God, I don't remember what I used). At one point I even made us a website. It was actually the first time I learned to write HTML and CSS.
Forums are encapsulated communities, with both technical and social features for building and enforcing norms. Good forum software provides both private and public spaces (although typically it's necessary to sign up to post and reply), and forums are archival, in that conversations aren't lost but categorized and saved, with permalinks, in a way that is designed to be accessible to be referenced and interacted with long-term.
So once I identified that a forum is what I wanted, the next task was to choose the right software. We want something that is lightweight and easy to run, but not so archaic that it'll only appeal to alter kakers (old farts) like me, and ignore the needs of mobile users altogether. We also want to look for something that's got a good plugin architecture in a language we're familiar with, and federation or some plans to build it in.
The winner: NodeBB
I've spent quite some time looking at various forum software. Some are just too old-school, like phpBB. Some don't inspire confidence that they are well-supported, like Flarum. Some are just proprietary, which is not what I'm into for this project. In the end, I landed on the two most popular modern Internet forum applications, Discourse and NodeBB. They both promise great mobile support and have strong developer and user communities.
I took a look at both, tried using some forums powered by them, and looked at various developer and user discussions. t seems that there is something close to feature parity with Discourse and NodeBB, so the real deciding factor is cost and performance.
Performance
NodeBB seems lighter and faster, both on the back end and on the front end. While I didn't do a systematic study for this blog post, I was was definitely waiting less for NodeBB threads to load. I think that NodeBB is a winner here, although I don't have numbers to back it up.
Cost
Beyond running the service itself, both apps will require installing dependencies. Neither Discourse nor NodeBB work with MySQL, which Ghost depends on. Discourse will require Postgres and Redis, whereas NodeBB has a more flexible configuration: MongoDB is default, but it can also run on Postgres, although that's considered a legacy option, or on Redis.
The advantages here are less clear cut. Do I prefer to add Postgress to my stack, or one of Redis or MongoDB? I think that simply as a matter of principle, I would rather add a NoSQL DB than another relational database. I also suspect that we'll get better performance with NoSQL in this use case. Also, Redis will be very quick—but expensive in RAM. All things considered, I think that my preferred option here will be MongoDB as a middle ground between an RDBMS and an in-memory store.
Federation
Both applications support federation via ActivityPub. Discourse has a plugin, and NodeBB just came out with native support this week.
So, all things consedered, while I can't claim that NodeBB is objectively superior, it's clearly a better choice for me. We'll go with NodeBB + MongoDB on Docker.
Standing it Up
In the NodeBB repo, there is an example docker-compose.yml
. We'll review it for guidance and inspiration, and then update our existing setup to stand up first MongoDB and then NodeBB. We'll also update Caddy to reverse-proxy the appropriate endpoint.
The first thing you'll have to do is to add the appropriate A
record to your DNS. While that's getting propagated, we can work on configuring and standing up the services. The forum I'm standing up is https://forum.tachlis.social.
MongoDB
The example docker-compose.yml
in the NodeBB docs is clearly not suited for anything but a quick demo. I reviewed the documentation and came up with this more practical alternative.
We'll set the mongo
service up with environment variables and secrets to initialize a root account, volumes for the data, logs, configs and init. Finally, we'll also be sure to connect it to the web
and mongo
networks.
If I ever have issues that convince me that I need to use MongoDB 8 or later, I'll set up a different host and move the service over there.
Just like we did for mysql, we'll create mongo_root_password
. To reduce confusion, we'll also rename db_root_password
to mysql_root_password
.
We'll also set up configuration. Even though at the time of this writing I don't have logging and metrics set up for MySQL, I'll set MongoDB logging up from the start. We'll also make sure that authorization is enabled. The Docker documentation says that it'll turn on by default if both the username and password environment variables are supplied, but I wanted to be extra certain. The configuration will live in ./mongo/mongo.conf
from where it'll be bind mounted to the container.
Let's try to stand this up, and take a look at the logs:
marc@gause:~$ sudo docker compose up mongo
...
marc@gause:~$ sudo docker logs marc-mongo-1 -f
...
Reviewing the logs, I saw this warning:
{ ... "ctx":"initandlisten","msg":"vm.max_map_count is too low","attr":{"currentValue":65530,"recommendedMinimum":1677720,"maxConns":838860},"tags":["startupWarnings"]}
What is this vm.max_map_count
value? According to this Stack Exchange answer, it is a setting that limits the number of discrete mapped memory areas - on its own it imposes no limit on the size of those areas or on the memory that is usable by a process. So MongoDB says that it wants this number to be higher. Why?
This other Stack Exchange answer can help us understand what's going on. It turns out, according to Joe, that MongoDB requires two mapped memory areas per incoming connection, and therefore the host kernel parameter vm.max_map_count
should be twice the MongoDB setting net.maxIncomingConnections
. Now, based on the warning log, we know that our max_map_count
is set to 65,530. The MongoDB docs suggest:
For large systems, the following values provide a good starting point:
...
-vm.max_map_count
value of 131060
So I'm not sure what they mean by "large," but I am certain that our system is way, way smaller than that. Therefore, I think that we can keep the lower value that the host is already set to. Plus, I'm not really thrilled to mess with the host kernel parameter.
What's the alternative? We could reduce the net.maxIncomingConnections
setting. Another look at the MongoDB docs tells us that the default for that on Linux is set to (
RLIMIT_NOFILE
) * 0.8
. So all we have to do is set the nofiles limit on the container to 65,530 / 2 / 0.8 = 40956.
We can find the limits on our container like so:
marc@gause:~$ sudo docker exec marc-mongo-1 sh -c "ulimit -a"
...
nofiles 1048576
...
We'll set ulimits
in docker-compose.yml
like this, setting the soft limit to our requirement and leaving the hard limit at the default:
And the warning goes away. However, now we have a new one:
{ ... "ctx":"initandlisten","msg":"Soft rlimits for open file descriptors too low","attr":{"currentValue":40956,"recommendedMinimum":64000},"tags":["startupWarnings"]}
I hate warnings. I know that they're just warnings, and might be safe to ignore. But something about them offends my aesthetic sense, and that's no good in a Russian.
According to SUSE Linux docs, there should be no negative side effects to increasing vm.max_map_count
, so let's give it a try. We'll have to edit the host's /etc/sysctl.conf
file and apply the settings:
marc@gause:~$ echo 'vm.max_map_count=102400' | sudo tee -a /etc/sysctl.conf
vm.max_map_count=102400
marc@gause:~$ sudo sysctl -p
fs.xfs.xfssyncd_centisecs = 100
fs.xfs.filestream_centisecs = 100
vm.dirtytime_expire_seconds = 100
vm.max_map_count = 102400
Next we'll set services.mongo.ulimits.nofile.soft
in docker-compose.yml
to 64000, and now when we re-up mongo
, the warning is gone. Yay!
NodeBB
Before we can stand up NodeBB itself, we have to initialize the forum_tachlis_social
database and forum_tachlis_social_user
user in MongoDB:
marc@gause:~$ sudo docker exec -it marc-mongo-1 mongosh admin
Current Mongosh Log ID: 678d1b4f312713189ce94969
Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.3.4
Using MongoDB: 7.0.16
Using Mongosh: 2.3.7
For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/
admin> db.auth("mongodb_root", passwordPrompt());
Enter password
******************************************{ ok: 1 }
admin> use forum_tachlis_social
tachlis> db.createUser({
... user: "forum_tachlis_social_user",
... pwd: passwordPrompt(),
... roles: [
... { role: "readWrite", db: "forum_tachlis_social" },
... { role: "clusterMonitor", db: "admin" },
... ],
... });
Enter password
*****************************************{ ok: 1 }
forum_tachlis_social>
Let's test our new user and the authorization:
forum_tachlis_social> db.auth("forum_tachlis_social_user", passwordPrompt());
Enter password
*****************************************{ ok: 1 }
forum_tachlis_social> db.getCollectionNames();
[]
forum_tachlis_social> use admin
switched to db admin
admin> db.getCollectionNames();
MongoServerError[Unauthorized]: not authorized on admin to execute command { listCollections: 1, filter: {}, cursor: {}, nameOnly: true, authorizedCollections: false, lsid: { id: UUID("f5699e08-cdab-4d05-82a2-4ca1bccc302a") }, $db: "admin" }
admin> quit()
marc@gause:~$
Looks like we have read access to forum_tachlis_social
but not to admin
, which is the intended behavior. Perfect. Now we should be ready to install NodeBB.
The NodeBB Dockerfile works like this: when it comes up, it detects whether or not certain configuration files are present, and if not it launches a setup application in the browser, where you can set up some things like the forum web address, an admin user and the database connection information. Therefore, it is not necessary to pass any passwords or anything like that into the environment.
The docker-compose.yml
in the NodeBB repo has the image set to nodebb:latest
, which is not what I want—I want to stick to the minor so that I don't get any major surprises later on... I'll upgrade when I'm ready, not by accident because the service came down. We can check what tags are available by running:
marc@gause:~$ sudo docker run --rm quay.io/skopeo/stable list-tags docker://ghcr.io/nodebb/nodebb
{
"Repository": "ghcr.io/nodebb/nodebb",
"Tags": [
"latest",
...
"4.0.0",
"4.0",
...
]
}
Turns out 4.0.0 got released the very day I was working to stand this up! Neat. I was wondering why the community forum was down for a couple of hours this afternoon.
We'll just add the service to docker-compose.yml
with the right volumes, and set the image tag to 4.0:
NodeBB listens on port 4567. Let's set up the route in Caddy:
Now restart Caddy and stand up NodeBB:
marc@gause:~$ sudo docker compose down caddy
marc@gause:~$ sudo docker compose up -d caddy
marc@gause:~$ sudo docker compose up -d forum_tachlis_social
The installer should be available at the forum address:
Done! Once the setup application is done writing the configs and initializing the database, we can visit our new forum:
Now we can use the admin interface to start customizing our community. There's just one more thing, though, that has to be set up before we can call our forum fully functional.
Setting up Transactional Emails
We can set up SMTP transport in the settings/email section of the admin section. I'm still shopping around for SMTP options, especially since Mailgun only lets you use one domain on the free plan. This time I tried smtp2go, which has great deliverability and a decent free tier. I'm not going to get into how to set up your account, but they have good documentation.
Here's what the email settings look like in the NodeBB admin interface. Don't forget to enable SMTP transport, enter your credentials, and click "send test email."
Done! Now we can start inviting people to our forum, and hope that our friends will want to join and post things while we address the next steps.
Next Steps
- Monitoring. We'll want to set up monitoring similar to what we did with Ghost. We'll use Fluent Bit to read the log file that should be in
/var/log/
and use it to generate metrics. - Optimization. NodeBB has some optimization tips, such as serving static assets using the reverse proxy as the file server. A deeper dive into this subject can help us reduce costs and improve performance.
- Explore! NodeBB is very feature rich. I'm going to work with my users and spend some time exploring all its features to facilitate the right kind of community building for us.