You don’t need enterprise GitOps to be professional. You need a history you can trust when a deploy goes wrong at 10pm — and a way to explain what changed when a client asks “what went live last week?”
This is a solo-friendly workflow that still scales to a small team. It is deliberately light: enough structure to reverse a bad change, not so much process that you stop using Git properly on Friday afternoon.
Minimum viable discipline
mainis always deployable — broken main means broken production risk- Feature branches for non-trivial work
- Commit messages that say why, not only “fix” or “updates”
- Never commit secrets — even “temporarily”
- Tag or note production releases when it matters
- One obvious path from “code on my laptop” to “code on the server”
# Start work
git checkout main
git pull
git checkout -b feature/contact-form-validation
# Small, logical commits as you go
git add -p
git commit -m "Validate contact form email and length server-side"
# Merge when staging is green
git checkout main
git merge --no-ff feature/contact-form-validation
Git is a time machine. Leave yourself breadcrumbs.
Commit messages that help humans
Good messages answer: what changed and why should a future reader care?
- Bad:
stuff,wip,final2 - Better:
Fix double form submit on slow mobile networks - Better:
Add CSRF token to invoice payment form - Better:
Reduce hero image weight for mobile LCP
Atomic commits beat giant “weekend of work” dumps when you need to revert one piece. If a commit message needs a paragraph, the commit might be doing too many things — split it.
Optional style that scales well for freelancers:
type: short summary
Optional body — what problem, what approach, any risks.
Examples of type: fix, feat, chore, security, perf. Use them if they help you; skip them if they become bureaucracy.
Branching without ceremony
main— production-readyfeature/…— new workfix/…— production hotfixes (short-lived)chore/…— dependency bumps, tooling (optional)
Delete merged branches. Long-lived branches that diverge for months become archaeology projects. If a feature is large, prefer a sequence of small merges to main (behind a flag if needed) over a three-month branch of doom.
Staging and production
- Develop on a branch
- Deploy branch or merged main to staging
- Smoke-test the real user path
- Merge to main if not already
- Deploy main only to production
Never “quick FTP a single file that isn’t in git”. That file becomes the file nobody can find later. If the host forces FTP, still treat Git as source of truth: change locally, commit, then deploy the known tree.
A simple solo deploy script beats memory:
# Conceptual — adjust to your host
git pull --ff-only origin main
composer install --no-dev --optimize-autoloader
php bin/migrate.php
# reload php-fpm / clear caches as needed
Hotfixes without shame
When production breaks:
- Reproduce and confirm
- Branch from the production commit if needed
- Minimal fix + test
- Deploy, then merge back so main isn’t behind production
Panic overwrites without a commit are how you lose the only good copy. If you used an emergency edit on the server, copy that change back into Git immediately and treat the mismatch as an incident.
.gitignore essentials
- Environment files (
.env, local DB dumps) node_modules/, vendor caches if you rebuild on deploy- IDE junk, OS files, uploaded user content
- Build artefacts if they’re generated in CI
- Personal notes and local Docker overrides with secrets
# snippet
.env
.env.*
!.env.example
/vendor/
/node_modules/
/public/uploads/
*.log
Commit a safe .env.example with dummy values so the next human knows which keys exist.
Secrets and history
If a secret hits git, rotating the secret is step one; scrubbing history is step two (and harder if already pushed). Prevention is cheaper: pre-commit habits, secret scanning, and never pasting keys into tracked config samples.
- Assume anything pushed to a remote may leak someday
- Use deploy keys and tokens with least privilege
- Don’t store production database dumps in the repo “for convenience”
Tags and releases
For client sites, lightweight tags help:
git tag -a v1.4.0 -m "Launch: performance pass + contact form fix"
git push origin v1.4.0
Combine with a short changelog in the project or ticket system. When a client asks what changed in June, you want an answer in minutes, not a forensic scroll through every commit message.
Pull requests when you’re “only one person”
Even solo, a PR (or merge request) is useful:
- Diff review on GitHub/GitLab catches accidental debug dumps
- CI can run lint or tests before merge
- Future collaborators inherit a normal process
Protect main if the host allows it: no direct pushes, or at least no force-pushes. Future you will thank present you.
Common solo failure modes
- Everything on main — one bad afternoon blocks all work
- Never pulling before branching — surprise conflicts at merge time
- Huge binary assets in git — clone times explode; use object storage or LFS carefully
- No staging — production becomes the test environment
- Undocumented deploy — only one brain knows the ritual
A weekly hygiene checklist
- Merge or close stale branches
- Confirm staging still mirrors production PHP/extensions
- Check that last week’s production tag (or note) exists
- Scan for accidental secrets in recent commits
- Update the README if the runbook changed
Worked examples (Git commands)
Copy-paste sequences for common solo situations.
1) Start a feature cleanly
git checkout main
git pull origin main
git checkout -b feature/contact-csrf
# work… small commits
git add -p
git commit -m "Add CSRF protection to contact form"
git push -u origin feature/contact-csrf
# open PR / merge when staging looks good
2) Meaningful commit messages
# Weak
git commit -m "fix"
git commit -m "updates"
# Strong
git commit -m "Fix double submit on slow mobile networks"
git commit -m "Add CSRF token to invoice payment form"
git commit -m "Reduce hero image weight for LCP on mobile"
3) Hotfix without losing main
git checkout main
git pull
git checkout -b fix/checkout-500
# minimal fix + test
git add -p
git commit -m "Fix null shipping method crashing checkout"
git checkout main
git merge --no-ff fix/checkout-500
git push origin main
# deploy main → production
4) Undo safely
# Unstage (keep file changes)
git restore --staged path/to/file.php
# Discard local file changes (careful)
git restore path/to/file.php
# New commit that reverses a bad one already on main
git revert HEAD
git push origin main
5) Tag a release
git tag -a v1.4.0 -m "Launch: performance pass + contact form fix"
git push origin v1.4.0
Need a cleaner deploy path for a client site? I can help →