This article explores how Node.js web applications can be scaled across multiple CPU cores and even machines using Docker.
Deploying and scaling a Node.js web app (like a Next.js app) is easier than ever thanks to the cloud and serverless!
But what if you still want to be in charge of your own server architecture? Or maybe you just want to pay for your server resources and not your traffic or execution time?
Deploying your web app on a VPS (Virtual Private Server) is the perfect option in that case!
Node.js is single-threaded by nature. That means that it can usually only utilize one CPU core/thread and only achieve "concurrency" by switching between tasks on that single thread (the so called "Event Loop").
But what if your server has more than one CPU core? How can you leverage those to be able to handle more incoming traffic?
Despite its single-threaded nature, Node.js still allows you to utilize multiple CPU cores.
Node.js introduced "cluster mode" to achieve some level of "multi-threading". However, because Node.js is still a single-threaded runtime, all cluster mode does is running multiple instances of your app each having their own interpreter/runtime.
There are also very popular third-party libraries like "pm2" that implement this concept. pm2 also has built-in load-balancing, so it's definitely worth checking out!
If you're already using Docker and Docker Compose in your setup, it can be a great alternative to skip implementing cluster mode or pm2 altogether.
You can just use Docker Compose to create multiple replicas of your Node.js app (a Next.js app in my case).
Docker Compose makes it super easy to create multiple containers running the same service ("replicas"). They can all utilize a different CPU core without the need for any tools or additional code in your app.
It just requires a little edit of the
version: '3.7' services: your-web-app: image: registry/.../your-website-image:latest ... deploy: replicas: 3 ...
Simply add the
deploy key to the service you want to replicate and specify the amount of replicas you wish (just like in the example above).
The last thing you have to figure out now is load balancing. After starting your Docker Compose stack, there are three containers running the same application. Your reverse proxy or load balancer needs to evenly send requests to one of the replicas.
For an example setup see the example below.
Let's take a look at my website fabiancdng.com as a real-world example:
My website is a Next.js application that uses Server Side Rendering (SSR) on some pages. Therefore, it needs the Node.js runtime and can't just be deployed as static assets.
I deployed the site in a Docker container (with Docker Compose) on a cheap VPS with 6 virtual CPU cores.
In configured Docker Compose to run three replicas of the Next.js app (all in their own container).
For routing and distributing incoming traffic evenly across the replicas, I need a reverse proxy that also acts as a load balancer.
I use Traefik as a reverse proxy that takes all the incoming traffic to
http(s)://*.fabiancdng.com/* and routes it to the corresponding Docker container within the internal Docker network.
NGINX is another popular solution for this purpose. And both NGINX and Traefik support load balancing.
In my case, Traefik supports load balancing between replicas of the same service out of the box so there was no additional configuration needed.
Once you've got your load balancing between the replicas in place, your traffic will be distributed among CPU cores, making your service able to handle many more incoming requests. 🥳 🎉
If you run this configuration using a container orchestration tool like Kubernetes or Docker Swarm, you can even scale your app horizontally this way. You can distribute the load not just across replicas on one server, you can have a ton of replicas running on a ton of different servers.
Docker Swarm has built-in load balancing between nodes. So if you plan on doing this, that might be worth checking out.
However, if your application has reached an amount traffic that is worth distributing across multiple machines, you might just consider moving to the cloud and a managed infrastructure.
Whether this is a good alternative to just moving to a managed service like Vercel or AWS Amplify (when deploying a Next.js app, for instance) or a managed container orchestration service is hard to tell...
Even though those services can get quite expensive for high-traffic sites, they often offer generous free-tiers and pay-as-you-go models. Also, they guarantee availability all around the globe thanks to edge routing and CDNs.
However, you can scale a loooong way with just a cheap VPS and you can protect yourself from unexpected costs for traffic or execution time. Also, you can deploy many different services on a single machine (maybe you would like a database or a free, open-source analytics tool as well?) and you can learn a lot about server administration, Docker and the complexity behind web applications and their architecture.
My recommendation: If you plan on building a full-stack side project or small SaaS and you need to deploy front end, back end, database, caching layer, etc., use a VPS, if you are okay with the additional configuration effort.
If you need to scale and you're just getting started with networking and backend engineering... Don't even bother with tools like Kubernetes. The growing complexity and maintenance effort to just keep your service running is likely not worth your time. Focus on building the app rather than the architecture around it and just use a cloud service.
📣 This post was originally published on my website on May 6, 2023.
If you found this article helpful, consider following for more and giving the post a 💖.
You can also show your appreciation by leaving me a tip for my next coffee ☕️.
Your support would mean a lot to me! ❤️