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
| Tool | SaaS Price/Month | Self-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
- Deploy the SSH gateway — a Go service running
golang.org/x/crypto/sshthat manages sessions, logs activity, and enforces key policies. - Configure session logging — store connection metadata in PostgreSQL (who, when, what commands).
- Set up alert rules — watch for brute-force attempts, unusual login times, and failed authentications.
- Integrate with your vault — CAShell already provides encrypted credential storage.
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
- Metrics Collection: Prometheus (Go) — free, powerful, no per-host fees.
- Log Processing: Loki (Go) — indexes metadata, stores logs cheaply.
- Visualization: Grafana (Go) — dashboards, alerting, reporting.
- Tracing: Jaeger (C++/Go) — distributed request tracing.
- Alert Routing: custom Go service delivering to Telegram, WhatsApp, and Discord webhooks.
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
- Single VPS (4GB RAM, 2 vCPU): $20/month
- All services running: Prometheus + Loki + Grafana + Jaeger + custom alert router
- Zero per-host fees, zero per-log fees, zero surprise invoices
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
- Schedule Manager — iCal-compatible calendar for on-call rotations (daily/weekly).
- Escalation Engine — if no acknowledgment in 5 minutes, escalate to the secondary responder.
- Notification Dispatcher — concurrent Telegram, WhatsApp, email, and SMS delivery.
- Status Page — public-facing page showing the current on-call engineer and incident status.
- 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
| Item | PagerDuty | Self-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
- Server Health Panel — CPU, memory, disk, and network from Prometheus metrics.
- Deployment Status — current version, last deploy time, health checks.
- Alert Feed — real-time incident stream from your alert router.
- On-Call Widget — who is on call, upcoming rotation, escalation path.
- 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)
- Inventory all current SaaS subscriptions and their costs.
- Identify which features you actually use versus what you pay for.
- Calculate annual spend and potential savings.
- Document data retention requirements and compliance needs.
- Map integrations between tools (Slack, GitHub, and so on).
Phase 2: Infrastructure Setup (Week 2)
- Provision a VPS (recommended: Hetzner CX22 or DigitalOcean 4GB).
- Set up Tailscale for secure remote access.
- Configure Coolify for application deployment.
- Deploy PostgreSQL for data persistence.
- Set up Let's Encrypt SSL certificates.
Phase 3: Tool Replacement (Weeks 3-4)
- SSH: deploy the self-hosted SSH gateway with session logging.
- Monitoring: install the Prometheus + Grafana stack.
- Logging: deploy Loki for log aggregation.
- Tracing: install Jaeger for distributed tracing.
- On-Call: deploy the custom on-call service with Telegram integration.
- Alerting: configure the alert router with multi-channel delivery.
Phase 4: Validation (Week 5)
- Run parallel tests: SaaS plus self-hosted for seven days.
- Verify alert delivery on Telegram, WhatsApp, and email.
- Test escalation paths end to end.
- Validate log retention and search functionality.
- Measure performance: response time, uptime, accuracy.
- Document runbooks for each component.
Phase 5: Cutover (Week 6)
- Cancel SaaS subscriptions but retain access for 30 days.
- Redirect all alert routing to the new system.
- Update documentation and runbooks.
- Train the team on the new workflows.
- Monitor closely for two weeks.
- Decommission SaaS access after validation.
Ongoing Maintenance
Self-hosted does not mean zero maintenance. Budget 2-4 hours per week for:
- Updates — apply security patches to all services.
- Backups — daily PostgreSQL dumps, weekly full system backup.
- Monitoring — watch your monitoring (use Uptime Kuma).
- Log rotation — configure retention policies to prevent a full disk.
- 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)
- PagerDuty: $447/month
- Datadog: $2,800/month
- Termius Business: $150/month
- New Relic: $500/month
- Total: $3,897/month ($46,764/year)
After (Month 13)
- VPS (4GB): $20/month
- Custom alert router: $0 (Go)
- Prometheus stack: $0 (Docker)
- Self-hosted SSH gateway: $0 (Go)
- Total: $20/month ($240/year)
Annual savings: $46,524
Additional benefits:
- Zero data leaves the infrastructure.
- Custom alert rules specific to their business.
- Team trained in operational tooling, not just clicking buttons.
- Can extend the platform for proprietary needs.
Getting Started Today
Prerequisites
- Basic Linux administration skills.
- Docker and Docker Compose knowledge.
- Familiarity with Go, or the willingness to learn.
- One VPS (recommend starting with 4GB RAM).
First Week Checklist
- Install Ubuntu 24.04 LTS on the VPS.
- Set up Docker and Docker Compose.
- Deploy the Prometheus stack via Coolify.
- Configure a Telegram bot for alerts.
- Deploy the self-hosted SSH gateway.
- Create a basic Grafana dashboard.
- Test alert routing end to end.
Resources
- CAShell Documentation — github.com/cecepazhar/cashell
- Coolify Documentation — coolify.io/docs
- Prometheus Getting Started — prometheus.io/docs/introduction/first_steps/
- Grafana Dashboards — grafana.com/grafana/dashboards/
- Go SSH Package — pkg.go.dev/golang.org/x/crypto/ssh
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.