← Journal
06 Jul 2026 · 11 min read

The minimum viable DevOps stack for a funded startup

You raised seed money. Now someone expects you to ship reliably, answer investor questions about uptime, and not lose customer data at 2am. Here is the actual stack I install for seed-stage startups, what each piece costs, and what I deliberately leave out.

A founder raised 1.2 million in seed funding on a Monday. By Wednesday they were in my inbox. The investor memo said “team needs to demonstrate operational maturity before Series A.” The founder translated that for me: “We need to stop deploying by SSH-ing into a box and running git pull.”

They had two engineers, a Rails app on a single VPS, a Postgres database with no replicas, and a deploy script that was a shell file one of them had written at midnight six months ago. No CI. No staging environment. No monitoring beyond a UptimeRobot check. They were shipping to production by running docker compose up -d on the server, manually, after pulling the latest code. It worked because they were careful. It was going to stop working the moment they hired a third engineer who was not careful.

This is the stage where most startups I work with find me. They have money, they have pressure, and they have infrastructure that was fine for two people and is not fine for a team that needs to grow. Here is the stack I install for them. Not the theoretical stack. Not the conference-talk stack. The one that survives contact with a seed-stage team shipping every day.

The principle: build for the team you will have in six months

The mistake I see most often is founders building infrastructure for the company they want to be, not the company they are. They have four engineers and they install a service mesh. They have fifty users and they set up multi-region failover. They have no on-call rotation and they buy Datadog’s enterprise tier.

The minimum viable stack is built for the team you will have in six months: probably five to eight engineers, shipping multiple times a week, with one person vaguely responsible for infrastructure who is not a dedicated DevOps hire. Everything in the stack has to be operable by that person, not by a specialist.

CI/CD: GitHub Actions and ArgoCD, nothing else

I have used GitLab CI, CircleCI, Jenkins, Buildkite, and Drone. For a seed-stage startup, GitHub Actions is the right choice in 95% of cases. Your code is already on GitHub. The CI is configured in YAML files that live next to your code. The free tier covers a small team. The paid tier is cheap. You do not need to operate a CI server.

The pipeline does three things:

# .github/workflows/deploy.yaml
name: deploy
on:
  push:
    branches: [main]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  update-manifests:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.repository }}-deploy
          token: ${{ secrets.DEPLOY_TOKEN }}
      - name: Update image tag
        run: |
          sed -i "s|image: ghcr.io/.*|image: ghcr.io/${{ github.repository }}:${{ github.sha }}|" manifests/deployment.yaml
          git config user.name "ci"
          git config user.email "ci@yourstartup.com"
          git commit -am "deploy: ${{ github.sha }}"
          git push

The CI builds the image, pushes it to GitHub Container Registry, and updates a manifest in a separate deploy repository. That is it. CI does not touch the cluster. CI does not run kubectl. CI does not deploy.

ArgoCD does the deploying. ArgoCD watches the deploy repository and syncs the cluster to whatever is in Git. If the manifest changes, ArgoCD applies it. If someone manually changes the cluster, ArgoCD reverts it. The deploy repository is the source of truth, and the source of truth is reviewed through pull requests.

This separation matters more than the tool choice. CI builds artifacts. GitOps deploys them. The two concerns are separate because they have different failure modes and different audiences. A broken build is an engineering problem. A broken deploy is a production problem. Mixing them is how startups end up with a CI pipeline that can take down production at 2am because someone merged a config change without review.

What I leave out: Jenkins, GitLab CI if you are not already on GitLab, any CI that requires you to operate infrastructure. If your CI server is down, you cannot ship. GitHub Actions being down is GitHub’s problem, not yours.

The cluster: managed Kubernetes, or a VPS with Docker Compose

I wrote about this in detail before. The short version: if you have one application, one database, and a team under five, a VPS with Docker Compose is honest and sufficient. If you have multiple services, a queue, a cache, and a team that is growing, managed Kubernetes (EKS, GKE, or AKS) earns its keep.

For the seed-stage startup I am describing here, the answer is usually managed Kubernetes. They have raised money, they are hiring, and the complexity of running multiple services on Docker Compose is about to exceed the complexity of running one cluster. The managed control plane costs $70-150 a month depending on the provider. The node pool for a small startup runs $100-300. Total cloud bill for the cluster: under $500 a month, and it scales with usage, not with ambition.

What I leave out: self-hosted Kubernetes (kubeadm, k3s on bare metal) unless you have a specific reason. Managed control planes are one of the best deals in cloud computing. You pay a small premium to never think about etcd backups, API server upgrades, or certificate rotation. That premium is worth it at every stage I have worked with, up to about fifty engineers.

Observability: Prometheus, Grafana, and OpenTelemetry

The startup I described at the beginning had no monitoring. Their answer to “is the system healthy” was to open the app and click around. This is how most seed-stage startups operate, and it is fine until it is not. The moment it is not is usually the first time a customer emails to say the app is down and nobody on the team knows why.

The stack I install is not exotic:

  • Prometheus scraping metrics from your services, your cluster, and your infrastructure. The kube-prometheus-stack Helm chart gives you Prometheus, Grafana, Alertmanager, and node exporters in one install.
  • Grafana for dashboards. One dashboard for the CTO that shows request rate, error rate, and latency. One dashboard for engineers that shows pod status, CPU, memory, and restart counts. One dashboard for the database that shows connections, query latency, and replication lag.
  • OpenTelemetry for traces. OTel collector as a daemon set in the cluster, exporting to Tempo or Jaeger. Traces are the thing nobody asks for until they have an incident that takes three hours to diagnose, and then they cannot imagine working without them.
  • Loki for logs, shipped via Promtail. Not Elasticsearch. Not Splunk. Loki is cheap, simple, and integrates with Grafana. Logs are the highest-volume telemetry signal and the most expensive to store. Loki indexes labels, not content, which keeps the bill predictable.
# values.yaml for kube-prometheus-stack
prometheus:
  prometheusSpec:
    retention: 30d
    storageSpec:
      volumeClaimTemplate:
        spec:
          resources:
            requests:
              storage: 50Gi

grafana:
  dashboards:
    default:
      startup-overview:
        json: |
          # Import the standard overview dashboard
          # Four panels: request rate, error rate, p95 latency, pod status

alertmanager:
  config:
    route:
      receiver: slack
      group_by: ["alertname", "namespace"]
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
    receivers:
      - name: slack
        slack_configs:
          - api_url: $SLACK_WEBHOOK
            channel: "#alerts"

The alerting rules I start with are deliberately small. Five rules, not fifty:

  1. Pod crash-looping for more than 5 minutes.
  2. Error rate above 5% for 2 minutes.
  3. p95 latency above 1 second for 5 minutes.
  4. Database connections above 80% of max.
  5. Node CPU above 90% for 10 minutes.

Every alert goes to Slack, not to a pager. At seed stage, you do not have enough traffic or enough engineers to justify 24/7 on-call. You have business-hours response. If the app goes down at 2am and nobody notices until 8am, that is acceptable for most seed-stage products. It is not acceptable at Series A, but that is a different article.

What I leave out: Datadog, New Relic, and any SaaS observability platform that bills by host or by custom metric. These tools are excellent. They are also expensive in ways that are hard to predict at seed stage. A startup I worked with was paying $4,200 a month for Datadog with twelve hosts. The same coverage on Prometheus and Grafana cost them $80 a month in storage and compute. The tradeoff is that you operate the stack yourself, but “yourself” at this stage is a Helm chart and a persistent volume. It is not a research project.

Security: the baseline that actually blocks attacks

I wrote about this separately, so I will keep it short here. The security baseline I install for every seed-stage startup is:

  • No containers running as root. Enforced by Pod Security Standards, restricted baseline.
  • Network policies on every namespace. Default deny, explicit allow. The database namespace can talk to the application namespace. Nothing else can talk to the database namespace.
  • Secrets in Kubernetes Secrets, encrypted at rest with a KMS key, not in environment variables in plaintext. For anything that touches regulated data, External Secrets Operator backed by AWS Secrets Manager or HashiCorp Vault.
  • Container images scanned in CI with Trivy. Builds that fail the scan fail the pipeline.
  • RBAC scoped per namespace. No cluster-admin for developers. No cluster-admin for anyone except the break-glass account that is audited every time it is used.
# network-policy-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

This baseline takes about a week to install properly. It blocks the attack paths that actually get startups compromised: exposed databases, over-permissioned pods, secrets committed to Git. It does not get you a SOC 2 report. It makes you more secure than most companies I audit, which is the thing that actually matters.

What I leave out: service meshes (Istio, Linkerd) for mTLS. The complexity is not justified at seed stage. Network policies give you segmentation. TLS between your services is nice but it is not the gap that will get you breached. The gap that will get you breached is the database password sitting in a ConfigMap, and the baseline above closes that.

Runbooks: written before the incident, not during it

Every startup I work with has a wiki page titled “Runbooks” that is empty. The first thing I do is fill it with five runbooks, one for each alert above. Each runbook follows the same format: what the alert means, how to confirm it, what to do, who to call, and what not to do.

The “what not to do” section is the one most teams skip. It is the most important one. During an incident, someone will suggest restarting the database. Someone will suggest rolling back the last deploy. Someone will suggest scaling up the nodes. One of those is the right move. The other two will make the incident worse. The runbook tells you which is which, written by someone who has time to think, not by someone who is panicking at 2am.

A runbook for the crash-loop alert:

ALERT: PodCrashLooping
Meaning: A pod in the production namespace is restarting repeatedly.
         The liveness probe is failing or the container is exiting non-zero.

1. Confirm: kubectl get pods -n production --field-selector=status.phase!=Running
   Look for pods with high RESTARTS count.

2. Check logs: kubectl logs -n production <pod-name> --previous
   The --previous flag shows logs from the crashed container, not the current one.

3. If the crash is caused by a bad config or bad deploy:
   - Roll back: git revert in the deploy repo, ArgoCD syncs automatically.
   - Do NOT restart the pod manually. The crash loop will continue.
   - Do NOT scale the deployment to 0 unless you are taking the service offline intentionally.

4. If the crash is caused by an upstream dependency (database, queue, API):
   - Check the dependency's dashboard in Grafana.
   - If the dependency is down, follow its specific runbook.
   - Do NOT roll back the deploy. The deploy is fine. The dependency is not.

5. If you cannot identify the cause in 15 minutes:
   - Page the on-call engineer (currently: the CTO).
   - Post in #incidents with the pod name, the alert, and what you have checked.

That runbook takes twenty minutes to write. It saves an hour the first time someone uses it at 2am. Five runbooks. One hour to write. The first incident they prevent pays for the time.

What I leave out: a full incident management process with severity levels, incident commanders, and formal postmortem templates. That comes at Series A, when you have enough incidents and enough engineers to justify the structure. At seed stage, you need runbooks and a Slack channel. That is it.

What this costs

I keep a spreadsheet for every startup I work with. Here is the monthly cost for the stack above, running on AWS in a single region, for a team of five shipping a single application with a Postgres database and a Redis cache:

ComponentMonthly cost
EKS control plane$73
Two t3.medium nodes$60
RDS Postgres (db.t4g.medium)$80
ElastiCache Redis (cache.t4g.micro)$16
Application Load Balancer$20
NAT Gateway$35
S3 + EBS storage (Prometheus, logs, images)$25
Route53 + CloudWatch$15
GitHub Actions (paid tier)$44
Total$368

Under $400 a month. That buys you a production environment with CI/CD, GitOps, observability, a security baseline, and runbooks. It is not the cheapest possible setup. A VPS stack would cost under $50. But this setup scales to ten engineers and five services without rearchitecting anything. The VPS stack does not.

The number I tell founders to hold in their heads is $500 a month for infrastructure at seed stage. If you are spending more than that and you are not running compute-heavy workloads, you are paying for complexity you have not earned yet.

The thing nobody tells you about the minimum viable stack

The stack above is not difficult to build. A competent engineer with Kubernetes experience could install it in two weeks. The reason most seed-stage startups do not have it is not that it is hard. It is that it is not anyone’s job.

The founder is raising the next round. The engineers are building the product. Nobody is responsible for infrastructure because infrastructure is not yet a full-time role. So the deploy script from six months ago keeps running, the monitoring that does not exist keeps not existing, and the security baseline that would take a week to install keeps not getting installed.

This is the gap. The stack is small. The work is small. But it requires someone who knows what the stack should be, has installed it before, and has the time to do it properly. That person is either a hire you are not ready to make, or someone you bring in for a fixed period to build the foundation and hand it over.

If you are a startup reading this and recognizing the gap between what you have and what you need, Basecamp exists for exactly this reason: digitalaultis.com/basecamp

What changes at Series A

At Series A, the stack above is still the foundation. What changes is the operations around it:

  • On-call becomes formal. PagerDuty or Opsgenie, a real rotation, real SLOs with error budgets.
  • Observability gets deeper. Distributed tracing across services, SLO dashboards that feed into planning, alerting tuned to user impact rather than system metrics.
  • Security moves toward compliance. SOC 2 or ISO 27001, but built on top of the baseline above, not replacing it.
  • The cluster grows. Multiple environments, multiple regions for some teams, autoscaling that is actually tuned.

None of that requires ripping out the seed-stage stack. It requires extending it. The Helm charts stay. The GitOps workflow stays. The dashboards stay. You add on top. That is the point of building the minimum viable stack properly the first time: it is not a throwaway. It is the foundation you keep building on.