Skip to main content

Push-Based GitOps for Staging (Test) Environments

Metadata

ID: RA-001-GITOPS-LITE
DOI: DOI
Status: Stable / Production-Proven
Author: Ivan Baha (ORCID: 0009-0005-7024-7724)
Published Date: 2026-02-17
Last Updated: 2026-02-17
Tags: #devops #docker #gitops #cost-optimization #cicd

1. Executive Summary

This architecture defines a lightweight, cost-effective Continuous Deployment (CD) pipeline designed for Dev/Test/Staging environments in small-to-medium projects.

It achieves the reliability of GitOps (versioned infrastructure state, automated reconciliation) without the complexity or cost overhead of managed Kubernetes clusters. It utilizes GitHub Actions, Docker Compose, and a single Virtual Machine (VM).

2. Applicability Criteria

Use this pattern when

  • Cost Sensitivity: The project budget cannot justify managed control planes (EKS/GKE) (~$70-100/mo, in addition to the main resources cost).
  • Team Size: The engineering team is small (1-5 devs) and lacks dedicated DevOps specialists.
  • Scale: The workload fits within a vertically scaled VM (e.g., CPU and RAM can be increased to meet the requirements) or can be split across a few distinct VMs.
  • Downtime Tolerance: The environment can tolerate brief (<5s) downtime during deployment (acceptable for Dev/Test/Staging).

Do NOT use this pattern when

  • Zero-Downtime is Critical: Mission-critical production systems require rolling updates with no connection drops.
  • Complex Orchestration: You need advanced scheduling, auto-scaling based on CPU/Memory, or service meshes.

3. Architecture Overview

The "GitOps-Lite" Pattern

The core principle is the decoupling of Application Code from Infrastructure State.

  • Application Repositories build artifacts (Docker Images).
  • The Infrastructure Repository defines the desired state (docker-compose.yml).
  • The Handshake: A CI workflow updates the Infrastructure Repo, which triggers the actual deployment.

4. Implementation Details

4.1. The Three-Stage Pipeline

Stage 1: Build & Publish (Source Repo)

  • Trigger: Push (merge) to main.
  • Action: Executes automated quality gates and tests, builds a Docker image tagged with semantic version (e.g., v1.2.0), and pushes to the container registry.
  • Output: Triggers the Update Infrastructure workflow.

Stage 2: Update State (Infrastructure Repo)

This step updates the source of truth. It uses default Linux tools, such as sed or yq, to modify the deployment manifest.

# .github/workflows/update-manifest.yml
name: Update Deployment State
on: [repository_dispatch]

jobs:
update-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Update Docker Compose
run: |
sed -i "s|image: .*/${{ github.event.client_payload.service }}:.*|image: registry.com/${{ github.event.client_payload.service }}:${{ github.event.client_payload.version }}|" docker-compose.yml
- name: Commit Change
run: |
git commit -am "chore(deploy): update ${{ github.event.client_payload.service }} to ${{ github.event.client_payload.version }}"
git push

Stage 3: Surgical Deployment (The VM)

The push to the Infrastructure Repo triggers the final deployment via SSH. Crucially, it uses Smart Routing based on the commit message to restart only the affected service.

#!/bin/bash
# deploy.sh
COMMIT_MSG=$(git log -1 --pretty=%B)

if [[ "$COMMIT_MSG" =~ "service-a" ]]; then
echo "Deploying Service A..."
docker compose pull service-a
docker compose up -d --force-recreate service-a
elif [[ "$COMMIT_MSG" =~ "service-b" ]]; then
echo "Deploying Service B..."
docker compose pull service-b
docker compose up -d --force-recreate service-b
else
echo "Full Redeployment (Fallback)"
docker compose pull
docker compose up -d --remove-orphans
fi

4.2. Secrets Management

  • Method: .env files stored on the VM filesystem, with a naming convention to distinguish configs by service (e.g. .<service-name>.env).
  • Justification: For Test environments, this offers the highest security-to-complexity ratio.
  • Evolution: Can be upgraded to inject secrets via GitHub Actions or fetch from HashiCorp Vault/Bitwarden at runtime.

5. Scalability & Evolution Path

While tailored for a single node, this architecture supports horizontal growth:

High Availability (HA)

  1. Deploy a second identical VM.
  2. Place a Load Balancer (LB) in front of them.
  3. Update the GitHub Action to deploy to VM1 → Health Check → VM2.
  4. Result: Zero-downtime deployments.

Service Splitting

  • Resource-intensive services (e.g., data-processor) can be moved to dedicated VMs.
  • The Infrastructure Repository remains the single source of truth, but the deploy script routes SSH commands to the appropriate server based on the service name.

6. Trade-off Analysis

FeatureBenefitDrawback
CostExtremely Low (Free Tier + cheap VM)N/A
ComplexityLow (Standard Docker/Bash)Manual setup of VM is required initially
ObservabilityGit History acts as an Audit LogNo built-in dashboard (unlike ArgoCD)
ReliabilityAtomic updates via GitSingle Point of Failure (if using 1 VM)

7. Conclusion

This architecture provides a pragmatic "GitOps-Lite" foundation. It allows engineering teams to focus on product velocity in the early stages while maintaining strict infrastructure discipline. It effectively bridges the gap between manual deployment scripts and enterprise-grade Kubernetes.