REVIEW: Coolify - self-hostable PaaS
Deep dive into a Heroku and Netlify alternative that runs on your infrastructure of choice
https://github.com/coollabsio/coolify: v3.12.32 dated May 25th 2023.
Coolify is a self-hostable platform-as-a-service (PaaS) for deploying databases and applications. The product tries to offer functionality similar to Heroku and Netlify, but makes it available in your infrastructure of choice.
The system defines three main categories of software that can be deployed: databases, applications and services.
The database category is bundled with a curated set of popular database solutions, among which are MongoDB, MariaDB, MySQL, PostgreSQL, CouchDB, EdgeDB and Redis.
And the service category comes with another set of widely-used solutions, such as WordPress, MinIO, VSCode Server and many others.
Finally, the third category, applications, covers custom-built software distributions that users might want to deploy and run alongside the software from the aforementioned categories. Coolify is bundled with a number of so-called build packs that facilitate build processes for various environments and runtimes, e.g. NodeJS, Python and Rust etc. What this mean in practice is that developers can relatively easily build and package their software for further deployment. The notion of a build pack is originally nailed by Heroku and now is widely used by popular PaaS and generalized into CNCF Buildpacks and the corresponding tooling. Luckily Coolify is no exception and supports the Heroku-maintained build packs out of the box.
Now, an important question: where do all those run exactly? The product in its current form heavily relies on Docker Engine, both in terms of self-hosting and deployments. The system’s control plane requires a machine with a local Docker Engine where its components run. The deployments can be run on the same local Docker Engine or on any remote Docker Engine configured in the control plane. Coolify’s marketing collateral also mentions a work-in-progress Kubernetes integration. And you might think that the mentioned Docker Engine architecture slightly reminds Kubernetes to some extent. But as far as the code goes, the Kubernetes support seems to be a pretty distant place to reach, and later in the post you will see why.
The primary ways to interact with a Coolify setup is via its Web UI and the underlying API.
Before diving into Coolify’s codebase, let’s get back to the three software categories introduced before. The separation between databases and services might not seem ideal from the product perspective as the service category ends up being “everything but databases”. From the technical perspective there might have been reasons for this separation, but it also might have been the case that some internal design considerations significantly influenced the overall product design, unnecessarily. Abstractly speaking, there isn’t much of a difference between PostgreSQL and MinIO: they both rely on block storage and both listen to TCP ports. And custom-built applications can be perceived similarly. Providing a generic abstraction that covers all sorts of deployments could potentially simplify UX, allow more versatile integrations across the board and offer a flexible open architecture.
Codebase
Coolify’s server and web components are written in TypeScript. The server utilizes the asynchronous concurrency model.
Almost all metadata the server manages is stored in a SQLite embedded database, available via Prisma, a popular TypeScript ORM library. Reliance on an embedded database might not be ideal as this introduces availability and maintenance concerns:
It won’t be possible to run multiple replicas of the server.
It might be somewhat challenging to move the server and its database if such a need arises.
Splitting responsibilities and concerns gives additional flexibility. This is why some relatively new prominent open-source systems choose to keep coordination and storage components separate, e.g. Pulsar, KeyDB, JunoDB, some of which may rely on RocksDB for persistent storage.
Luckily Prisma supports other database providers and it might be just a matter of exposing additional configuration to keep the metadata in a remote RDBMs.
apps/api/prisma/schema.prisma#L1
The RESTful Web API is exposed using Fastify, a popular web framework for Node.JS. Let’s see how the server’s initialization code looks like.
apps/api/src/index.ts#L53
Above, you might think that the “raw” access to the ORM collection in such a spot is rather exceptional and we can expect more organized logic downstream, but generally such invocations are all over the place in the codebase. There’s simply no centralized layer dedicated to managing queries to the underlying database. Many obscure and complex queries are scattered across the modules. This instantly results in higher codebase maintenance costs.
$ rg prisma.setting -c
apps/api/src/index.ts:9
apps/api/src/lib.ts:2
apps/api/src/jobs/deployApplication.ts:1
apps/api/src/lib/common.ts:5
apps/api/src/routes/webhooks/traefik/handlers.ts:2
apps/api/src/routes/webhooks/github/handlers.ts:1
apps/api/src/routes/webhooks/gitlab/handlers.ts:1
apps/api/src/routes/api/v1/handlers.ts:4
apps/api/src/routes/api/v1/services/handlers.ts:1
apps/api/src/routes/api/v1/sources/handlers.ts:1
apps/api/src/routes/api/v1/settings/handlers.ts:5
apps/api/src/routes/api/v1/applications/handlers.ts:2
apps/api/prisma/seed.js:4
And this is only one model.
$ rg "model " apps/api/prisma/schema.prisma -c
42
Now we can imagine complexity of managing 42 models using the same approach. Certainly no fun. Please keep this in mind as we will soon discover even more insights about maintenance costs here.