The Problem: Sharing Code Without Relying on External Services
I work with a 3–4 person team on an internal project where code can’t be pushed to public GitHub because of sensitive information. GitHub Private either costs money or has limitations, and self-hosting GitLab or Gitea means installing software, configuring a database, nginx, HTTPS — all of that before you can push a single line of code.
One afternoon, staring at an underused VPS, it hit me: Git is distributed version control at its core. You don’t need a fancy web UI or an issue tracker — just a place to push and pull code between team members. A Git bare repository over SSH is exactly that, and I’ve been using this approach for over a year without any issues.
Analysis: Why Small Teams Don’t Need Gitea or GitHub
Gitea and GitHub solve a lot of big problems: web UI, pull requests, code review, CI/CD integration… But for a team of just 2–5 people working together daily, all of that is overkill.
Installing Gitea requires:
- A background binary (or Docker container)
- A database — SQLite works, but it’s still an added dependency
- Nginx reverse proxy + SSL certificate
- User management via web UI, periodic software updates
With a Git bare repository:
- Nothing extra to install — Git comes pre-installed on most Linux distros
- Authentication via SSH keys — something every team already uses
- Setup done in 10 minutes
- No web service exposed externally, meaning a smaller attack surface
The trade-off is clear: you lose the web UI and pull requests. But for a small team working directly together, reviewing via chat and git diff is more than enough.
Other Approaches
Before diving into the guide, here’s a quick overview of your options so you can choose what actually fits your needs:
- GitHub/GitLab.com — Convenient, but your code lives on a third-party server. Not suitable if the project has data sovereignty requirements or internal security constraints.
- Gitea/Gogs self-hosted — A polished web UI similar to GitHub, but requires installation and ongoing maintenance. Better suited for larger teams or when a real web interface is truly needed.
- Git bare repo over SSH — The lightest option, no web UI, but completely sufficient for small teams. This is what I’ll cover in detail below.
The Best Approach: Setting Up a Git Bare Repository over SSH
Prerequisites
You’ll need:
- A Linux server — a VPS, internal server, or even a Raspberry Pi will do
- SSH access to that server
- Git installed (
sudo apt install gitif it’s not there yet)
Step 1: Create a Dedicated Git User on the Server
Best practice is to create a dedicated user for Git — don’t use root or a personal account:
sudo adduser git
sudo mkdir -p /home/git/.ssh
sudo touch /home/git/.ssh/authorized_keys
sudo chmod 700 /home/git/.ssh
sudo chmod 600 /home/git/.ssh/authorized_keys
sudo chown -R git:git /home/git/.ssh
Step 2: Add Each Team Member’s SSH Public Key
Each team member sends their SSH public key (usually at ~/.ssh/id_ed25519.pub). Add each key to the authorized_keys file on the server:
# Run on the server with sudo
sudo bash -c 'echo "ssh-ed25519 AAAAC3Nza... member_1@laptop" >> /home/git/.ssh/authorized_keys'
sudo bash -c 'echo "ssh-ed25519 AAAAC3Nza... member_2@workstation" >> /home/git/.ssh/authorized_keys'
Alternatively, if a team member can temporarily SSH into the server, they can add their own key from their machine:
ssh-copy-id git@your-server-ip
Step 3: Create the Bare Repository
A bare repository has no working tree — it contains only raw Git data, equivalent to the .git directory inside a regular repo. This is what you need on the server.
# On the server, switch to the git user
sudo su - git
# Create a directory to hold all repos
mkdir -p ~/repos
cd ~/repos
# Initialize the bare repository
git init --bare myproject.git
Convention: name it with a .git suffix to make it clear it’s a bare repo.
Step 4: Clone from a Team Member’s Machine
Once the repo is on the server, each team member clones it to their machine:
# Clone the repo (replace your-server-ip with the actual IP)
git clone git@your-server-ip:repos/myproject.git
# If SSH is not on port 22
git clone ssh://git@your-server-ip:2222/home/git/repos/myproject.git
From here, the workflow is exactly like GitHub:
git add .
git commit -m "feat: add login feature"
git push origin main
# Pull the latest code from teammates
git pull origin main
Step 5: Restrict the Git User’s Permissions (Recommended)
The git user should only be used for Git operations — no need for a full shell. Use git-shell to restrict access:
# Check git-shell path
which git-shell
# /usr/bin/git-shell
# Add to /etc/shells if not already present
echo $(which git-shell) | sudo tee -a /etc/shells
# Change the git user's login shell
sudo chsh git -s $(which git-shell)
After that, if anyone SSHes in as the git user for anything other than Git commands, they’ll see:
fatal: Interactive git shell is not enabled.
A small but useful extra layer of security.
Step 6: Adding New Repos When Needed
When a new project comes up, just repeat step 3:
sudo su - git
cd ~/repos
git init --bare project2.git
git init --bare internal-tools.git
When a new member joins the team, just add their SSH key to authorized_keys — no account creation, no additional configuration needed.
A Hard Lesson About git push –force
I once lost important code because of an accidental force push to the wrong branch — ever since, I’ve been extremely careful with git push --force. Specifically, I rebased a local branch and force pushed it, forgetting that a teammate had new commits on that branch. Their commits were completely overwritten. It took two hours of git reflog archaeology to recover them.
To prevent this with your bare repo, block force pushes using a server-side hook — the kind no one can bypass:
# On the server, create a hook in the bare repo directory
cat > ~/repos/myproject.git/hooks/update << 'EOF'
#!/bin/bash
refname="$1"
oldrev="$2"
newrev="$3"
# Prevent force push to main branch
if [ "$refname" = "refs/heads/main" ]; then
if git merge-base --is-ancestor "$oldrev" "$newrev"; then
exit 0
else
echo "Force push to main branch is not allowed!"
exit 1
fi
fi
exit 0
EOF
chmod +x ~/repos/myproject.git/hooks/update
Extra Tips for a Smoother Experience
Create an SSH Alias to Avoid Memorizing the IP
Add this to ~/.ssh/config on each team member’s machine:
Host gitserver
HostName your-server-ip
User git
Port 22
IdentityFile ~/.ssh/id_ed25519
Then cloning becomes much shorter:
git clone gitserver:repos/myproject.git
Simple Bare Repo Backup
# Create a bundle — a single file containing the entire history
git -C ~/repos/myproject.git bundle create /backup/myproject-$(date +%Y%m%d).bundle --all
When Should You Upgrade to Gitea?
Git bare repo over SSH works well when your team is 5 people or fewer, everyone is comfortable with the terminal, you don’t need a web UI or pull request workflow, and you want a quick setup with minimal maintenance.
Consider Gitea when the team grows larger, you need to onboard new members frequently, you need code review with inline comments, or team members aren’t comfortable with the Git CLI.
The nice thing is that migrating later is painless: when you’re ready to upgrade, just push the bare repo to Gitea and you’re done — the entire commit history is preserved, nothing is lost.

