Self-Hosting Your Company Stack: 8 Honest Lessons From Our First Weeks

Self-Hosting Your Company Stack: 8 Honest Lessons From Our First Weeks

Our SaaS bill sat between €200 and €400 a month. We rebuilt the company stack on open source: 18 services on two small servers, with a third for backups, for about €10 a month in total. The build took one week, the deployment took one day, and moving the data and onboarding the team took a few days.

Eight numbered lessons from self-hosting a company stack, in two columns, with the restore-drill caveat in the footer
The eight lessons, in the order this article covers them.

This is not a victory lap. The interesting part of the project was not the bill. It was the data we had been paying to keep messy, the secrets work that had to come before any container started, and the operational habits we had outsourced to vendors and now had to own. These are the eight lessons we would pass to a team starting the same move. Every number below is ours.

1. Start with the data, not the server

The first real step was an export, not a Docker install. The CRM export from 13 September 2026 held 1,371 contacts, 572 companies, 585 notes, 282 meetings and 14 deals, none of them closed. It also held 508 contacts still marked NEW, meaning nobody had ever contacted them, 445 cleanup candidates, 54 contacts with no company link, and 10 connected mailboxes of which only 3 were active.

The worst finds were outside the CRM fields. Sixteen form submissions from our drone site were sitting in a mail trash where nobody saw them, and four concrete demo or partnership requests had never been answered.

That audit changed the plan. We stopped treating the move as “put the same records in a different database” and started treating it as a cleanup with a migration attached. The lesson: export early, while the old account is still live, and reconcile before you cancel anything. The data you cannot take with you is the real lock-in, and it is usually worse than the pricing page suggests. A self-hosted stack does not fix bad data. It just makes the bad data your responsibility.

2. Secrets are the real project

The stack has 18 services and one rule that decides whether the whole thing is safe: no service gets a secret it does not need, and no secret sits in a file that persists, a compose block or a repository.

We built that with Vault. Each service has its own Vault document, its own read-only policy and its own AppRole, which can log in only from the servers in that service’s group. A Vault Agent per service renders those secrets into a tmpfs directory, memory only, gone on reboot. Each container mounts only its own directory, read-only. There is no .env file anywhere.

The detail we are proudest of is boring: if a secret file is missing, the service fails to start and retries instead of starting with a random key. Fail closed, not fail quietly. The CRM accepts its settings only as environment variables, and Kaneo two of them, so a small wrapper builds those inside the container from the secret files. The values never appear in docker inspect or in the compose file.

The lesson: do the secrets design before the first deployment, not after the second incident. It is also the part with no shortcuts. The first version of a stack can survive a slow wiki. It cannot survive a copied .env file in a repository.

3. Backups are a drill, not a cron job

A backup job that runs is not a backup. Our restic job runs daily, encrypts and deduplicates, keeps 7 daily, 4 weekly and 12 monthly snapshots, and writes to the server’s disk and to a separate backup server. That is the easy half.

The half that matters is verification. Every Sunday a job reads back 20% of the data, and if the latest snapshot contains the Postgres dump, it restores that dump into a throwaway Postgres container with no network and counts the databases and tables. The result is posted to our chat’s report room; failures go to the alert room. That catches the classic failure mode where the job reports success and the archive is unusable.

The part we have not done is the full restore drill: rebuilding the stack from backups on a temporary server. Until that happens, our 4-hour recovery target is a plan, not a measurement, and we say so. The lesson is not “take backups”. It is “automate the verification, schedule the full drill, and write down the gap until you close it”. Two other details save you later: keep a copy of the restic password outside Vault, and never treat a backup on the same disk as protection against losing the server.

4. The main server is a single point of failure

Every application runs on one main server. Vault has its own machine and backups go to a third, but a provider incident, a disk failure or a bad kernel update on the main server still takes the company’s tools down together. We publish no availability figure for this design, because there is no managed SLA behind it. We are the platform team.

That is a conscious trade, not an accident. The Ansible inventory is service-per-host, so moving a service to its own machine is a one-line change. Vault was the first to move: the system that protects the secrets should not share a machine with the services that use them. Backups go to a third server, so losing the main one does not lose the copies.

The lesson: design the split before you need it, and decide which service leaves first while you are calm. The other lesson is to price the human risk. The €10 a month buys hosting. It does not buy someone to watch disk pressure, a failed upgrade, or Vault coming back sealed after a reboot. If a company cannot absorb that attention, the honest answer may be to keep the critical piece managed and self-host the rest.

Two lists. Done: five SaaS accounts cancelled, 18 services on our own servers, Vault on its own server, daily encrypted backups to a separate backup server, weekly restore check. Not done yet: the full restore drill, n8n moved from SQLite to Postgres, the website analytics snippet repointed to our own PostHog
Where the stack stood when we wrote this: what is done, and what is not done yet.

5. Email is the hardest part

Email is not one service. It is a path with a reputation system attached, and most of it lives outside your compose files. We moved the website off the third-party CRM integration on 31 October 2026, and lead forms now deliver through our own mail path. The CRM sends invitations and password resets through a mailbox of its own, and the wiki, the ticket tracker and the chat each need that mailbox password too.

Those passwords live in Vault like everything else, and a placeholder in Vault stops the deployment until the real one is stored. That gate is annoying and correct: a stack that deploys “successfully” with a broken mailer looks fine until the first password reset.

The lesson: treat email as a project, test it from the real domain, and keep one obvious path for form submissions and operational notifications. Delivery depends on domain reputation and the receiving side’s rules, not only on your container being healthy. This is also the area where buying a service can still be the right call, even when you self-host the rest.

6. Adoption beats purity

A self-hosted tool the team will not open is not cheaper. It is a second bill paid in confusion and workarounds.

Two decisions helped. The chat runs on Matrix with Element, and the password manager is Vaultwarden, which works with the official Bitwarden apps. Element and the official Bitwarden apps felt familiar to people who had used Slack and 1Password for years. The team is small, two to five people plus AI agents, and the migration window was a few days, not a few weeks. That was deliberate: onboarding happened while the data was being moved, before the old tools were cancelled.

The lesson: optimise for the tool people will actually use, not for the purity of the stack. Favour clients that are familiar or have a familiar equivalent. Then watch adoption, not service count. If the team keeps opening one old SaaS tab, that is data about the migration, not a discipline problem. The €0 per-seat cost of self-hosting is real, but only for people and agents who use the thing.

7. Document for the next person, or an agent

If the only person who can rebuild the stack is the person who built it, the company has a bus factor of one. That is why the repository is written so a competent stranger, human or AI agent, can redeploy it from scratch using only the documentation. It is a deliberate constraint, not a documentation hobby.

Every step is explicit. Versions are pinned. The bootstrap script is resumable and stops at the secrets a human must supply. Deploying is one command per service. The README explains the order, the manual steps and the failure modes, including the ones that are still open. The repository was initialized on 2 November 2026 and had about 60 commits by 11 November, with roughly 1,900 lines of documentation around 18 services.

The lesson: write the runbook while you build, not after. It forces the hidden manual steps into the open. It also makes a handover to a client a copy of the repository instead of a rewrite, which is exactly what we want to be able to do.

8. Know what not to self-host

The most useful decision in the project was subtractive. Five SaaS accounts are cancelled, and PostHog now runs on a self-hosted instance on our own server. It was usage-based, not per-seat, which is exactly why it belongs in the same story: the bill was never only about seats.

The general rule: do not self-host what you cannot secure, back up and maintain at the level the business needs. That is not a list of products, it is a list of conditions. Compliance-heavy surfaces where a vendor’s certifications are the actual product. Tools where the open-source alternative is immature or has no maintained upgrade path. Anything the team has no appetite to operate, because the operation itself becomes the hidden cost. And areas like email reputation, where the vendor’s real value is the network effect, not the UI.

Four conditions for staying with a vendor: compliance is the product, the open-source option is immature, nobody wants to operate it, and email reputation
When to keep paying a vendor: a list of conditions, not a list of products.

The lesson: subtracting is a design decision. A smaller stack you can defend beats a larger stack you cannot. The point of this project was never that self-hosting is easy. It was that it is doable, one honest service at a time.

If you are weighing the same move

We are happy to share the mistakes in more detail, and this is the kind of infrastructure work we do for clients — the case studies are here. If you are staring at your own SaaS bill and wondering what your version would look like, tell us what your stack is made of. The same goes for the parts we are still figuring out: a project that starts with its weaknesses written down, and its costs on the table, is easier to trust — and easier to hand over when someone else has to run it.

Talk to us about your stack →