What "bare" means
A bare repository contains only the contents of .git - no working tree, no checkout. It is what you push to and clone from. Every Git server (GitHub, GitLab, your own) ultimately stores bare repositories.
Creating one
mkdir myproject.git
cd myproject.git
git init --bare
The .git suffix on the directory is a convention indicating bareness. The directory contents look like the inside of any .git: HEAD, config, objects/, refs/.
Pushing to it
cd ~/work/myproject
git remote add origin /path/to/myproject.git
git push -u origin main
Local paths work; so do SSH and HTTPS URLs once you put the bare repo on a server.
Sharing on a server
scp -r myproject.git user@server:/srv/git/
# On the server:
chgrp -R devs /srv/git/myproject.git
chmod -R g+rwX /srv/git/myproject.git
find /srv/git/myproject.git -type d -exec chmod g+s {} +
Group ownership and the setgid bit ensure new objects inherit the group, so multiple developers can push.
Update config:
# /srv/git/myproject.git/config
[core]
sharedRepository = group
Or:
git init --bare --shared=group myproject.git
Cloning
git clone user@server:/srv/git/myproject.git
SSH versus HTTP
For small teams, SSH access plus git-shell as the user's login shell gives a clean, secure setup. For HTTP, you need a small server like git-http-backend behind nginx or Apache - more setup, easier authentication.
Self-hosting tools
For more than a handful of repositories, run Gitea, Forgejo, GitLab Community Edition, or similar. They give you a UI, issue tracking, and merge requests on top of the bare-repo storage model.
Mirrors and backups
# Create a mirror clone
git clone --mirror <url> backup.git
# Periodically refresh
cd backup.git
git remote update --prune
A mirror clone is like a bare clone, but it tracks all refs - branches, tags, notes - identically. It is the right tool for backup or migration staging.
Hooks on the bare side
Bare repos can run server-side hooks: pre-receive, update, post-receive. Use these for policy enforcement (signed commits, branch protection) and post-push triggers (deploy, notify):
# /srv/git/myproject.git/hooks/post-receive
#!/usr/bin/env bash
while read old new ref; do
if [ "$ref" = "refs/heads/main" ]; then
cd /srv/www/myproject &&
git --git-dir=. fetch /srv/git/myproject.git main &&
git --git-dir=. reset --hard FETCH_HEAD
fi
done
This auto-deploys main on push. (Production needs more care, but the principle is right.)
Why bare matters
Pushing to a non-bare repo is risky - it bypasses the working tree and can corrupt the receiving repo's index. Bare-by-design servers prevent this. Whenever you set up a shared repo, make it bare.