Hosting Multiple Sites on one VPS
Jun 29, 2024 • 6 minute read
tags:
#coolify
#docker-compose
#self-hosting
#vps
A few months ago, I came across a Pieter Levels tweet, claiming he hosts all his sites on a single VPS:
Arguably a pretty beefy VPS (given the price), but a single VPS nonetheless. Reading more about his setup, I noticed he’s using sqlite and as few tools as possible to keep things simple. Generally tools not considered optimal for high-traffic websites. And yet, his sites make $200K/month and can obviously handle a lot of traffic.
Personally, I’ve mostly hosted my side-projects on services like Vercel, Netlify, Replit and Heroku. In my startups, the architecture has often been more complex, with Kubernetes clusters and/or managed services. Tbf, quite a few of my side-projects could have been static sites on like GitHub Pages. (Let me over-engineer in peace!)
I admit, I am young enough to have “grown up” with scalable and serverless options available, and it seems their marketing has worked on me. But I’ve come to the realization that I don’t actually need all that for my side-projects.
So if @levelsio can host all his sites on a single VPS, I can host my side-projects on one too, for like $5-10 USD/month. I won’t get nearly as much traffic as he does, so it should be a breeze.
Hell, I could probably host all my side-projects on a single Raspberry Pi and still have plenty of resources to spare. After all, current tech is pretty powerful, and it takes quite a few concurrent users to max out a single server.
So… how do we actually do this?
Self-hosting with one server?
Before I really started learning about the topic, I had a lot of questions.
- How?
- What about CI/CD?
- What about backups?
- What about scaling?
- Also, how?
I know how to run projects on my local machine, I can run Docker images, and I can set up Kubernetes clusters on GCP. Still, I have little experience with VPS’s and self-hosting, so I still felt a bit lost at first.
Like, how do you run multiple projects at once, and properly route them to different domains? Do you need multiple public IPs on a VPS for this? How do you handle SSL certificates? How can you set up CI/CD for your projects?
And what about scaling?? I looked into this, and turns out, you can handle a lot of traffic with a single server. Way more than you think, and probably more than your side-project will ever need. It will take a while before I have hundreds of concurrent users on these side-projects. After all, ”build it and they will come” isn’t really a thing.
In practice it’s usually the opposite, and you have to work hard to get people to visit your site. After all, you’re competing for their attention with the entire Internet. Sure, you may get some significant spikes from Product Hunt launches or Reddit posts, but even then, you can handle a lot of traffic with a single server.
Before I found answers to all these questions, I had to figure out how to get a VPS and set up a project on it. I figured I’d learn the rest as I went along. So let’s start with the first step.
Renting a VPS
I’ve previously had a droplet on DigitalOcean, and I’ve been happy with their service. I used it when going through the fantastic Zero to Production in Rust book, and it was easy to set up. Quick to reserve, and easy to manage. Still, I wanted to try something new this time, and find something even cheaper if possible.
I’d heard good things about Hetzner, especially related to their pricing. That said, I had also heard some negative things about some of their IPs being blacklisted. Anyway, it seemed like a good enough option for my use-case.
As an extremely lucky coincidence, while I was signing up to Hetzner, I got a ping in the Coolify Discord server. coolLabs (the company building Coolify) had partnered with Hetzner and offered a discount code to their services.
So I signed up, redeemed the code, and set up one of the cheapest VPS’s on Hetzner. Got it up and running, and checked that I could SSH into it. Once done, I was ready to start hosting my projects. Wait… which projects?
Creating a project
I’ve recently created a nice boilerplate project. It’s a monorepo (pnpm workspace) with multiple SvelteKit app templates, as well as some API templates (one in Python and one in Rust). I created this so that I could rapidly spin up new projects. The boilerplate uses my preferred stack and services, and has been a great success so far.
So I forked it, went through a few config steps, designed some pages, and had a site ready to be hosted within an hour or two. The project in question is community.fyi, a guide to online community building for brands.
Then it was time to figure out how to properly host it on my VPS.
Self-Hosting with Coolify
I initially started looking into how to use Docker and Docker Compose directly, but came over a YouTube video showcasing Coolify. It looked interesting, and fast enough to set up for testing, without overcommitting to it.
There were two options: self-hosting Coolify, or using their managed service. With the first option, it was recommended to have your deployed apps on a separate VPS, as Coolify would take up a few resources.
I decided to go with the managed service, as it seemed like a good deal for the cheap price. And it cost the same or less than a separate VPS to self-host it on would have. From there, it was simple to set up.
Using Coolify
Coolify has a simple dashboard where you can manage your projects, databases, and settings. You can create projects from sources like GitHub, Docker Compose files, or with a few clicks to set up one of the pre-configured templates. Coolify supports lots of different services, like Postgres, Redis, Grafana, PocketBase, Gitea, and more.
You can create different environments, manage proxies, and easily set environment variables for your resources. It’s kind of like Vercel in its simplicity, and easy to get into.
If you use GitHub repositories as a source for your projects, you instantly have built-in CI/CD via the webhook-based deployments (i.e. on push to main). You can also set up your own CI/CD pipeline, but I haven’t had the need for that yet. The built-in one has filled my needs, and rolls out new updates without issues.
If you use Docker, you can also use GitHub Actions to build your images and push them to some registry, and then pull from there in your Coolify project. I decided to use the Coolify CI/CD for building images, as it’s a bit simpler and I don’t have to worry about the registry part.
Hosting a Database
One of the first things I did was to set up an instance of a PostgreSQL database with Coolify. All it took was a few clicks, and my database was up and running on my Hetzner VPS.
This was a great experience, and while it may not be as scalable, it’s certainly a lot cheaper than f.ex. PlanetScale or AWS RDS. I used to like the simplicity of PlanetScale’s serverless PostgreSQL DBs, but it’s become too expensive.
Using Schemas in PostgreSQL is a way to separate data for different projects, while still using the same instance. So now I just specify a schema in Prisma, and I can have multiple projects use the same database without interfering with each other. At least they’re not big enough to interfere with each other yet. I’ll have to see how it scales.
Working with Monorepos on Coolify
It was mostly plug-and-play to get my SvelteKit project up and running with Coolify. However, it took a bit of extra work to get it working with my monorepos (pnpm workspaces). I’ll go through the steps I took to get it working.
Most notably, it was the environment variables that were a bit tricky to get right (in Coolify build v4.0.0-beta.306
,
expecting it to get fixed eventually). But up until that point, everything was smooth sailing.
It was extremely easy to map each project to a domain, as Coolify auto-generates the required Traefik and Caddy configurations. You can edit them, and do some proxy stuff if you want, but for most people, this should be good enough. It was easy to add custom domains, you could set it to redirect from www to non-www, and more.
Let’s take a look at the different build systems I’ve used with Coolify, and how they’ve worked out. Those are Nixpacks, Dockerfiles, and Docker Compose files.
Building SvelteKit Apps with Nixpacks
I’ve only looked briefly at it, but it looks like Nixpacks automatically analyzes the app source, and creates a build plan.
Then, it builds the app as an OCI-compliant image (OCI = Open Container Initiative, a standard for container images).
I believe it generates a Dockerfile
and uses docker build
under the hood.
Before my project had any environment variables, this was super easy to use.
I set the build context directory (recently added by Coolify), and let Nixpacks auto-detect pnpm
and my build commands.
I didn’t really have to do anything, and it just worked out of the box.
So, I worked a bit more on my project. I set the build context to root, as I now used a shared package that requires full monorepo context.
Otherwise, it won’t recognize the package with the workspace:*
field in the package.json
.
I adjusted the build and the run commands slightly. And again, this worked perfectly fine right away.
Passing Env Vars to SvelteKit
It was only when I added an environment variable to the SvelteKit app, that it didn’t quite work as expected.
When SvelteKit can’t find the env var with its virtual:env/static/private
setup, it breaks the build.
So it was easy to see that the build process simply didn’t pick up the environment variables at all.
At first, I noticed that env vars in Coolify have to be toggled to be available during the build process. I did this, and triggered a rebuild by pushing an update (note: not just a redeploy, but a full rebuild to get the env vars).
It still couldn’t find the env vars, and I was a bit confused. Googling and searching in their GitHub repo and Discord server didn’t give me any answers, so I tried tinkering a bit and started looking for alternatives.
Note: this seems to be an issue with SvelteKit and/or Nixpacks. At the time of writing, this possibly-relevant issue is still open. There are also a few other issues that seem related.
Converting to Docker images
While looking for solutions, I decided to whip up a quick Dockerfile and see if I could get it working with that.
I remade the project in Coolify, and set it to use the new Dockerfile
this time. I then set up the environment variables in the Coolify dashboard, and set “Build Variable?” to true
.
I eventually got it to work with the environment variables, but I actually needed both the ARG
, ENV
and a mount command:
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
RUN
echo "DATABASE_URL=$DATABASE_URL" >> .env
I normally don’t have to use both in other projects built with GitHub Actions, so I was a bit confused at first. But it worked, and I was happy to have it up and running. Possibly related issue: coollabsio/coolify#1930
Moving to Docker Compose
Once I had it working with a single Docker image in the project, I wanted to try using a docker-compose.yaml
file.
This way, I could easily run all the services within my monorepo setup in the same Coolify project.
I set up a docker-compose.yaml
file, and added the services I needed. I then set up the environment variables in the Coolify dashboard.
It took a bit of experimenting to get the env vars passed in, but I eventually got it working.
I doubt I have the optimal setup, but here’s an example of one of the services using some env vars:
version: '3.8'
services:
web:
build:
context: ./
dockerfile: ./apps/web/Dockerfile
args:
- DATABASE_URL=$DATABASE_URL
- OAUTH_CLIENT_ID=$OAUTH_CLIENT_ID
- OAUTH_CLIENT_SECRET=$OAUTH_CLIENT_SECRET
ports:
- '3000:3000'
A neat little Docker Compose trick
Okay, not really much of a trick, but I found it neat since I hadn’t used Docker Compose that much yet.
Anyway, let’s say you have the following docker-compose.yaml
file:
version: "3.8"
services:
api:
build:
context: ./
dockerfile: ./apps/api/Dockerfile
ports:
- "8000:8000"
web:
build:
context: ./
dockerfile: ./apps/web/Dockerfile
ports:
- "3000:3000"
Here, I have two services, api
and web
. If you wanted to call an endpoint on the api
service from the web
service,
you would normally have some URL for it. Using http://localhost:8000
wouldn’t work here, since the services are in separate containers.
However, in Docker Compose, you can just use the service name as the URL.
So in this case, I could call the api
service from the web
service with the URL http://api:8000
. Could it be any easier?
This is because Docker Compose sets up a network for the services, and the service names are automatically resolved to the correct IP. Here, we use the default network, but it seems like it can be configured as you wish. Docker Compose was surprisingly easy to work with.
Scaling with Docker Swarm
I have yet to test this myself, but a way to gain more scalability than a single VPS can offer is to use Docker Swarm. Coolify has built-in support for Docker Swarm, so you can easily set up a cluster of servers to handle more traffic.
I will be looking into this in the future, but in the spirit of keeping things simple, I’ll wait until I actually need it.
Hosting Multiple Projects at Once
I expected this to be a tricky step. Especially with routing to different domains. But it was actually the easiest step.
Once a new project was ready to be hosted with it’s own Docker Compose file, all I had to do was to create a new project in Coolify. I set it up like the last one, added env vars, and added the domain I wanted to use, and set the same public IP in the domain’s DNS settings. And it just worked. Straight away. An insane relief, to be honest.
Note: I had to use an unoccupied public port in the Docker Compose, but no other special considerations.
After this, I updated my monorepo boilerplate with the default Dockerfiles and Docker Compose file. This way, I can host new projects right away, and know what to set and adjust in Coolify to get them up and running.
Conclusion
The title “Hosting Multiple Sites on one VPS” is a tad bit misleading, as you technically use two servers (one for the Coolify dashboard and one for your projects). Arguably, the sites themselves are on one VPS though. Additionally, you can in theory self-host Coolify on the same VPS if you really wanted to (though it’s not recommended).
I went with the managed Coolify service, both to support the project and to save myself some time and hassle. I haven’t really seen many projects with the same development speed as Coolify, so I’m happy to support them.
Either way, Coolify makes it extremely simple to self-host multiple projects and databases on one VPS. It has everything I need, with automatic SSL, CI/CD, backups, and a simple dashboard to manage everything. The automatic configuration of Traefik and Caddy has been great to work with, and I’ve had no issues with it so far.
I quickly set up some projects, a database, and now have a smooth and easily-repeatable setup for future projects. And I won’t get any nasty surprise bills (like from Vercel or AWS), as I know exactly what I’m paying every month.
Thanks to Coolify, I can now host all my side-projects on a single VPS, and have them up and running in no time. It will take a while to catch up to @levelsio’s $200K/month, but I’ve got no excuse now.