DEV Community

Art
Art

Posted on

From IONOS Rage-Quit to Self-Hosted Freedom: How a 120 Euro Bill Improved ForgeCMS

The bill that started it all

IONOS sent me a 120 Euro invoice. Two domains at 42 Euro each - way more than I originally paid for them. Plus some positions I was sure I had cancelled months ago. I paid what I could. They locked every server on my account. No warning. No partial restore. Everything gone overnight.

I cancelled all contracts the next day. They pulled the servers immediately - even though the contracts had months left to run. No grace period. Clean cut.

The support offered me a backup for 70,- EUR...

FU was my answer ;-)

When a provider holds your infrastructure hostage over a billing dispute, you don't negotiate. You migrate.


Green field

I had been planning to self-host on my own 1 Euro VPS using ForgeCMS anyway. The plan was just... not today. IONOS made the decision for me.

This forced exactly the feature I had been postponing: multi-site hosting from a single ForgeCMS binary.


What we built

One Go binary, multiple domains

ForgeCMS is a single Go executable. It receives incoming HTTP requests and routes them to the correct site based on the domain. No virtual machines, no containers, no orchestration layer.

Currently hosting two sites from one process:

  • crowdware.info
  • atesti.crowdware.info

Caddy sits in front as the TLS reverse proxy. That is the entire stack.

Browser -> Caddy (TLS) -> ForgeCMS binary -> Codeberg (page content)
Enter fullscreen mode Exit fullscreen mode

One of the VPS instances also runs our own Postfix mail server. No Google, no Microsoft, no third-party email provider. Full stack independence for 2 Euro per month.

Content lives in Git, not in a database

All page content - written in SML and Markdown - lives in a Codeberg repository. ForgeCMS fetches files from the repo at request time and caches them locally. No build step, no static site generation, no database.

The app.sml config for each domain looks like this:

App {
    name:     "Atesti para Dana"
    base_url: "https://atesti.crowdware.info"
    lang:     "de"

    Site {
        host:   "codeberg.org"
        user:   "CrowdWare"
        repo:   "sites"
        branch: "main"
        path:   "atesti"
    }

    Languages {
        default:   "de"
        supported: "de en es ca eo"
    }

    Menu {
        Item { label: "Home"      href: "/"          }
        Item { label: "Impressum" href: "/impressum" }
    }

    Routing {
        Route { path: "/"           page: "index.sml"     }
        Route { path: "/impressum"  page: "impressum.sml" }
        Route { path: "/404"        page: "404.sml"       }
    }
}
Enter fullscreen mode Exit fullscreen mode

Each domain gets its own folder under config/ in the repo. ForgeCMS reads all of them on startup.

Staging via a second 1 Euro VPS

The staging workflow is dead simple: a second 1 Euro server running the identical setup. Deploy the new version there, test it, then flip the DNS A record. ForgeCMS and Caddy do not need to know anything about staging - it is purely a DNS switch.

# Deploy to staging
./deploy.sh 5.6.7.8

# Verify via /etc/hosts override locally:
# 5.6.7.8  atesti.crowdware.info

# Go live: update DNS A record to 5.6.7.8
Enter fullscreen mode Exit fullscreen mode

The deploy script is fully idempotent - run it again and it installs Caddy, uploads the binary, syncs configs, and starts the systemd service. First deploy or tenth deploy, same command.

New UI components

While we were in there, we added Bootstrap-based components that had been on the list:

  • Image sliders for visual content sections
  • Project cards for listing work and initiatives

Both are driven from Markdown content in the repo - no hardcoded HTML in the binary.

i18n built in

The atesti site serves five languages: German (default), English, Spanish, Catalan, Esperanto. ForgeCMS tries the language-specific file first and falls back to the default.

atesti/
  index.sml        - default (de)
  main.md
  en/
    main.md
  es/
    main.md
  ca/
    main.md
  eo/
    main.md
Enter fullscreen mode Exit fullscreen mode

The architecture in one line

All content is Markdown and SML in a Codeberg repo. One Go binary serves everything. Caddy handles TLS. Total infrastructure cost: 2 Euro per month.


What comes next: load balancer for 1 Euro more

The IP-swap staging approach works but a load balancer in front would make zero-downtime deploys cleaner and faster.

The plan: the second VPS currently runs Postfix for mail tied to the our domains. That Postfix server becomes the load balancer. Total infrastructure cost at that point: 3 Euro per month.


Reference implementation

The full repository structure - configs, deploy script, content layout, and README - is public:

codeberg.org/CrowdWare/sites

If you want to run your own ForgeCMS installation, that repo is the reference. The deploy script handles everything from a bare VPS to a running multi-site setup in one command.

ForgeCMS itself: codeberg.org/CrowdWare/ForgeCMS


Part of the CrowdWare open source ecosystem. Built with Go.


P.S. - The two 1 Euro VPS servers running all of this? Also IONOS. Setup fee for the second one was 10 Euro, then 1 Euro per month. The old hosting contract was 11 Euro per month alone. ROI: month one.

Top comments (0)