web / hosting

I have hosted my database-backed web apps three ways: a PaaS from 2009 to 2023, a second PaaS since, and virtual machines for the workload that outgrew both.

Heroku

From 2009 to 2023 I deployed to Heroku. The workflow was:

Performance and reliability were good. Its GitHub integration broke in April 2022 and took over a month to resolve, so I lost confidence in the platform. Heroku also lacked HTTP/2, and restricting its Postgres from the public internet meant a large cost increase.

Render

To compare replacements I built croaky/webstack, a set of PaaS prototypes. Each stack served a health-check endpoint that ran SELECT 1 and returned {"status":"ok"}, with a lightweight router, a SQL driver without an ORM, and a connection pool:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/jackc/pgx/v4/pgxpool"
)

func main() {
	// env
	port, ok := os.LookupEnv("PORT")
	if !ok {
		port = "8080"
	}
	dbUrl, ok := os.LookupEnv("DATABASE_URL")
	if !ok {
		dbUrl = "postgres:///webstack_dev"
	}

	// db
	db, err := pgxpool.Connect(context.Background(), dbUrl)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// routes
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		var col int
		db.QueryRow(r.Context(), "SELECT 1").Scan(&col)
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, "{\"status\":\"ok\"}")
	})

	// listen
	log.Println("Listening at http://localhost:" + port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

I set up API checks in Checkly from Northern California and London, where my users are.

I chose Render with Crunchy Bridge Postgres, the smallest step from Heroku. What I liked about Render:

Crunchy Bridge fits because I am all-in on Postgres, for transactions and for job queues. I deploy with a small Go CLI: cmd / deploy.

Virtual machines

In 2026 I moved cibot to virtual machines on Ubicloud.

cibot's cost was almost all workers running test suites. A PaaS bills a premium for managed convenience, and its scaling model fought my workload: a persistent disk keeps caches warm across runs, but the PaaS does not scale servers that have disks. I could only scale up to a bigger box, never out to more of them.

Ubicloud gave me roughly twice the CPU and RAM for about a third less than the PaaS charged. A VM per box gives warm local caches and horizontal scale together. Each cibot workers box owns its NVMe disk and registers with the cibot farmer over HTTP.

Pets, not cattle

I run a small number of long-lived VMs and provision them directly instead of baking an image. A shell script installs the toolchain and a local Postgres for tests, tuned for the box size. The script is idempotent, so re-running it on a fresh VM is the whole setup.

The application is a static Go binary. I cross-compile on my laptop and copy it up:

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /tmp/cibot ./cmd/cibot
scp /tmp/cibot cibot-workers:/tmp/cibot

No Docker and no registry. See unix / ssh for the host aliases that make scp cibot-workers: work.

Running the processes

A PaaS starts application processes and restarts them after crashes. On a VM, systemd does that job. I run the web process and the background workers as units, and read their output with journalctl.

Networking

A PaaS gives a private network between services for free. On Ubicloud I put the VMs on one private subnet, so the farmer and workers talk over private addresses with no public exposure and no egress cost.

The workers reach the farmer at its private address (its FARMER_URL is http://10.x.x.x:1994, not the public IP), so that traffic never leaves the subnet. Only the dashboard and the git transport are public, behind Caddy for TLS.

I lock the provider firewall to least privilege: SSH plus the public web ports from anywhere, and everything else only from the private subnet. Nothing internal is reachable from the internet.

Managed Postgres

cibot's database runs on Ubicloud's managed Postgres, with backups and a CA I verify against. The throwaway test databases stay local on each worker box, where speed matters and durability does not.

← All articles