Git deployment to a VPS: a simple workflow without a CI/CD platform
TL;DR: For a small project or a solo/small-team workflow, a bare git repository on your VPS combined with a post-receive hook (a script git automatically runs after receiving a push) gets you a genuine push-to-deploy workflow, git push production main and your live site updates, without needing to set up or pay for a separate CI/CD platform. This is not a replacement for a full CI/CD pipeline's testing, staging, and rollback sophistication at genuine team scale, but for a project where that overhead is not yet justified, it is a fast, reliable, and completely self-hosted deployment method.
Why a full CI/CD platform is often more than a small project needs
Dedicated CI/CD platforms (GitHub Actions, GitLab CI, Jenkins, and similar) provide genuinely valuable capability at team and production scale: automated testing before deployment, staged rollouts, easy rollback, and coordination across multiple contributors and environments. For a solo developer or a small team running a straightforward application or site on a single VPS, this infrastructure represents real setup and maintenance overhead that may not be justified yet, when the actual need is simply "when I push my code, I want the live server to update," a considerably smaller, more directly solvable problem using git's own native capabilities without introducing an additional third-party platform dependency at all.
The core concept: bare repo + post-receive hook
The approach relies on two git features working together. A bare repository (a git repository with no working directory, existing purely as the version-controlled data store itself) is created on your VPS, serving as the deployment target you push to. A post-receive hook, a script git automatically executes on the server immediately after it receives a push to that bare repository, is where you define what "deploying" actually means for your specific project, commonly checking out the latest code into your live site's actual working directory, then running any necessary build steps (installing dependencies, compiling assets) and restarting relevant services.
This means the entire deployment mechanism lives within git's own native hook system and a shell script you control directly, with no additional platform, account, or external service dependency beyond your own VPS and your local git installation.
Step-by-step setup
On your VPS, create a bare repository in a location separate from your actual live site's directory, commonly something like /var/repo/yourproject.git, using git init --bare. Create the post-receive hook file within that bare repository's hooks directory (hooks/post-receive), containing a shell script defining your deployment steps, covered concretely in the example below, and make it executable (chmod +x hooks/post-receive). On your local development machine, add this VPS repository as a git remote to your existing project (git remote add production ssh://user@yourserver/var/repo/yourproject.git), using SSH for secure, authenticated access. Push to this new remote (git push production main, or whichever branch you are deploying), which triggers git to receive the push on the server, and immediately, automatically execute your post-receive hook script, performing the actual deployment steps you defined.
A realistic post-receive hook example
A representative, simplified example of what a post-receive hook script might contain for a typical project:
#!/bin/bash
TARGET="/var/www/yourproject"
GIT_DIR="/var/repo/yourproject.git"
BRANCH="main"
while read oldrev newrev ref
do
if [[ $ref = refs/heads/$BRANCH ]]; then
echo "Deploying $BRANCH branch to production..."
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
cd $TARGET
npm install --production
pm2 restart yourapp
fi
done
This checks out the latest pushed code directly into your live site's working directory, installs any updated dependencies, and restarts the running application process (using PM2 in this example, covered in more depth in our PM2 vs systemd guide), all automatically triggered the moment a push completes, with no manual server login or manual command execution required for a standard deployment.
Handling environment-specific files and secrets
A genuinely important practical detail: environment-specific configuration and secrets (API keys, database credentials, environment variables) should never be committed to your git repository at all, meaning your deployment process needs to account for these files existing independently on the server rather than being part of what git checks out on each deploy. A common, reasonable approach: keep a .env file (or equivalent environment-specific configuration) permanently in place on the server, outside of what your checkout process would overwrite, either by excluding it from the checked-out paths entirely or by using a .gitignore-respecting deployment approach that leaves untracked files on the server undisturbed by the checkout operation.
Confirm your specific hook script's checkout behavior does not inadvertently delete or overwrite these persistent, server-specific files during each deployment, testing this specifically during initial setup rather than discovering a problem only when a live secret gets accidentally wiped during a routine deploy.
Adding a basic safety check before going live
A meaningful, low-effort improvement over the most basic version of this workflow: adding a simple check within your post-receive hook before actually completing the restart step, confirming a build or basic syntax check succeeds before the live application is actually restarted with the new code, rather than blindly deploying and restarting regardless of whether the new code is actually valid and functional. This does not replace genuine automated testing the way a full CI/CD pipeline provides, but even a basic "does this build/lint successfully" check before committing to a restart meaningfully reduces the risk of a broken deploy taking your live site down entirely, a reasonable middle ground between no safety check at all and the full sophistication of a dedicated CI/CD pipeline.
When this approach stops being enough
This workflow is well-suited to a solo developer or small team's straightforward project, but it is worth being honest about where it genuinely stops being sufficient, so you recognize the signal to invest in more robust tooling rather than continuing to stretch this simpler approach past its reasonable limits. A growing team with multiple contributors regularly pushing changes benefits from a genuine CI/CD pipeline's coordinated testing and review gates before code reaches production, which this simple hook-based approach does not provide. A project requiring genuine zero-downtime deployment (rather than a brief restart interruption, covered in principle in our Next.js 502 guide) needs more sophisticated deployment orchestration than a straightforward checkout-and-restart script provides. And a project with a genuine need for easy, reliable rollback to a previous version benefits from a dedicated pipeline's built-in versioning and rollback tooling, which this simple approach can approximate with additional scripting effort but does not provide natively.
FAQs
Is this approach secure, given it relies on direct SSH access to push to the server?
Yes, provided your SSH access itself is properly secured (key-based authentication rather than password authentication, and access limited to authorized users), since the deployment mechanism relies entirely on the same SSH security your server already depends on for any administrative access, not introducing a separate, additional attack surface beyond what proper SSH hardening already addresses.
Can multiple developers push to this same deployment setup?
Yes, any developer with appropriate SSH access to the server and the bare repository can push using the same mechanism, though for a team beyond a very small size, the coordination benefits of a genuine CI/CD pipeline with pull request review gates before deployment become increasingly valuable compared to this simpler, more direct push-to-deploy approach.
Does this work for a WordPress site, or is it primarily for custom applications?
This general pattern is more commonly and naturally applied to custom applications (Node.js, Python, and similar) with a clear build and restart process; a WordPress site's typical file and database structure makes this specific git-based deployment pattern less commonly used, since WordPress deployment more commonly relies on other methods (staging environments, dedicated migration tools, covered in our staging environment guide) better suited to its particular architecture.
What happens if the post-receive hook script itself fails partway through?
Depending on how the script is written, a failure partway through can leave the deployment in an inconsistent state (partially checked out code, dependencies not fully installed); writing the hook script with reasonable error handling (checking that each step succeeded before proceeding to the next) reduces this risk, though this is a genuine limitation compared to a more robust CI/CD pipeline's built-in failure handling and rollback capability.
Can I use this approach alongside a staging environment?
Yes, a common, reasonable pattern is maintaining two separate git remotes and bare repositories, one for a staging server and one for production, allowing you to push to staging first for testing before separately pushing the same, now-verified code to production, a lightweight approximation of a proper staging-then-production pipeline.
Does this require any special software installed on the VPS beyond git itself?
No additional software is strictly required beyond git (which is standard on virtually any Linux VPS) and whatever your specific application's own runtime and build tools require (Node.js and npm, for example, if deploying a Node application); the deployment mechanism itself relies entirely on git's own built-in hook capability.
Is this a reasonable long-term solution, or should I always plan to eventually move to a full CI/CD platform?
For a project that genuinely remains small in scope and team size indefinitely, this can remain a perfectly reasonable long-term approach; the signal to move to a more robust CI/CD pipeline is genuine team growth, increasing deployment complexity, or a specific need (zero-downtime deployment, sophisticated rollback) that this simpler approach does not adequately provide, rather than an inherent expiration point purely based on time or project age.
Conclusion
A bare git repository combined with a post-receive hook provides a genuine, reliable push-to-deploy workflow on your own VPS without requiring a separate CI/CD platform, well-suited to a solo developer or small team's project where the full sophistication of a dedicated pipeline is not yet justified. The core setup, a bare repo, a deployment script triggered automatically on push, and careful handling of environment-specific secrets kept outside version control, takes about fifteen minutes to establish and provides a meaningfully faster, more reliable deployment process than manual server updates.
Recognize the signal to move toward a full CI/CD platform when your team grows, when genuine automated testing before deployment becomes necessary, or when you need more sophisticated rollback and zero-downtime deployment capability than this simpler approach reasonably provides.
HostCloud's VPS plans give you full root access needed to set up this kind of deployment workflow directly, and our support team can help troubleshoot SSH and git configuration if you run into setup issues. See VPS plans at https://hostcloud.in.
