DEV Community

Indra Gunanda
Indra Gunanda

Posted on Originally published at ciptadusa.com

Our DevOps Playbook: How a Small Team Ships and Operates 40+ Client Products

Our DevOps Playbook: How a Small Team Ships and Operates 40+ Client Products

Agencies have a reputation problem. The stereotype is a team of developers who build something for six months, hand over a tarball, and disappear. We run the opposite model at Cipta Dusa: small team, dozens of concurrent client products, delivery measured in days — not quarters.

The only way that works is a serious DevOps playbook. Not because enterprise tooling is aspirational, but because automation is the only thing standing between us and chaos. This is the playbook we actually run, the decisions behind it, and the failures that shaped it.

The Constraint: Small Team, Many Products

We typically run 10–15 active client engagements at once: company profile sites, internal dashboards, SaaS backends, AI chatbots, WhatsApp CRM implementations. The team is intentionally small. That means every minute spent on manual deploys, environment drift, or "works on my machine" is a minute stolen from building.

So we made one system-level decision early: every project gets the same skeleton. Opinionated defaults, not bespoke setups. A new client project starts from an internal template repository, not from a blank git init. Customization happens after the pipeline is working, never before.

That single choice removes an entire class of problems. When every project shares the same CI workflows, the same deployment targets, and the same logging contract, the knowledge from one project transfers directly to the next. Onboarding a new developer takes days, not months.

The CI/CD Pipeline: Trunk-Based and Boring

We use GitHub Actions for everything. Not because it's exciting — because it's boring, reliable, and free for our scale.

The pipeline for every project looks like this:

name: ci
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm test

  deploy-staging:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: ./scripts/deploy.sh staging
        env:
          DOCKER_REGISTRY: ${{ secrets.REGISTRY_TOKEN }}

  deploy-production:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: [test, deploy-staging]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: ./scripts/deploy.sh production
        env:
          DOCKER_REGISTRY: ${{ secrets.REGISTRY_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Key decisions embedded in that file:

  • Trunk-based development. Short-lived branches, merge to main, deploy automatically. We don't do git-flow ceremony. For our team size, release branches are overhead, not safety.
  • Small scripts over platform magic. deploy.sh is a plain shell script that builds, pushes the image, and runs the rollout. Anyone on the team can read it in one sitting. No opaque "deploy via wizard" workflow.
  • Staging deploy is mandatory. Production deploy depends on staging having succeeded. If staging is red, nothing ships.

Containers: Local Parity Is Non-Negotiable

Every backend project ships with a Dockerfile and a docker-compose.yml that runs the full stack locally: app, database, redis, and any queue worker. The goal is that docker compose up on a fresh machine produces the same environment as staging and production.

This kills the most expensive failure mode in small teams: "it worked on my machine." If a developer's local environment matches production bit-for-bit at the dependency level, then most environment bugs surface during development, not after a deploy at 11 PM.

We also push version pinning seriously. Base images are pinned to a digest, not a tag. Dependencies are locked (package-lock.json, go.sum, uv.lock). Package upgrades are deliberate events with their own review, never incidental side effects of a feature branch.

Hosting: Match the Platform to the Job

We're unapologetically pragmatic about hosting:

  • Static marketing sites → Cloudflare Pages or Workers. Global edge, free SSL, instant rollbacks. There is no reason a company profile site should ever need a server.
  • Web apps and APIs → Docker on cloud VMs (Hetzner or DigitalOcean). We pick them deliberately for Southeast Asian latency, and the VM gives us full control without committing a client to a specific cloud's lock-in.
  • Managed databases → We default to managed Postgres. Yes, it costs a little more than self-hosting. It also means backups, failover, and point-in-time recovery are someone else's 3 AM problem.

The rule: use managed services for state, and keep compute disposable. If a VM dies, the deploy script should be able to recreate it in minutes. If the database dies, that's a real incident — so it gets real guarantees.

Environments, Migrations, and Zero-Downtime Deploys

Every project has at least preview → staging → production.

  • Preview: spun up per pull request, so the client can click a link and comment on the actual feature before merge.
  • Staging: mirrors production config, seeded with anonymized data.
  • Production: the real thing, with blue-green rollouts for anything that serves traffic.

Database migrations run as a separate step before the new code is live, never during. We learned the hard way that running migrations after rollout races the migration against live traffic — and losing that race means 500s for real customers.

Zero-downtime is the default, not a feature request. If a client asks "will there be downtime during the upgrade?" the honest answer for us is "there shouldn't be."

Observability: Logs, Errors, and Alerts That Actually Page Someone

You can't operate 40+ products by logging into 40 dashboards. So we standardize:

  • Centralized logging — every service streams structured logs (JSON, not free-text) to one place.
  • Error tracking — unhandled exceptions report themselves with stack traces and the exact deploy version that introduced them.
  • Uptime checks — synthetic health checks hit every production URL on an interval.
  • Metrics — response percentiles, error rates, queue depth per service.

Then the important part: alerts go to a single Telegram channel. Not 40 separate notification systems. One channel that the whole team watches. A p95 crossing 1 second or an error rate spiking gets a message within a minute, and whoever is free picks it up.

The probe rules are deliberately coarse. Alert on customer-visible degradation (latency, errors, downtime), not on every minor anomaly. Alert fatigue is real, and it's the fastest way to make your team ignore the system.

Security Without a Security Team

We don't have a dedicated security engineer. So security has to be a property of the pipeline, not a person:

  • Secrets live in the CI secret store, never in repositories. No .env files committed, ever — and a pre-commit hook refuses to stage them.
  • Dependency scanning runs on every PR. Known-vulnerability alerts block merges.
  • Least privilege by default — deploy credentials can only do their one job, and tokens rotate on a schedule.
  • Basic runtime hygiene — non-root containers, read-only filesystems where possible, HTTPS everywhere, security headers as part of the template.

None of this is fancy. It's the boring version of security that actually gets done because it's built into the workflow instead of being a quarterly checklist.

What Broke (So You Don't Have to)

Three failures shaped this playbook:

1. The un-rollbackable deploy. Early on, a deploy to production carried no rollback story. When it went wrong, we fixed forward — under pressure, at night. Now every deploy tags the image with a version, and rollback is a single command that swaps the tag. Fixed forward is the exception; rollback is the plan.

2. Config drift between environment and local. We worked on one project where staging and production had different environment variables for weeks. It worked in staging, broke in production, and took a whole afternoon to trace. The fix: a checklist in the template that diffs environment files across environments during deploy.

3. Alerting on everything. Our first monitoring setup alerted on every blip. Within a month the team had muted it entirely. We rebuilt it around the three signals that actually matter — latency, errors, uptime — and the channel became useful again.

Why This Matters for Clients

Our clients don't buy DevOps. They buy outcomes: the product is live, fast, and doesn't fall over.

With this playbook, a company profile site ships in two days (yes, really — it's our core offer at ciptadusa.com), a custom web app ships in weeks, and every one of them keeps getting deployments and monitoring after launch. When a client asks for a new feature six months later, we don't have to relearn the project — the pipeline, the environments, and the runbooks are all still warm.

It also means the team can be small. Automation substitutes for headcount, which keeps prices honest for startups and SMEs that can't afford a 10-person engineering org. That's the whole point of Cipta Dusa: production-grade engineering, delivered at the speed and budget a fast-moving company actually needs.

The Takeaway

If you're building or running client products, you don't need a platform team. You need:

  1. One opinionated skeleton for every project — same CI, same scripts, same logging contract.
  2. Boring CI/CD — trunk-based, small readable scripts, staging before production.
  3. Managed state, disposable compute — never self-host the database; make the app servers replaceable.
  4. Rollback as the default plan, not emergencies.
  5. One alerting channel that only fires on customer-visible problems.

We've run this playbook across 40+ products and it holds up. The tooling changes, but the discipline stays. If you want to see what it produces, we're building new stuff every week at Cipta Dusa.


Built by Cipta Dusa — software development for teams that move fast.

Top comments (0)