What does it take today to build a cloud platform?#

I’ve spent a good part of my career using the best cloud services available. I’ve always appreciated their convenience, efficiency, comprehensiveness, and power—but above all, the user experience. Behind those few simple steps—configuring a scaling group, managing a private network or a firewall, creating a managed database with a single click—lies a tremendous amount of work that we take for granted.

While I’ve used hyperscaler services professionally, for various self-funded personal and entrepreneurial projects I’ve opted for VPS or dedicated servers, following a path many have taken: deploying via FTP, then using Docker containers, and finally Docker Compose to manage an application that was starting to grow more complex. A database, a backend, a frontend, a Redis cache. At this stage, it was already possible to manage replicas of API components. Even as projects became more complex—with microservices, different types of databases, and queue management systems—I was still able to manage them with Compose.

I reached a limit when I had to manage multiple projects simultaneously across different servers. I felt the need for something less cumbersome and turned to Kubernetes, specifically the K3s distribution, which already included most of what I needed. I can say it was love at first sight: it was the right product at the right time. For the workflow I was used to, a UI would have been helpful as well, and the natural next step was Rancher.

At this point, someone might say: there you go, you’ve reached the peak; you have a solid infrastructure for your use cases. And in fact, that was true—except for a few aspects. Provisioning virtual machines and adding nodes to clusters on providers without pre-existing integration required further automation. This made it more cumbersome to add resources and activate autoscaling mechanisms when needed.

However, it wasn’t unmanageable: this configuration allowed me to host various projects on a sort of unified platform.

Meanwhile, a series of events led me to the conclusion that the choice of a cloud provider can no longer be based solely on technical or economic considerations. In my opinion, it’s also a matter of trust. For many companies, the cloud has become an integral part of their operations and holds an essential part of their assets.

Consider, for example, a company that entrusts years of research, projects, and knowledge to the cloud. Even when everything is working smoothly, this question remains: How much freedom does it have to decide where to store that data? If economic conditions, needs, or trust in the provider change, must the ability to choose an alternative remain a real possibility?

The same question, on a different scale, could also be posed to individuals, institutions, and nations. The more our activities depend on digital infrastructure, the more important it is to retain control over the choices that affect them.

That said, I’m the first to be enthusiastic about the cloud services offered by AWS, Azure, and Google Cloud; I’ve used the first two, in particular, most extensively. Yet I’ve felt—and still feel—the need to have a choice without having to compromise the user experience. I dream of a scenario in which, wherever there are one or more servers connected to a network, we can create a cloud platform worthy of the name.

Many companies and services we currently use hold a dominant position, built on investments that are difficult to match and a model based on massive data centers distributed on a global scale.

The real issue here is the search for a credible alternative: is this a need that only I feel, or do others feel it too?

My background and current circumstances lead me to focus on the needs of a developed and prosperous continent like Europe, but also on those of developing countries, where one can tangibly feel the sense of dependence on infrastructure that is not fully controlled or governed.

In Europe, as in other regions of the world, there is a wide array of providers offering excellent hardware resources. The range of services, however, varies greatly: some focus on infrastructure, while others also offer more comprehensive managed services. For my needs, I didn’t always find the operational experience I’d come to expect from hyperscalers with the providers I wanted to use. This required me to integrate and manage various components directly.

One question I honestly asked myself was: Do we really need another giant to offer an alternative? Is it actually feasible? Or do we need a different approach?

The answer I came up with is that a platform that can rely on existing providers is much more feasible. To put it very simply: a cloud platform that uses the resources of providers A, B, and C.

Belongs to the installation Object storage from Provider C C DNS zone from Provider B B Outgoing email from Provider C C Inference from Provider D D Control plane Ubuntu servers you own Flui network A Provider A 3 servers · 5 apps A Provider A private network Flui firewall B DNS through Provider B B Provider B 3 servers · 7 apps B Provider B private network B Firewall at Provider B's edge C Provider C 2 servers · 4 apps C Provider C private network C Firewall at Provider C's edge from the provider supplied by Flui a server Flui may buy when the cluster runs out of room, within your spending limit
One installation across several providers. The control plane runs on servers you own; object storage, DNS, email and inference come from whichever provider offers them. Provider A offers servers only: Flui completes it with its own firewall and composes it with Provider B's DNS.

Clearly, I’m not the first to ask myself this question and come up with this answer. Establishing a technological standard among different parties requires reconciling differing visions and interests. For Flui, I chose not to make the project contingent on providers adopting a common standard.

From my perspective, the platform must build on existing providers, their APIs, and their capabilities. It must speak each provider’s language and provide a common abstraction, without hiding differences or promising capabilities that a provider doesn’t offer.

Of course, it’s easier said than done—or in this case, it involves a lot of commits and a lot of open-source components.

Resource Abstraction#

Flui was born out of all these reflections. The first problem I had to tackle was resource abstraction: the platform must always request a resource in the same way, regardless of the provider—whether it’s a virtual machine, a firewall, a VNET, or something else.

When integrating providers, the scenarios vary widely and must be managed and standardized by a dedicated module of the platform. Some providers may have firewalls, others may not; for some, the VNET might be included, while for others it’s a paid feature; some charge by the hour, others don’t; some use open standards for APIs, while many have their own proprietary APIs. There are many examples: jokingly, we could say there are more differences than similarities.

With every decision I made during the development of Flui, I asked myself if there was an open-source solution available that could efficiently solve the problem at hand. If so, I adopted that solution; if not, I proceeded to develop the required component.

In our case, specifically, I experimented with Terraform/OpenTofu and Pulumi. They weren’t impossible to integrate, but in the tests I ran, I encountered some difficulties:

  • Encapsulating their execution model within actions triggered by Flui’s APIs—tracking their progress and providing feedback to interfaces like the dashboard—required an adaptation that I found cumbersome.
  • Managing the state and individual runs added mechanisms that needed to be coordinated, whereas I needed direct control over the operations.
  • Results and errors had to be remapped into the platform’s model. By using the providers’ APIs directly, this step felt more intuitive to me.
  • For providers lacking adequate integrations, I would still have had to implement communication with their APIs.

I therefore chose to manage the integration within dedicated modules, which share the same abstraction interface but have their own implementation. Flui retains the role of deciding when a resource is needed; these modules allow it to be managed through the provider’s APIs or a common interface such as OpenStack.

This makes it possible to integrate a new provider without modifying Flui’s core, provided its capabilities fall within the existing contract and logic, or with limited modifications when it’s necessary to extend them.

The choice of direct APIs also entails managing waiting times, errors, and partially completed operations. For now, this is the approach I’ve found most suitable; if I find a better model in the future, I may reconsider this choice.

Another principle that emerged during development, related to resource abstraction, is platform-driven completion and, when necessary, composition.

Let’s take a concrete example: firewall management is a fundamental component, but there may be providers that do not have a firewall that can be created via API. In these cases, Flui’s approach is one of completion: a firewall provided by the platform, based on nftables, while maintaining abstraction. The platform therefore exposes the same rule model across interfaces such as the dashboard and CLI. However, the point at which the rules are applied changes: either at the provider’s perimeter or on the individual host. Abstraction keeps management consistent, while the security features depend on the underlying mechanism.

At the provider's edge where the provider has one Provider firewall server app app app server app app app On each machine elsewhere, and on your own servers server Flui firewall app app app server Flui firewall app app app 80, 443 open to all 22 only to who is allowed cluster ports over the Flui network everything else closed
One policy, two places to enforce it: the provider's firewall around the machines, or the Flui firewall inside each one.

There are other scenarios where composition comes into play: for services like DNS and email, where the “complementation” approach is counterproductive, the functionality of another provider is utilized. Here’s an example: Provider A offers only servers, Flui provides the firewall, and Provider B is used for DNS. In this way, Flui is able to combine the best of both providers.

API Key and SSH Model#

During the project’s development, two types of installation were planned: one in which Flui creates and configures the servers via the provider’s APIs, and one in which the user provides existing servers, on which Flui installs the platform via SSH.

As mentioned above, the platform adapts to providers, but to automate provisioning, it requires documented APIs that allow for resource management. To enable automatic elastic provisioning, I’ve also set a cost requirement: hourly billing, so that adding a machine for a temporary need doesn’t incur the cost of an entire month.

If these conditions are not met, Flui continues to function, but the resource provisioning model changes: you’ll need the SSH credentials for a manually created machine, on which Flui installs the platform. In this mode, scaling nodes also requires a manual operation.

From Server to Platform#

When Flui provisions a server, the setup phase begins: it configures the firewall and private network, installs K3s, and sets up the system components necessary for the cluster’s role. Those dedicated to platform management and those intended to run applications have different requirements, as we’ll see shortly. I won’t list all the components here: the point I want to make is that Flui automates the steps needed to transition from individual servers to a cloud platform.

1 Server bought through the API,or yours, over SSH 2 Firewall + network the provider's,or supplied by Flui 3 K3s the Kubernetesruntime 4 System components by role: controlor workload cluster A cloud platform applications, environments, services, deployments: defined by the Flui API dashboard · CLI · MCP
From single servers to a platform: the steps Flui automates, with different system components depending on the cluster's role.

Management encompasses the infrastructure lifecycle: server provisioning, private networks, firewalls, and node configuration are coordinated to build and maintain clusters. Kubernetes distributes workloads across available nodes, while Flui manages capacity by adding or removing servers based on workload demands and user-defined policies.

Flui is a cloud platform that uses K3s as its runtime. I chose K3s for its simplicity, small footprint, and built-in components. The platform’s concepts and behaviors are defined by the Flui API: applications, environments, services, deployments, and many infrastructure operations.

This is where Flui’s distinctive vision takes shape: decisions about how to deploy an application, expose it, secure it, and monitor it become platform behaviors, accessible through dashboards, the CLI, and the MCP. The user describes what they need; Flui translates those requests into operations on providers and the necessary Kubernetes resources. No knowledge of Kubernetes is required to use the platform.

Separating Management and Applications#

A cluster is a set of servers that function as a single environment to run workloads such as applications and background jobs. Kubernetes coordinates these servers, distributes workloads, and maintains applications in their desired state. In Flui, each cluster also includes the network, storage, node provisioning, a scaling group, DNS management, and the core services necessary for its operation—and can start with as few as one server.

In Flui, clusters have two roles: the control cluster hosts the platform itself, while workload clusters host user applications. A single control cluster can manage multiple workload clusters, even across different providers.

Although it is possible to have a single cluster running both platform components and user workloads, in a production environment it is recommended to separate the control cluster from the workload clusters.

The control cluster coordinates installation management but is not part of the standard path of requests to applications. A temporary malfunction of the control cluster renders dashboards, APIs, and central observability unavailable and interrupts operations that depend on it. Workload clusters continue to run applications, provided those applications do not depend on services hosted on the control cluster.

Cross-Provider Topology and Networking#

There are excellent reasons to stay within a single provider and excellent reasons to work across different providers. One of the technological challenges that Flui seeks to address concerns the private network. Providers that offer a VNET allow you to connect servers privately, without routing that traffic over the Internet. Some networks span different regions, while others are limited to a single region. Network isolation and performance are reasons to rely on this option where available.

For clusters created through provider integrations, Flui keeps the nodes of each cluster on the same provider and uses that provider’s private network for Kubernetes traffic between nodes. When a private network is unavailable, Flui connects the nodes via the Flui network, a private virtual network based on WireGuard. This also allows for the management of user-provided servers without a pre-existing private network.

Flui also supports a cross-provider topology: a single installation with a control cluster that manages workloads distributed across different providers. There are several reasons for this:

  • Cost and availability: choosing the provider best suited to each workload based on pricing, machine specifications, and available capacity.
  • Geographic proximity: Run applications close to users, even where the initial provider is not present.
  • Data policies and constraints: placing workloads with providers and in regions that are compatible with the organization’s needs.
  • Resilience: Limiting the impact of an issue at a single provider by implementing recovery or failover strategies where necessary.
  • Gradual migration: Onboard a new provider and progressively transfer applications and data, while maintaining a single management model.

The biggest challenge in this context is secure communication between the control cluster and the workload clusters. Flui Network is also used to address this need. For connections between clusters, the topology is star-shaped: each workload cluster communicates with the control cluster for management operations and to send logs and metrics. Flui Network does not forward traffic between workload clusters.

A Control cluster Provider A Flui network A Provider A 2 servers · 5 apps A Provider A private network Flui firewall B Provider B 2 servers · 4 apps B Provider B private network B Firewall at Provider B's edge Ubuntu servers you own 2 servers · 3 apps Flui network Flui firewall from the provider supplied by Flui a server Flui may buy when the cluster runs out of room, within your spending limit
The control cluster stays on Provider A and also runs workload clusters on Provider B and on servers you own. The cluster on Provider A is reached over the provider's private network, the other two over the Flui network. No traffic flows between workload clusters.

This topology, combined with application and data management capabilities, enables several scenarios:

  • Separating the management location from the application location: the control cluster can remain with Provider A while workloads are deployed wherever it makes sense, without duplicating the platform.
  • Combining cloud and on-premises servers: Manage clusters at different providers and on-premises machines within the same deployment, even without a pre-existing private network between them.
  • Keep management and telemetry flows private: operations on clusters, logs, and metrics travel over encrypted connections, without having to publicly expose telemetry collection endpoints.
  • Separate production and backups: Keep an application on Provider A and store its backups on Provider B.
  • Prepare for a migration: Keep the application running on Provider A until the switchover to Provider B, without changing tools or the management model.

Autoscaling as an Operational and Economic Decision#

From my perspective, a fundamental characteristic of a cloud platform is elasticity: the ability to scale available capacity up or down based on demand. In Flui, I wanted the platform itself to adapt cluster capacity to the needs of workloads, within the limits and with the permissions set by the user.

This requires continuous monitoring of the status of the clusters and workloads. As we’ll see in the next chapter, gathering this information is one of the platform’s cornerstones: it allows us to recognize when additional capacity is needed and when it can be released.

The model starts with scaling groups: minimum, target, and maximum number of nodes; conditions for intervention; and criteria for selecting which machines to purchase. In addition to these elements, there are several choices to make:

  • Separation of decision from action. Flui can detect the need for a node without this resulting in an immediate purchase. If automatic provisioning is enabled and the provider allows it, the process proceeds within the established limits. In manual mode, however, Flui alerts the operator to the need. With servers provided via SSH, the operator must procure and connect the new machine.
  • Spending limits as well. A maximum number of nodes is not enough to define the cost: different machines have different prices. For this reason, automatic provisioning also requires a monthly cap on the estimated cost of the nodes managed by the scaling group. This limit applies to the computing capacity managed by the group, not the provider’s entire bill, which may include storage, traffic, and other services.
  • Machine Availability Management. A specific instance type may be temporarily out of stock. In situations where the configuration allows it, Flui waits for the machine to become available again before proceeding with the purchase.
Machines 1 2 3 4 5 6 7 8 minimum always held target where it rests maximum never past it Money committed each month limit, a monthly figure Urgency — seconds work that cannot be placed buys a machine,within the limits in machines and money Opportunity — hours back toward the target, giving back a machinethat can be emptied; stands down while urgency acts
A scaling group holds the fleet between a minimum and a maximum, within a spending limit. Urgency buys within those limits; opportunity gives capacity back when it is no longer needed.

Observability as a Foundational Part of the Platform#

The control cluster installation command, immediately after installing K3s, proceeds to install the observability stack. I wanted a new installation to be able to show what was happening—both in system components and in applications—without having to defer this to a later stage.

Here, too, I integrated open-source components: VictoriaMetrics for metrics, Loki for logs, vmagent for metric collection, and vmalert for evaluating alert rules.

In addition to the option to use Grafana, Flui integrates the viewing of this data into the relevant dashboard sections: application health, cluster metrics, logs, or incoming traffic to an application. The dashboard, CLI, and MCP all access the same data.

The monitoring stack resides in the control cluster; the workload clusters have only the bare minimum required to collect and send metrics and logs.

push Workload cluster Log shipper on every node Metrics agent, buffers to disk Workload cluster Log shipper on every node Metrics agent, buffers to disk Control cluster receives, never pulls Metrics from every cluster Logs from every node Alert rules read the metrics CLI logs, metrics, traffic Dashboard charts, traffic, logs Agent (MCP) logs, metrics, alerts
Every cluster pushes its metrics and logs to the control cluster, which receives and never pulls. The CLI, the dashboard and an agent read the same data.

The application describes its requirements in flui.yaml#

In an architecture designed to be portable by design, the application plays a central role. Containers offer an excellent starting point: they allow software and its dependencies to be packaged into a reusable format. To run an application, however, additional information is needed that the image alone does not describe: CPU and memory requirements, configurations, persistent storage, network access methods, and scaling conditions.

Essentially, I needed a way to declare these requirements alongside the code, without tying them to the provider or the Kubernetes resources needed to fulfill them. flui.yaml was born out of these needs: a manifest that describes how to run the application, what resources it requires, how it is reached, and what services it needs.

For example, the manifest of a Next.js application, taken from the specification’s examples:

apiVersion: flui.cloud/v1beta1
kind: Application
metadata:
  name: marketing-site
build:
  strategy: dockerfile
  dockerfile: ./Dockerfile
  context: .
deploy:
  port: 3000
  exposure: public
  healthcheck:
    path: /api/health
  resources:
    profile: small
  scaling:
    min: 1
    max: 3
  domain:
    auto: true
    tls: true
    certificateProvider: lets-encrypt
  env:
    NODE_ENV: production
    SESSION_SECRET:
      valueFrom:
        generate: secret
        length: 48
        format: base64url

The specification also resides in a separate project, with its own schema and versioning. Flui implements it, but other tools can implement it as well. The application description remains a user file that can be stored, verified, and versioned alongside the code.

Platform-Managed Databases and Catalog#

The Flui catalog is a collection of applications ready to be installed on the platform. Each entry associates one or more pre-built container images with a flui.yaml manifest, which describes how to run them and what configurations, resources, and volumes they require. It is this combination that makes the software deployable through Flui.

Catalog entries can be standalone applications, ready for use, or building blocks such as databases, caches, and queues, designed to be used by other applications. The catalog may also include stacks composed of multiple components.

For building blocks, this integration offers value that goes beyond the convenience of having something ready to install: it allows the principles of Flui’s portability to be extended to the services on which applications depend.

With databases, in particular, the value is most apparent. I’ve begun integrating features such as:

  • Backup, dump, and restore, and—for already supported engines—point-in-time recovery.
  • Migration between clusters—even across different providers—for supported engines, using replication mechanisms and a write-lock phase to coordinate the transition.
  • Connection pooling, an SQL console, monitoring of active sessions, and automatic connection to applications.

The ability to transfer and restore a database contributes to data portability, but it does not, on its own, equate to a geographic replication system or automatic failover.

These features bring some of the capabilities of managed database services to the platform; configuring these services independently involves a certain degree of complexity. Support is still evolving and varies across engines: the goal is to maintain common management procedures even when the underlying provider changes.

A Consistent Experience Across Dashboards, CLI, and MCP#

The dashboard, CLI, and MCP are the interfaces through which you interact with Flui. They rely on the same API and the same resource model, with the necessary distinctions and adjustments for permissions.

Some specific operations are available only via the dashboard or the CLI, but day-to-day management follows the same model regardless of the interface.

Agent access requires more precise control over credentials. It is possible to issue dedicated tokens, limiting access by functional area, cluster, or individual application, and distinguishing between read and write operations. An agent can thus be granted only the access necessary to perform a specific task, within a limited scope.

The platform also allows for the identification of actions subject to an approval workflow: the agent must obtain the user’s explicit consent or have prior authorization applicable to that action. This control is enforced in the API, alongside standard access controls.

Agent Flui API You 1 Asks to act a write you must allow 2 Opens a request one sentence, lasts 1 h 3 You answer once · always · no 4 Asks again same call: done, or no
An agent's write becomes a request; a person answers; the agent repeats the same call.

In a CLI-initiated installation, the user’s workstation plays a role in the system: it stores the references and credentials needed to operate on the control plane. This behavior is intentional: the management center is the control plane of the user’s own installation, without requiring an external central service operated by Flui to govern the infrastructure. Sensitive information stored locally, such as provider API keys, is protected by an encrypted vault using a master key under the user’s control. The CLI is therefore both the interface that initiates the installation and one of the tools used to manage it.

Choosing, and Choosing Again#

At first, I was looking for a way to replicate the convenience of the cloud services I used professionally on my own servers. While building Flui, that quest evolved into something more: the ability to combine the capabilities of different providers without having to adopt a single provider’s entire ecosystem.

Choosing a cloud provider shouldn’t be a vow made at the altar. A provider may be the right choice today but no longer tomorrow. Or it may be excellent for one part of our architecture, while another better meets other needs.

The cloud is the home of our most intimate secrets and our most important assets. It safeguards what we know how to do, what we’re building, our strengths, and our weaknesses. All of this comes down to a matter of trust, while the needs of people, businesses, and nations change over time. There are rapidly growing countries that will contribute more and more to this cloud economy, each with their own needs and priorities. I don’t believe that a model concentrated in the hands of just a few providers is the only possible way to meet these needs.

I’m working to ensure that freedom of choice is a reality: leveraging existing infrastructure, combining services, migrating applications and data, and revising a decision without having to rethink everything we’ve built on top of it. Changing infrastructure will always involve some work. Flui’s goal is to ensure that this work doesn’t become the reason we’re forced to stay.

Flui is still evolving, but this is the direction in which I want to continue building it: a platform that allows you to choose—and to choose again.


Flui lives at flui.cloud and its code is on GitHub, under the AGPL-3.0. The documentation, with every concept in this piece in depth, is at docs.flui.cloud.

All articles