web / hosting
I host database-backed web applications on Render and on virtual machines.
Render
I run web services on Render with Crunchy Bridge Postgres:
- IP access control on Postgres databases
- DDoS protection
- HTTP/2 and HTTP/3 support
- Zero-downtime deploys via health checks
- SOC 2 certification
Crunchy Bridge provides managed Postgres for transactions and job queues. I deploy with a small Go CLI: cmd / deploy.
Deploy client
cmd / deploy calls the Render API through an internal HTTP client:
type Client struct {
apiKey string
base string
httpClient *http.Client
}
func (c *Client) LiveGitCommit(ctx context.Context, serviceID string) (string, error) {
path := fmt.Sprintf("/services/%s/deploys?limit=5", serviceID)
var deploys []deployListItem
if err := c.get(ctx, path, &deploys); err != nil {
return "", err
}
for _, d := range deploys {
if d.Deploy.Status == "live" {
id := d.Deploy.Commit.ID
if len(id) > 9 {
id = id[:9]
}
return id, nil
}
}
return "", fmt.Errorf("no live deploy for %s", serviceID)
}
func (c *Client) Deploy(ctx context.Context, serviceID, commitID string) (string, error) {
path := fmt.Sprintf("/services/%s/deploys", serviceID)
body := map[string]string{"commitId": commitID}
var d deploy
if err := c.post(ctx, path, body, &d); err != nil {
return "", err
}
return d.ID, nil
}
WaitForDeploy polls the deploy status until it reaches live or fails:
func (c *Client) WaitForDeploy(ctx context.Context, serviceID, deployID string) error {
path := fmt.Sprintf("/services/%s/deploys/%s", serviceID, deployID)
deadline := time.Now().Add(30 * time.Minute)
for {
var d deploy
if err := c.get(ctx, path, &d); err != nil {
return err
}
switch d.Status {
case "live":
return nil
case "build_failed", "update_failed", "pre_deploy_failed", "deactivated", "canceled":
return fmt.Errorf("deploy %s ended in status %q", deployID, d.Status)
}
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for deploy %s", deployID)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(pollInterval):
}
}
}
A failed migration returns pre_deploy_failed, so cmd/deploy stops
before deploying web services. API requests retry through the
backoff helper.
Virtual machines
sockeye runs on virtual machines on Ubicloud.
Test suites account for most resource costs. A PaaS charges a premium for managed servers and restricts horizontal scaling for instances with persistent disks. Ubicloud provides twice the CPU and RAM at lower cost. Each runner owns an NVMe disk for local caches and registers with the server over HTTP.
Provisioning
I provision long-lived virtual machines with a shell script. The script installs the toolchain and local Postgres for tests.
The application compiles to a static Go binary on my laptop:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /tmp/sockeye ./cmd/soc
scp /tmp/sockeye sockeye-runners:/tmp/sockeye
cmd / r2 writes database snapshots and git bundles to object storage daily.
Process management and networking
systemd manages two web processes, a jobs process,
and the runners. I inspect service logs with journalctl.
The server and the runners share one private subnet. Runners connect to the server's private address with its public HTTPS name, so a runner still checks the certificate that Caddy serves (see apps / sockeye). The application server listens on loopback. The cloud firewall accepts SSH from anywhere, web ports only from Cloudflare, and other service ports only from the private subnet.
Test databases run in local Postgres on each runner box.