Private git repos at home: just git over ssh

I work on the same projects from several computers: a desktop, a laptop, a headless VM. Some of those projects are private and I’d rather not put them on GitHub. I don’t need a git hosting platform for that. A Linux box at home with ssh and git installed is enough. No web UI, no database, no daemon, no extra user. Git has supported ssh remotes since the beginning.

The setup in one picture:

 laptop ──┐
          │  ssh (over Tailscale)    my-server
 desktop ─┼────────────────────────▶ ~/git/foo.git   (bare repo)
          │                          ~/git/bar.git
 VM ──────┘

Every client has a full clone, with the whole history. The server is just the agreed meeting point where all the clones push to and pull from.

Prerequisites: Tailscale and ssh

The home server has to be reachable from all your machines, also when you’re away from home. Follow my Tailscale post: install Tailscale on the server and on every client, set up key-based ssh, and turn off password login.

With MagicDNS enabled (it’s on by default for new tailnets; check the DNS page in the Tailscale admin console), every machine is reachable by its short name, e.g. my-server. The name is the machine’s hostname, and you can rename it in the admin console. Check that this works from every client:

ssh my-server

It should log you in without asking for a password. That’s the only “auth” this setup needs: whoever can ssh into your account on my-server can push and pull. In this case that’s you, from the computers you own.

Install git on the server

Ubuntu 26.04 Server usually has git already, but just to be sure:

sudo apt install git openssh-server

That’s all the server needs. When you push or pull, the client ssh’s into the server and runs git-receive-pack or git-upload-pack there. Those are plain git binaries, nothing runs in the background.

Why not a dedicated git user?

Most tutorials create a git user with git-shell as its login shell. That’s useful when you hand out access to other people or to less trusted machines (CI runners, deploy keys), and want those keys limited to git only. Here it’s just me, with the same keys that already give me a full shell on my-server. A git user wouldn’t add any protection. I’d only get a second authorized_keys to keep in sync, so I reuse my own account. If you do need one, see the end of this post.

If ssh says Permission denied and the username on my-server differs from the one on your client, tell ssh which user to use in ~/.ssh/config on the client:

Host my-server
    User martin

Creating a repo

On the server, repos live in ~/git/ as bare repos: just the git database, no working directory. You never edit files in a bare repo, you only push to it and pull from it. The .git suffix is a convention for bare repos.

Git won’t create a repo on push, so create it first. You don’t even have to log in. Run this from any client:

ssh my-server git init --bare -b master git/foo.git

git init creates ~/git/ too if needed. -b master sets the default branch; use main if that’s what you prefer. It just has to match the branch you’ll push.

Pushing an existing project

In the project on your client:

git remote add origin my-server:git/foo.git
git push -u origin master

my-server:git/foo.git is the scp-like ssh syntax: host, a colon, and a path. A path without a leading / is relative to your home dir on the server. If the project already has an origin (say, GitHub), give this remote a different name, e.g. home.

Cloning on another computer

git clone my-server:git/foo.git

That’s it. origin and the upstream branch are set up already.

To see which repos you have:

ssh my-server ls git

Working on the server itself

my-server can be one of your work machines too. Clone the bare repo there as well, straight from the local path, no ssh needed:

git clone ~/git/xyz.git ~/work/my/xyz

~/work/my/xyz is now an ordinary working copy, with origin pointing to ~/git/xyz.git. Pull and push as on any other machine. The clients see your pushes as usual, because they all pull from the same bare repo.

Day-to-day

It’s plain git, the same as with GitHub. Pull before you start working on a machine, and push when you’re done:

git pull
# ...hack, commit...
git push

If you forget to pull and another computer pushed in the meantime, git push gets rejected. That’s fine: git pull (merge or rebase, as you like), then push again. Nothing gets lost.

What about backups?

I don’t bother. Every clone has the full history, so as long as I push and pull regularly, any of my computers has an up-to-date copy. If my-server dies, I create a fresh bare repo somewhere else and push to it from any clone.

What you don’t get

A web UI, pull requests, issues, CI. For a solo developer working on their own projects, I don’t miss any of it. For browsing history, diffs and branches I use lazygit, a terminal UI for git that’s packaged in Ubuntu:

sudo apt install lazygit

It’s worth an alias, since you’ll type it a lot. Add this to ~/.bashrc:

alias lg=lazygit

(In fish: alias --save lg lazygit.)

If you want a web UI and the rest anyway, Forgejo is a lightweight self-hosted option. I’ll cover that in a follow-up post.

Alternative: a dedicated git user

If you give push access to someone else, or to a machine you trust less than your own computers (a CI runner, a deploy key), don’t put their key into your own authorized_keys: it’d give them a full shell as you. Create a git user instead, whose login shell is git-shell. git-shell comes with git and only runs the git commands needed to push and pull, nothing else.

On the server:

sudo useradd --create-home --shell /usr/bin/git-shell git

The user has no password, so the only way in is an ssh key. Create its ~/.ssh/authorized_keys, one public key per line:

sudo install -d -m 700 -o git -g git ~git/.ssh
sudo install -m 600 -o git -g git /dev/null ~git/.ssh/authorized_keys
sudoedit ~git/.ssh/authorized_keys

Prefix every key with restrict, which turns off port forwarding, agent forwarding and the like. git-shell limits what the key can run, but not what it can tunnel through ssh:

restrict ssh-ed25519 AAAAC3Nza... ci-runner

If you’ve limited ssh logins with AllowUsers (or AllowGroups) in /etc/ssh/sshd_config, add git there too. Otherwise sshd rejects every key with Permission denied (publickey), even a correct one. sudo sshd -T | grep -i allow shows what’s in effect. After editing, run sudo systemctl reload ssh. sshd’s log, sudo journalctl -u ssh, gives the actual reason for any rejected login.

git-shell won’t run git init, so create repos as the git user via sudo. I put them straight into git’s home dir, /home/git:

ssh -t my-server sudo -u git git init --bare -b master /home/git/foo.git

(-t gives sudo a terminal to ask for your password.) The remote URL now names the user, and the path is relative to git’s home dir:

git clone git@my-server:foo.git

To move an existing repo over, move it on the server and fix its owner:

sudo mv ~/git/foo.git /home/git/
sudo chown -R git:git /home/git/foo.git

then point every clone to the new location:

git remote set-url origin git@my-server:foo.git

On Ubuntu, home dirs aren’t readable by other users, so on the server itself your own account can’t git clone /home/git/foo.git either. Clone over ssh instead, git clone git@localhost:foo.git, with your own key added to git’s authorized_keys.

Written on October 22, 2026