
Going load-balanced is not "add a bigger box" or even "add a second box" — it is a change to where state lives. On a single server, PHP-FPM writes session files to local disk, uploaded images land in that site's home directory, and the database sits right there too, so every request has everything it needs on one machine. The moment a second, identical app server joins the pool, a browser's next request can land on either one, and if a session file or an uploaded file only exists on the first machine, requests to the second machine start failing in ways that look random: users get logged out mid-session, an image upload succeeds but then 404s on reload, a shopping cart empties itself.
The database is usually the one piece that does not get duplicated. Most load-balanced setups keep MariaDB on its own dedicated server that both app servers connect to over a private network, rather than running MySQL locally on each web node and dealing with replication and split-brain risk. If you have not already moved the database off the app server, do that first — it is a simpler, separate migration, and a load-balanced setup with a database still living on "server one" just recreates the single point of failure you were trying to remove.
Nginx's upstream module is enough for most setups: it runs as a reverse proxy in front of two or more identical app servers and distributes requests using round robin by default. Round robin sends requests to each backend in turn, which works well when requests cost roughly the same amount of CPU and time. If your traffic mixes cheap static requests with heavy dynamic PHP pages, least_conn is usually the better algorithm — it sends the next request to whichever backend currently has the fewest active connections, so a backend stuck processing a slow request does not keep getting piled on.
A dedicated Nginx box as the load balancer works fine and costs you one more small server. The alternative is a managed load balancer from your cloud provider — a DigitalOcean Load Balancer or an AWS Application Load Balancer — which terminates TLS for you, runs health checks natively, and does not become its own single point of failure the way one self-managed LB server can. Either way, the backend app servers should be identical: same PHP version, same site code, same environment configuration, listening on their own private IP so the load balancer can reach them without exposing each one directly to the internet.
A load balancer that keeps sending traffic to a dead backend is worse than no load balancer at all, because now a fraction of every visitor's requests silently fail. Open-source Nginx handles this with passive health checks: the max_fails and fail_timeout parameters shown above tell it to stop sending traffic to a backend after a number of failed attempts, and retry it again after the timeout expires.
That is reactive — it removes a backend only after real requests have already failed against it. Managed load balancers (DigitalOcean, AWS ALB) instead run active health checks: they poll a lightweight endpoint like /healthz on each backend every few seconds and pull a backend out of rotation the moment it stops responding correctly, before it costs a real user a failed request. If you are self-managing Nginx as the load balancer and want the same behavior, a small external script or systemd timer hitting each backend's health endpoint and rewriting the upstream block (or switching to Nginx Plus, which adds active checks natively) closes that gap. Either way, the health check endpoint should verify the app can actually reach its dependencies — PHP-FPM responding and the database connection working — not just that Nginx itself is up, since a backend that answers HTTP but cannot reach the database is still a dead backend from the user's perspective.
Deploying code now means shipping the same commit to every backend server at effectively the same time, not SSHing into one box and running a git pull. If server one is running yesterday's code and server two is running today's, the load balancer will happily route different visitors to different versions of your site, and you will spend an afternoon debugging a bug that "only happens sometimes" before realizing it only happens on one specific backend.
A deploy script that loops over every backend's IP and runs the same pull-and-restart sequence on each is the minimum viable version of this. This is also where managing multiple servers from a single place stops being a nice-to-have: CloudStick connects all of your backend servers to one dashboard, so you can see which server is running which deployed version, push identical changes across the pool, and use Server Transfer to move a site to a new node when you are scaling the pool up, without losing track of which box has drifted from the others. If your team is more than one person, the Teams feature keeps everyone deploying against the same connected set of servers instead of someone quietly SSHing into just one.
Work through this in order rather than adding the second server first and patching problems as they surface:
Move the database onto its own dedicated server, reachable from app servers over a private network, if it is not there already. Set up a shared session store (Redis) and point PHP-FPM at it instead of local disk. Move uploaded files to shared storage — NFS mounted at the same path on every backend, or object storage referenced by URL. Provision a second, identical app server: same OS, same PHP version, same site code, same environment variables, listening on a private IP. Stand up the load balancer, choose round robin or least_conn based on how uniform your request costs are, and configure max_fails/fail_timeout (or a managed LB's active health checks) so a dead backend is automatically pulled from rotation. Write or adapt a deploy script that pushes the same code to every backend in one pass, and test a full deploy before you rely on it under real traffic. Finally, kill one backend on purpose during low-traffic hours and confirm the site keeps serving requests without a visible blip — that single test tells you more about whether the setup actually works than reading any config file twice.

