← Back to Cecep Azhar
Cecep Azhar #DevOps#SaaS Alternative#Self-Hosted#Go#Rust#Cost Reduction#Startup Infrastructure#Observability#On-Call#Automation#Solo Developer Tools#Termius Alternative#Datadog Alternative#PagerDuty Alternative

Replacing Expensive DevOps Tools: Self-Hosted Automation with Go & Rust (2026 Guide)

Cecep Saeful Azhar • 22 Oct 2026
Replacing Expensive DevOps Tools: Self-Hosted Automation with Go & Rust (2026 Guide)
Discover how startups can replace Termius, Datadog, and PagerDuty with self-hosted Go and Rust tools. Slash monthly DevOps cloud bills and keep data safe.

Introduction

Every startup founder has seen the SaaS bill hit. Termius for SSH management. Datadog for observability. PagerDuty for on-call alerts. Together they easily exceed $5,000/month as your team grows. The same infrastructure that runs a Fortune 500 company should not bankrupt a bootstrapped startup.

This guide shows solo developers and small tech teams how to replace these expensive tools with self-hosted alternatives built in Go and Rust. The result: infrastructure costs drop from thousands to under $100/month while retaining full data sovereignty and customization.


Why Self-Hosted Beats SaaS for DevOps

Cost Comparison

ToolSaaS Price/MonthSelf-Hosted Cost/Month
Termius (5 seats)$125$0 (open-source)
Datadog (3 apps)$2,400$80 (VPS)
PagerDuty (team)$600$0 (custom build)
Total$3,125~$100

Data Sovereignty

When you use Datadog or New Relic, your production logs leave your infrastructure. For startups handling sensitive data or operating in regulated industries, this is non-negotiable. Self-hosted means logs stay on your servers. You control retention policies, access controls, and compliance audits.

No Vendor Lock-in

SaaS tools change pricing without notice. They sunset features. They acquire competitors and merge user bases. A self-hosted Go application you own never changes its API on you. You upgrade when you choose.


Replacing Termius: Self-Hosted SSH Management

Termius provides cross-platform SSH management with key sync, command aliases, and session management. Its self-hosted alternative combines CAShell for CLI orchestration with a custom Go SSH gateway.

Architecture

User -> Web Terminal (CAShell/Ratatui) -> SSH Gateway (Go) -> Servers
                                    v
                            Key Manager (Rust crypto)
                            Session Logger (PostgreSQL)
                            Alert Rules (Go cron)

Implementation Steps

Code Example: SSH Session Logger (Go)

package main

import (
    "log"
    "os"
    "time"

    "github.com/cecepazhar/cashell/v2"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type SSHSession struct {
    ID        uint      `gorm:"primaryKey"`
    User      string
    SourceIP  string
    Host      string
    StartedAt time.Time
    EndedAt   time.Time
    Commands  string `gorm:"type:text"`
    ExitCode  int
}

func main() {
    db, err := gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    db.AutoMigrate(&SSHSession{})

    sshConfig := cashell.Config{
        Port:        2222,
        KeyDir:      "/var/lib/cashell/keys",
        LogDB:       db,
        MaxSessions: 50,
    }

    server := cashell.NewServer(sshConfig)
    log.Fatal(server.Start())
}

Replacing Datadog: Lightweight Observability Stack

Datadog charges per host and per log volume. A small team can exceed $3,000/month within weeks. The alternative is a self-hosted stack built with Go services and Rust-based log processors.

The Stack

Deployment via Coolify

All components deploy as Docker containers on a single VPS:

# coolify/docker-compose.yml
version: '3.8'
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./data/prometheus:/prometheus
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    depends_on: [prometheus]
    ports:
      - "3000:3000"

  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"

  alertmanager:
    image: prom/alertmanager:latest
    ports:
      - "9093:9093"

  # Custom alert router (Go)
  alert-router:
    build: ./alert-router
    environment:
      - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
      - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}

Custom Alert Router (Go)

The secret sauce: route alerts to the right channel based on severity and context.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strings"
)

func main() {
    http.HandleFunc("/alert", handleAlert)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleAlert(w http.ResponseWriter, r *http.Request) {
    var payload struct {
        Status string `json:"status"`
        Alerts []struct {
            Labels      map[string]string `json:"labels"`
            Annotations map[string]string `json:"annotations"`
        } `json:"alerts"`
    }

    json.NewDecoder(r.Body).Decode(&payload)

    for _, alert := range payload.Alerts {
        severity := alert.Labels["severity"]
        service := alert.Labels["service"]

        switch severity {
        case "critical":
            sendToTelegram(alert)
            sendToWebhook(service, alert)
        case "warning":
            sendToDiscord(alert)
        default:
            sendToEmail(alert)
        }
    }

    w.WriteHeader(http.StatusOK)
}

func sendToTelegram(alert struct {
    Labels      map[string]string `json:"labels"`
    Annotations map[string]string `json:"annotations"`
}) {
    token := os.Getenv("TELEGRAM_BOT_TOKEN")
    chatID := os.Getenv("TELEGRAM_CHAT_ID")

    message := fmt.Sprintf(
        "CRITICAL Alert\nService: %s\nSeverity: %s\n%s",
        alert.Labels["service"],
        alert.Labels["severity"],
        alert.Annotations["summary"],
    )

    url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
    data := fmt.Sprintf(
        `{"chat_id":"%s","text":"%s","parse_mode":"Markdown"}`,
        chatID, message,
    )

    http.Post(url, "application/json", strings.NewReader(data))
}

Cost Breakdown


Replacing PagerDuty: On-Call Without the Premium

PagerDuty starts at $149/user/month and scales aggressively. For a 3-person team that is $450/month. Build your own on-call rotation with a Go service and Tailscale for secure access.

Self-Hosted On-Call System Architecture

+-------------+     +--------------+     +-------------+
|  Escalation  |---->|  Notification |---->|  Webhook    |
|  Engine (Go) |     |  Dispatcher   |     |  Handlers   |
+-------------+     +--------------+     +-------------+
        |                   |                    |
        v                   v                    v
   +---------+       +----------+        +-----------+
   |Schedule |       |Telegram  |        |  WhatsApp |
   | (iCal)  |       |Webhooks  |        |  Webhooks |
   +---------+       +----------+        +-----------+
                              |
                        +----------+
                        |  Status   |
                        |  Page/On  |
                        |  Call     |
                        +----------+

Core Components

  1. Schedule Manager — iCal-compatible calendar for on-call rotations (daily/weekly).
  2. Escalation Engine — if no acknowledgment in 5 minutes, escalate to the secondary responder.
  3. Notification Dispatcher — concurrent Telegram, WhatsApp, email, and SMS delivery.
  4. Status Page — public-facing page showing the current on-call engineer and incident status.
  5. Incident Log — PostgreSQL table tracking every alert, response, and resolution time.

Go On-Call Service (Simplified)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/gorilla/mux"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type OnCallSchedule struct {
    ID       uint      `gorm:"primaryKey"`
    Engineer string
    Start    time.Time
    End      time.Time
    Rotation string // daily, weekly, biweekly
}

type Incident struct {
    ID             uint      `gorm:"primaryKey"`
    AlertID        string
    Severity       string
    Status         string // pending, acknowledged, resolved
    AssignedTo     string
    CreatedAt      time.Time
    AcknowledgedAt *time.Time
    ResolvedAt     *time.Time
}

func main() {
    db, err := gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    db.AutoMigrate(&OnCallSchedule{}, &Incident{})

    r := mux.NewRouter()
    r.HandleFunc("/api/next-on-call", getNextOnCall(db)).Methods("GET")
    r.HandleFunc("/api/incidents", createIncident(db)).Methods("POST")
    r.HandleFunc("/api/incidents/{id}/ack", ackIncident(db)).Methods("POST")
    r.HandleFunc("/api/incidents/{id}/resolve", resolveIncident(db)).Methods("POST")
    r.HandleFunc("/api/status", getStatusPage(db)).Methods("GET")
    r.HandleFunc("/webhook/telegram", handleTelegramWebhook(db)).Methods("POST")
    r.HandleFunc("/webhook/whatsapp", handleWhatsAppWebhook(db)).Methods("POST")
    r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })

    log.Fatal(http.ListenAndServe(":8080", r))
}

func getNextOnCall(db *gorm.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        now := time.Now()
        var schedule OnCallSchedule

        err := db.Where("start <= ? AND end >= ?", now, now).
            Order("start DESC").
            First(&schedule).Error

        if err != nil {
            w.WriteHeader(http.StatusNotFound)
            w.Write([]byte("no one on call"))
            return
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "engineer": schedule.Engineer,
            "rotation": schedule.Rotation,
            "start":    schedule.Start.Format(time.RFC3339),
        })
    }
}

Telegram Bot Integration

func sendTelegramAlert(botToken, chatID, message string) error {
    url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)

    data := url.Values{}
    data.Set("chat_id", chatID)
    data.Set("text", message)
    data.Set("parse_mode", "HTML")

    resp, err := http.PostForm(url, data)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    return nil
}

Cost Comparison

ItemPagerDutySelf-Hosted
3 engineers$447/month$0
Escalation policies$100/month add-on$0 (built-in)
Status page$200/month$0 (Grafana)
Total$747/month~$20 (VPS)

Building a Unified Operations Dashboard

Instead of logging into five different SaaS dashboards, build a single pane of glass using Go, Grafana, and custom widgets.

Components

  1. Server Health Panel — CPU, memory, disk, and network from Prometheus metrics.
  2. Deployment Status — current version, last deploy time, health checks.
  3. Alert Feed — real-time incident stream from your alert router.
  4. On-Call Widget — who is on call, upcoming rotation, escalation path.
  5. Cost Tracker — monthly spend per service and projected annual cost.

Grafana Dashboard Provisioning

# grafana/provisioning/dashboards/main.yaml
apiVersion: 1

providers:
  - name: 'Operations'
    orgId: 1
    folder: 'Startup Ops'
    type: file
    disableDeletion: false
    editable: true
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: false

Custom Go Widget Server

Build a lightweight Go service that aggregates metrics from multiple sources into a single JSON endpoint for your dashboard:

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type DashboardData struct {
    ServerHealth    map[string]interface{} `json:"server_health"`
    DeployStatus    map[string]interface{} `json:"deploy_status"`
    ActiveIncidents []interface{}          `json:"active_incidents"`
    OnCallEngineer  string                 `json:"on_call_engineer"`
    MonthlyCost     float64                `json:"monthly_cost_usd"`
}

func getDashboardData(db *gorm.DB) DashboardData {
    return DashboardData{
        ServerHealth:    getPrometheusMetrics(),
        DeployStatus:    getDeployStatus(),
        ActiveIncidents: getActiveIncidents(db),
        OnCallEngineer:  getCurrentOnCall(),
        MonthlyCost:     calculateMonthlyCost(),
    }
}

func main() {
    db, _ := gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")), &gorm.Config{})

    http.HandleFunc("/api/dashboard", func(w http.ResponseWriter, r *http.Request) {
        data := getDashboardData(db)
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(data)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Migration Checklist: From SaaS to Self-Hosted

Phase 1: Assessment (Week 1)

Phase 2: Infrastructure Setup (Week 2)

Phase 3: Tool Replacement (Weeks 3-4)

Phase 4: Validation (Week 5)

Phase 5: Cutover (Week 6)


Ongoing Maintenance

Self-hosted does not mean zero maintenance. Budget 2-4 hours per week for:

  1. Updates — apply security patches to all services.
  2. Backups — daily PostgreSQL dumps, weekly full system backup.
  3. Monitoring — watch your monitoring (use Uptime Kuma).
  4. Log rotation — configure retention policies to prevent a full disk.
  5. Review — monthly cost analysis and capacity planning.

Backup Strategy

#!/bin/bash
# backup.sh - Daily backup script

DATE=$(date +%Y%m%d)
BACKUP_DIR="/backups/$DATE"

pg_dump -U postgres gcc > "$BACKUP_DIR/database.sql"

tar czf "$BACKUP_DIR/config.tar.gz" \
    /var/lib/prometheus \
    /var/lib/loki \
    /var/lib/grafana \
    /etc/cashell \
    /etc/alert-router

rclone sync "$BACKUP_DIR" s3:backups/startup-ops-$(hostname)

find /backups -type d -mtime +30 -exec rm -rf {} \;

Real-World Results

Case Study: Jakarta Fintech Startup

Before (Month 12)

After (Month 13)

Annual savings: $46,524

Additional benefits:


Getting Started Today

Prerequisites

First Week Checklist

  1. Install Ubuntu 24.04 LTS on the VPS.
  2. Set up Docker and Docker Compose.
  3. Deploy the Prometheus stack via Coolify.
  4. Configure a Telegram bot for alerts.
  5. Deploy the self-hosted SSH gateway.
  6. Create a basic Grafana dashboard.
  7. Test alert routing end to end.

Resources


Conclusion

The SaaS subscriptions that seem manageable at $50/month each compound into thousands annually. For a bootstrapped startup or solo developer, every dollar counts. Self-hosted alternatives built with Go and Rust provide equivalent or superior functionality at a fraction of the cost, with the added benefits of data sovereignty and customization.

The migration requires upfront investment — roughly two to three weeks of work — but pays for itself within the first month. More importantly, it builds organizational capability. Your team learns to operate production systems, debug Go services, and manage infrastructure. These are skills that scale with your company.

Start with one replacement. Then another. Within 60 days, you will have eliminated $3,000+ in monthly SaaS spend while building infrastructure that is uniquely yours.

← More articles from Cecep Azhar Central Content Hub Home →