Zero-Cost CI/CD with Git Hooks and Docker Compose
For small projects or self-hosted apps, using a full-fledged CI/CD tool like GitHub Actions or Jenkins can be overkill. What if you could have automated deployments without any third-party service, and all you need is Git and SSH?
In this post, we’ll walk through setting up a lightweight, no-cost CI/CD pipeline using Git hooks and Docker Compose, with deployments triggered by a simple git push
to your production server.
Overview
Here’s the basic idea:
- You push your code from your dev machine to both:
- A central Git host (like GitHub/GitLab)
- Your production server (a bare Git repo)
- The production server runs a
post-receive
hook to:- Checkout the latest code
- Rebuild and restart Docker containers
This method is great for solo developers or small teams who want simple, fast deployments without external dependencies.
Step-by-Step Setup
1. Prepare the Production Server
Install Git and Docker:
sudo apt update && sudo apt install git docker docker-compose -y
Create a bare Git repository:
mkdir -p ~/repos/myapp.git
cd ~/repos/myapp.git
git init --bare
Set up SSH access:
From your dev machine, copy your SSH key to the production server:
ssh-copy-id user@yourserver
Test it:
ssh user@yourserver
2. Add the Production Server as a Remote on Your Dev Machine
In your project repo:
git remote add production ssh://user@yourserver/home/user/repos/myapp.git
Now you can push to the production server:
git push production main
3. Create a Post-Receive Hook on the Server
On the production server:
nano ~/repos/myapp.git/hooks/post-receive
Paste the following:
#!/bin/bash
APP_DIR=/var/www/myapp
# Checkout code
git --work-tree=$APP_DIR --git-dir=$(pwd) checkout -f
# Deploy with Docker
cd $APP_DIR || exit
docker compose down
docker compose build
docker compose up -d
Make it executable:
chmod +x ~/repos/myapp.git/hooks/post-receive
What Exactly Happens During git push
-
You run:
git push production main
-
Your local Git client:
- Connects to the production server over SSH.
- Invokes
git-receive-pack
on the server’s bare Git repo.
-
Git negotiates what data is needed:
- It sends new commits, trees, and blobs over SSH.
-
The server’s Git receives the data and updates the bare repo.
-
The
post-receive
hook is automatically triggered:- It checks out the new code to the app directory.
- It runs
docker compose
to deploy the latest version.
-
Only once the hook script finishes does the
git push
complete on your dev machine.
This means your git push
blocks and provides real-time feedback on deployment success or failure.
How It Works
- When you run
git push production main
, Git connects to the server over SSH. - Your dev Git sends commit data to the bare repo on the server.
- Git automatically runs the
post-receive
hook. - The hook checks out the new code and runs Docker commands.
No daemon, no polling, no fancy tools. Just Git + SSH + Docker.
FAQ
Q: Does the push block until the deployment finishes?
Yes. The
git push
command will block until thepost-receive
hook finishes. That way, you get immediate feedback if the deployment fails.
Q: Does Git use SSH to send data?
Absolutely. All Git data (commits, trees, blobs) is transferred securely over SSH when using
ssh://
remotes.
Q: Can I still use GitHub/GitLab?
Yes! You can push to both:
git push origin main # Push to GitHub
git push production main # Deploy to server
Final Thoughts
This setup gives you a super simple and secure way to deploy code with nothing more than Git and Docker. For many indie devs and internal tools, it’s all you really need.
Want logging? Add >> /var/log/deploy.log 2>&1
to the hook.
Want async? Run the hook script in the background with &
.
Happy hacking! 🚀