A Pragmatic Git-Alternatives Workflow: Structured Worktrees, Containerized Repositories, and Atomic
A Pragmatic Git-Alternatives Workflow: Structured Worktrees, Containerized Repositories, and Atomic Deploys
If you’re working on a multi-repo project or a polyglot codebase, your version-control workflow can quickly outgrow the traditional fork-and-branch model. This guide presents a cohesive, beginner-to-advanced workflow that combines: 1) Git worktrees for parallel feature work, 2) containerized per-feature repositories to isolate changes and dependencies, and 3) atomic, verifiable deploys with lightweight tooling. You’ll learn how to set up, use, and automate this approach with real-world scenarios, including best practices, caveats, and a minimal toolchain you can adopt today.
Overview
- Why consider a multi-repo, containerized workflow
- Core concepts and prerequisites
- Setting up a multi-repo with worktrees and per-feature repos
- Building and testing in isolated containers
- Versioning and releasing with atomic deploys
- CI/CD considerations and lightweight automation
- Practical pitfalls and troubleshooting
- A concrete end-to-end example
1) Why consider a multi-repo, containerized workflow
- Isolation: Different components or services can live in separate repos with their own dependencies and release cadences.
- Traceability: Atomic deploys are easier to roll back when every feature or fix is contained in its own containerized environment.
- Parallel development: Work with multiple worktrees and repositories without polluting the mainline.
- Reproducibility: Containers ensure consistent build/test environments across machines and CI.
2) Core concepts and prerequisites
- Git worktrees: Allow multiple checked-out branches (or commits) in a single repository, enabling parallel exploration without stashing or branching overhead.
- Per-feature repositories: A lightweight pattern where each significant feature or fix gets its own repository (or a monorepo fragment) that can be built into a container image.
- Containers for isolation: Use Docker or Podman to containerize builds, tests, and runtime environments.
- Atomic deploys: Deploy a single, self-contained artifact (e.g., a container image) that can be versioned and rolled back atomically.
- Lightweight tooling: Shell scripts, Makefiles, and a minimal CI/CD pipeline to coordinate across repos and containers.
Prerequisites:
- Git (2.x)
- Docker or Podman
- A central repository (let’s call it “core-project”) containing shared configs, common libraries, or orchestrator scripts
- Basic familiarity with Dockerfile, docker-compose (optional), and a simple CI system (GitHub Actions, GitLab CI, etc.)
3) Setting up a multi-repo with worktrees and per-feature repos
Goal: Work on two features in parallel, each in its own containerized environment, while keeping the core project stable.
-
Step 1: Initialize the core repository
- Create core-project as the central repo containing shared code and deployment orchestration.
- Structure example:
- core-project/
- services/
- service-a/
- service-b/
- infra/
- docs/
- deploy/
- Commit a minimal baseline.
-
Step 2: Create a feature worktree in the core repo
- Use Git worktree to check out a separate branch for a feature without creating a nested clone.
- Commands:
- git worktree add ../core-feature-a feature/a-awesome-feature
- cd ../core-feature-a
- Edit files for feature a
- git commit -m "Start feature A in dedicated worktree"
- Benefits: Independent development, easy switching between features, no dirty working trees in main.
-
Step 3: Create per-feature repositories (optional but powerful)
- Create a minimal per-feature repo that contains only the code and configs for that feature, plus a Dockerfile to build a container image.
- Example:
- feature-a-repo/
- app/
- Dockerfile
- tests/
- This allows independent security reviews, access control, and release cycles per feature.
-
Step 4: Link core and feature repos
- Use submodules or a lightweight manifest to keep track of per-feature repos from the core project.
- Submodules approach:
- In core-project, add: git submodule add services/feature-a
- Commit the submodule reference.
- Manifest approach (a simple YAML):
- core-project/feature-manifest.yml
- features:
- name: feature-a repo: https://example.com/feature-a-repo.git path: services/feature-a
- Tools can parse manifest to orchestrate builds.
-
Step 5: Standardize build definitions
- Each feature repo includes:
- Dockerfile to build the runtime image
- docker-compose.yml (optional) to wire services for local testing
- A small test suite (unit/integration)
- Ensure consistent base images and tooling versions across features to simplify CI.
4) Building and testing in isolated containers
-
Local container workflow
- Build feature container:
- docker build -t feature-a:0.1.0 ./feature-a-repo
- Run tests inside container:
- docker run rm feature-a:0.1.0 pytest tests
- Run the service locally (optional):
- docker run -d name feature-a -p 8080:80 feature-a:0.1.0
- curl http://localhost:8080/health
-
Reproducible environments
- Pin dependencies with exact versions in the feature Dockerfiles (e.g., pip install -r requirements.txt with pinned versions, npm ci).
- Use multi-stage Docker builds to keep final images small.
-
Isolating CI jobs
- Each feature’s CI pipeline builds and tests only its own container image.
- Artifacts produced: container image tags (e.g., registry.example.com/core/feature-a:0.1.0).
-
Local verification script (example in Bash)
- A simple script that builds, tests, and runs health checks:
- #!/bin/bash
- set -euo pipefail
- IMAGE_TAG=${IMAGE_TAG:-feature-a:0.1.0}
- docker build -t $IMAGE_TAG ./feature-a-repo
- docker run rm -d name feature-a -p 8080:80 $IMAGE_TAG
- sleep 2
- if curl -sS http://localhost:8080/health | grep -q "ok"; then echo "Health OK"; else echo "Health check failed"; exit 1; fi
- docker stop feature-a
5) Versioning and releasing with atomic deploys
-
Versioned container images
- Use semantic versioning: A.B.C for each feature release (e.g., feature-a:1.2.0)
- Tag images with build metadata if needed: feature-a:1.2.0+build.123
-
Atomic deploy principle
- Deploy a single, immutable container image that represents the feature’s state.
- Use a small orchestrator that handles production deploys by updating a single image tag or a Kubernetes/Compose service to a new image.
- Rollback is as simple as rolling back to the previous image tag, with health checks ensuring traffic shifts only on success.
-
Deployment examples
- Docker Compose (local or simple edge deployments):
- In a deploy/ directory, maintain docker-compose.yml that uses image: registry/core/feature-a:1.2.0
- docker-compose up -d
- Kubernetes (production-ish):
- Deploy manifests reference the specific image tag
- Use a canary or blue/green pattern by flipping a service's target image tag
- Implement readiness probes to ensure the new version passes health checks before shifting traffic
-
Changelog and release notes
- Maintain a simple CHANGELOG.md per feature or in core-project.
- Include the feature scope, breaking changes, and migration notes for downstream consumers.
6) CI/CD considerations and lightweight automation
-
Central automation flow
- When a feature repo is updated, CI builds a container image and runs tests.
- Upon success, CI publishes the image to a registry with a deterministic tag.
- Core repo’s deployment orchestrator picks up the new image tag and triggers an atomic deploy.
-
Lightweight tooling
- Use a small Makefile or a Python script to coordinate:
- Build: docker build
- Test: docker run tests
- Push: docker push
- Deploy: run a deployment script that updates the running service
-
Example Makefile (high-level)
- IMAGE=registry/core/feature-a
- VERSION=1.2.0
- build:
- docker build -t $(IMAGE):$(VERSION) .
- test:
- docker run rm $(IMAGE):$(VERSION) pytest
- push:
- docker push $(IMAGE):$(VERSION)
- deploy:
- ./deploy/atomic-deploy.sh $(IMAGE):$(VERSION)
-
GitHub Actions (sketch)
- On push to feature-a repo: build, test, push image
- On tag: trigger core-project deployment to update the manifest and trigger rollout
- Use environment protection and approvals for production deployments
7) Practical pitfalls and troubleshooting
- Pitfall: drift between core and feature repos
- Mitigation: enforce a clear contract in a manifest; pin submodule commits; run cross-repo integration tests in CI.
- Pitfall: slow builds due to large base images
- Mitigation: use slim or alpine variants; multi-stage builds; cache dependencies aggressively.
- Pitfall: inconsistent environments
- Mitigation: lock down base images with digests; use same Docker version across environments.
- Pitfall: flaky tests in containers
- Mitigation: run tests with deterministic timeouts; isolate external dependencies; provide local mock services.
8) A concrete end-to-end example
- Scenario: You’re adding a new search feature to a web app, with a focused backend service and a frontend component.
- Step-by-step:
- Create feature-a-repo: backend-search
- Dockerfile builds a Python FastAPI app with pinned dependencies
- tests/ includes unit tests for search logic
- Create core-project and wire feature-a via submodule or manifest
- In core-project, add a small orchestrator script that, when a new feature image is available, updates a Kubernetes Deployment to the new image tag
- CI builds feature-a image: registry.example.com/core/backend-search:1.0.0
- CI runs tests inside container; passes
- Deploy: orchestration updates the Deployment to use backend-search:1.0.0; health checks verify search endpoint
- Rollback: if health checks fail after the update, revert to previous image tag
9) Quick-start checklist
- Decide on whether to use per-feature repos, submodules, or a manifest approach
- Set up an initial core-project with a minimal deploy orchestration
- Create first feature repo with a Dockerfile and tests
- Implement a simple build/test/deploy script or Makefile
- Establish tagging and versioning conventions
- Configure CI to automate builds, tests, and deployments
- Document the workflow for the team and enforce it with checks in CI
Illustration: The workflow in one glance
- Core-project acts as the conductor, hosting shared logic and the orchestrator
- Each feature has its own isolated container image; you can run, test, and verify independently
- Deploys are atomic: the entire feature ships as a single versioned container
- Rollbacks are trivial by reapplying the previous image tag
Would you like me to tailor this workflow to a specific tech stack (e.g., Node + NestJS services, Python microservices, or Go services) and provide concrete code snippets for Dockerfiles, a manifest schema, and a minimal CI pipeline?
-
Rizwan Saleem | https://rizwansaleem.co
Top comments (0)