Kemal Yılmaz front-end developer

Setting Up Git Repositories in `/srv/` Directory on Ubuntu Server 24.04.2 LTS

So, you decided to setup a home server on an old laptop. (Or on your VPS). Here is how to setup a Git repositories directory without sharing it with anyone but yourself. But why?

Table of Contents

If you’re looking for a simple, single-user setup, you can skip to the “Full Setup” section or the “Short Setup” if you’re already comfortable with configuring an Ubuntu Server environment. Other sections of this post are more of an output of my personal interest in details.

Story

Let’s start by addressing the “Ok But Why?” question.

I’ve been using source code repository hosting services for a long time, and I will continue to do so. However, in today’s software development landscape, where AI crawlers are increasingly involved, maintaining repositories on these services reminds me of a years-old concern: Privacy! Do I have full control over my content? I haven’t seen a clear option in the settings of these services, and frankly, I don’t believe any of them would truly respect our privacy choices, even if such an option existed.

For now, there isn’t a free solution that satisfies me in the cloud. The internet was supposed to be a free environment, but clearly, it is not the case anymore. Until I find a better alternative, I’ve decided to keep my side projects local.

I’m aware that a few personal projects can add zero to none value to LLMs, especially if the coding is done by me. 😀 But it’s not about the value of the code; it’s about having a choice.

Prerequisites

This post doesn’t cover the initial setup for Ubuntu Server, installing Git, or setting up SSH access on your server. To get the most out of this post, you need to have:

  1. A server with Ubuntu Server 24.04.2 LTS installed. (Probably other versions will also work. I tested it with the default image on an old laptop and with the 24.04.2 LTS (GNU/Linux 6.8.0-1018-raspi aarch64) image on a Raspberry Pi 3. On the Raspberry Pi Imager software it’s code is Server OS for RPi Zero 2W/3/4/400/5.)
  2. A sudo user on the server. (SSH-accessible by you.)
  3. Git and SSH software installed on both the server and the client. (Ubuntu Server includes both Git and OpenSSH.)
  4. And a person who feels uncomfortable using the root user for such operations!

Server, as used in this post, is the place where you set up and store your bare repositories. It’s the origin and the single source of truth. Client is any other place where you clone your repositories from the server. Users’ local copies of the origin repositories.

Short Setup

A super short commands summary for the lazy people. If you’re looking for an explained version, visit the Full Setup section.

Assuming you have access to the uglycat sudo user on the server with a public key.

# ---
# Server Part
# ---

# 1. connect to the server
ssh uglycat@your.server.ip.address
# 2. create parent git directory in `/srv/`
cd /srv/
sudo mkdir git
# 3. change ownership of `/srv/git/` directory
sudo chown uglycat:uglycat /srv/git/
# 4. (optional) setup default git branch
git config --global init.defaultBranch main
# 5. create a directory for your first repository
cd git/
mkdir my-first-repo.git
# 6. initialize a bare repo
cd my-first-repo.git/
git init --bare
# 7. logout from server
logout

# ---
# Client Part
# ---

# 1. create a directory on your computer (wherever you keep your repositories)
cd ~
mkdir Repositories
cd Repositories/
# 2. clone the bare repo you've just created
git clone uglycat@your.server.ip.address:/srv/git/my-first-repo.git/
# 3. verify write access
cd my-first-repo/
touch README.md
git add README.md
git commit -m "Added README file"
git push

# FIN

Full Setup

First, start with verifying your username and the user groups you belong to. After connecting to your server with SSH, run the commands below for verification:

# verify your user name:
whoami
# uglycat

# verify your groups:
groups uglycat
# uglycat : uglycat some other groups sudo ...

So, we are the user uglycat and we belong to the sudo group. Now, let’s continue with the directory we’ll use as the repository directory.

The /srv directory is the proper place to use such content serving purposes in Linux-like systems. (I believe it’s short for “serve”.) We’ll use the /srv directory; however, it’s a root user owned directory by default. Run ls -l in the / directory of the Ubuntu Server and verify it. (Focus on the ... root root ... srv parts of the console output.)

# change to the / directory
cd /

# find the /srv directory in the list and see the owner
ls -l
# PERMISSIONS LINK_COUNT root root SIZE DATE srv

I’ve seen comments and articles that suggest changing permissions on the /srv/ directory itself. That’s not a good idea, I guess. We won’t do that. Instead, we’ll create a directory under the /srv/ directory to keep our repositories. We’ll use /srv/git/, but the name doesn’t matter.

# change to the /srv directory
cd /srv/

# create /srv/git/ directory (WITH sudo)
sudo mkdir git

# check created directory
ls -l
# PERMISSIONS LINK_COUNT root root SIZE DATE git

Above, we used the sudo command to create the git directory because the parent directory /srv belongs to the root user. Now we need to change the ownership of the git directory so we can interact with it as the uglycat user.

# change ownership of /srv/git/ directory (WITH sudo)
sudo chown uglycat:uglycat /srv/git/

# check the directory again 
# added `-a` flag to see the current (.) and parent (..) directories are still `root` owned.
ls -al
# PERMISSIONS LINK_COUNT uglycat uglycat SIZE DATE git

A Note on uglycat:uglycat (user:group) notation:

In Ubuntu (and most Linux distros), every user has their own group with the same user name. This is a feature called User Private Groups. Here, we’re using a single user setup, so that’s ok. See the extra content below on multiple users.

And that’s all you need to do to set up the parent directory (git) of your future repositories.

Before creating repositories, I suggest you set the global Git default branch name with:

# setup default git branch
git config --global init.defaultBranch main
git config set --global init.defaultBranch=main # (git version 2.46.0 and above)
git config --list --global
git config list --global # (git version 2.46.0 and above)

Not required to continue, but while we’re here:

# setup default git user
git config --global user.email "uglycat@ugly.cat"
git config --global user.name "Ugly Cat"
git config set --global user.email "uglycat@ugly.cat" # (git version 2.46.0 and above)
git config set --global user.name "Ugly Cat" # (git version 2.46.0 and above)

Check the default Git configuration:

git config --list --global
# init.defaultbranch=main
# user.email=uglycat@ugly.cat
# user.name=Ugly Cat

Now we have our basic global Git configuration. Let’s create the first Git repository under the /srv/git/ directory:

# assuming we're still in the /srv directory
cd git/
# `mkdir` WITHOUT `sudo` since it's already owned by uglycat
mkdir my-first-repo.git 
cd my-first-repo.git/
git init --bare
# Initialized empty Git repository in /srv/git/my-first-repo.git/

Adding .git at the end of the bare repository name is a common naming convention for bare repositories.

Congratulations! 🎉 You’ve set your first bare repository on your home server. Now let’s see if we have proper access to this repository from outside the server.

First, log out from your server (logout command) and return to your client machine. Set a directory to keep your repositories.

# switch to the client
logout

# assuming you're on the same machine! :)
cd ~
mkdir Repositories
cd Repositories/

# clone your repo under the `~/Repositories/` directory
git clone uglycat@your.server.ip.address:/srv/git/my-first-repo.git/
# Cloning into 'my-first-repo'...
# warning: You appear to have cloned an empty repository.

# see if the default branch is set as `main`
cd my-first-repo/
# my-first-repo git:(main)
git status
# On branch main
# ...

And you have read access if you see the same above. Let’s check if we have write access as well:

# create and empty file
touch README.md

# add & commit
git add README.md
git commit -m "Added README file"

# push to remote
git push
# Enumerating objects: 3, done.
# ...
# To your.server.ip.address:/srv/git/my-first-repo.git
#  * [new branch]      main -> main

And if you see a successful push message on your terminal, you’re all set!

# optionally, see the commit log
git log

FIN

Contrary to the common belief, there’s a possibility that the future internet will push us more and more local. This could also result in more and more services being placed behind paywalls. This is my temporary plan to keep my work to myself while dreaming of a truly free internet.

Continue with the “Extras” section to dig deeper into different use cases.


  1. 4.2 Git on the Server - Getting Git on a Server
  2. 4.4 Git on the Server - Setting Up the Server
  3. git-shell - Restricted login shell for Git-only SSH access
  4. sshd — OpenSSH daemon
  5. ssh — OpenSSH remote login client
  6. tar - an archiving utility
  7. Notation of traditional Unix permissions

Extras

Here are a few extras for more use cases.

Extras 1: Putting an Existing Repository as a Bare Repo on the Server

You should have the server setup described in the Full Setup section before continuing with this section. (Or a similar one. Or just be sure of having a read/write accessible server directory.)

# change directory to your existing repository folder
cd my-existing-repo/

# optional but beneficial
# switch to the `main` branch to set the remote branch head to main (you can change it later)
git checkout main

# switch to the top directory (or a different directory)
cd ..

# clone the existing repo as a bare (mind the ".git" part)
git clone --bare ./my-existing-repo/ ./my-existing-repo.git/

# optionally, see the commit log of the newly created bare repository
# switch to cloned bare repo (mind the ".git" part)
cd my-existing-repo.git/
git log

When creating a bare repository, all the origin/* counterparts of local branches will be created, even if you don’t have them in your current origin. You can delete them later if you don’t want them in the origin. (Something like a local-only work-in-progress feature branch.)

We created the bare repository on the client. Now we need to push it to the server:

# scp is OpenSSH's secure file copy tool
# recursively (-r) copy the contents of the client copy to the `/srv/git/` directory in the server
scp -r ./my-existing-repo.git/ uglycat@your.server.ip.address:/srv/git/

# wait until copy operation finishes
# ...
# ... (lots of hashed numbers and file names)
# ...

# SSH into the server and see if the repo is copied
ssh uglycat@your.server.ip.address
ls -al /srv/git/
# PERMISSIONS LINK_COUNT uglycat uglycat SIZE DATE my-existing-repo.git

# switch to the client
logout

# you can now delete the old repository and clone the server copy to continue from the local repository
cd ~/Repositories/
git clone uglycat@your.server.ip.address:/srv/git/my-existing-repo.git/
cd my-existing-repo/
git log

Extras 2: Multiple Git Users Setup

If you want to set up a shared repository directory for multiple outside users, here’s a quick and easy way:

Outside Users are users who either have no direct access to the server or are users of the server without sudo privileges. The git-shared user account will be created here.

Backup User is an always-available account to access the server with sudo privileges to correct things in case of situations like accidentally or knowingly locking yourself out of your user access.

Here are the steps we will follow to create the shared Git account:

  1. Create a new user on the server with sudo access and name it something like git-shared, or any other name.
  2. Log out from the current user, copy your public key to git-shared user as, well and log back in as the new git-shared user.
  3. Create the shared directory under /srv/git-shared/ and change ownership of the directory to the git-shared user and git-shared group as we did in the previous section.
  4. Create a bare repository.
  5. Copy outside users’ public keys to the ~/.ssh/authorized_keys file of the git-shared user manually.

Now, we can start adding the git-shared user:

# 1. create `git-shared` user
sudo adduser git-shared
sudo usermod -aG sudo git-shared
# 2. logout and login again with the new `git-shared` user (you need to copy your public key to the `git-shared` user as well with `ssh-copy-id` for ssh access)
logout
ssh git-shared@your.server.ip.address
# 3. create the shared repo directory
sudo mkdir /srv/git-shared/
sudo chown git-shared:git-shared /srv/git-shared/
# 4. create a bare repo
cd /srv/git-shared/
mkdir my-shared-repo.git
cd my-shared-repo.git/
git config --global init.defaultBranch main
git init --bare
# 5. copy public keys (you need to ask each outsider to share you with their public key)
echo "outside-user-s-public-key-1" >> ~/.ssh/authorized_keys
echo "outside-user-s-public-key-2" >> ~/.ssh/authorized_keys
echo "outside-user-s-public-key-3" >> ~/.ssh/authorized_keys

# step 5. confused you? that's good, you're reading, thanks
# since we've already copied our very own public key to access the `git-shared` user account on the server confusion is expected
# think your own public key as the administrator role

# the intention here is to have public keys of outside contributors, not yours or administrator's
# outsiders' public keys should be added in the `~/.ssh/authorized_keys` in a restricted way not in an administrative manner, as explained below

# to continue with the tutorial, you may use your own public key, just be sure to have a backup `sudo` account

The only difference from the single-user setup in the first section is a new git-shared user account with multiple public keys.

We assume that outside users won’t have any access to the server other than restricted git interactions. So, you need to copy and paste their public keys manually as the administrator of the git-shared user account.

⚠️ This setup is not complete yet! With this setup, outside users will still have full access to the git-shared user, who has sudo privileges. This is not desirable. We’ll restrict it in a bit.

Before moving forward, a helper for the server. Since git-shared account users should not know about the server, add an empty .hushlogin file under the user directory to suppress welcome message of the server:

# add this file to the `git-shared` user's home directory to suppress initial server messages after ssh connection
touch ~/.hushlogin

# this will not suppress the custom welcome messages we'll add in this section later

Another helper below, for the client. If you just want to test SSH connections, you may use the -T flag:

# use -T flag if your intention is to see only the shell access status in a limited shell environment that doesn't need pseudo-terminal allocation
ssh -T git-shared@your.server.ip.address

You don’t need -T flag when you need to access the server with the default shell. (It won’t connect anyway.)

No more extra information will be provided with these helpers when logging into the server. Let’s continue.

Thanks to one of the most powerful tools on the internet, “Git,” we have a helper inside the Git software called git-shell. It’s a restricted login shell for Git-only SSH access.

The change we need to complete the setup is simple:

  1. Add git-shell to the list of shells allowed by Ubuntu Server.
  2. Then replace the git-shared user’s default shell with git-shell.

Before moving further, make sure you have a backup sudo account to restore things later (Like, re-enabling the default shell for the git-shared user or managing the git-shared user’s server configurations.)

By default, Ubuntu Server supports multiple shells. To see what shell you’re currently using as the git-shared user, use the command below:

cat /etc/passwd | grep git-shared
# git-shared:...:...:/home/git-shared:/bin/bash

# Bash (/bin/bash) is the default shell for most user accounts

Now add git-shell to the shell list:

# list current shells
cat /etc/shells

# check existence of `git-shell`
which git-shell
# /usr/bin/git-shell

# add the path of `git-shell` (/usr/bin/git-shell) to the valid login shell list (/etc/shells)
# or `sudo nano /etc/shells`
# or `echo "/usr/bin/git-shell" | sudo tee --append /etc/shells`
sudo --edit /etc/shells

# add `/usr/bin/git-shell` to the list

# list current shells again to see if `git-shell` is added into the list
cat /etc/shells

The final step is to change the git-shared user’s default login shell to git-shell:

# change default shell of the `git-shared` user
sudo chsh git-shared --shell $(which git-shell)
# or use hard-coded value `/usr/bin/git-shell` instead of command subsition `$()`

# re-check the login shell
cat /etc/passwd | grep git-shared
# git-shared:...:...:/home/git-shared:/usr/bin/git-shell

Now, you may log out of the git-shared user and try to SSH into the server again using the same user:

⚠️ Instead of logging out, you may also keep the current server shell open. It will still keep the default shell as Bash until you close this terminal session. You can switch to another terminal window to avoid restricted shell lock-in temporarily. New login attempts will be forcing git-shell.

# logout
logout

# try logging in again
ssh git-shared@your.server.ip.address
# ...
# ...
# ...
# fatal: Interactive git shell is not enabled.
# hint: ~/git-shell-commands should exist and have read and execute access.
# Connection to your.server.ip.address closed.

# you may see the server information here as well if you didn't create the `.hushlogin` file, connection error message above is at the end in that case

Congratulations again! 🎉 Your public key has no direct access to the server anymore. (You’re also locked in, btw, if you’re using your own public key and you’ve listened to me and logged out. 🤣)

Now, you can’t use the default login shell, but you can pull and push repositories from/to the server:

# switch to the client

# change to repositories directory

# clone repo
git clone git-shared@your.server.ip.address:/srv/git-shared/my-shared-repo.git/

# add something and push freely, it is allowed

At this point, users can not SSH into the git-shared user account, and the user account can only accept a limited set of git commands for the authorized public keys. (Including the initial administrator account, your key.)

You may stop here for the basic multiple user setup. But I’ll add 3 more interesting inclusions and then conclude this section:

  1. Instead of closing the server connection completely, we’ll offer an interactive git> shell to the users with an auto-running help command. (2.1)
  2. We’ll disable the interactive shell completely with a pretty info message. (2.2)
  3. We’ll apply more restrictions to the outsiders’ public keys for more protection. (2.3)

Before starting with the first one, here is how to unlock yourself if you’re locked in:

2.0: Resolving Account Lock-In Situations

Remember, we locked ourselves in with git-shell? When you need to update the git-shared user account configuration, you can temporarily switch to the default shell using another sudo account. Here’s the quick tutorial:

  1. Log in with the initial uglycat user. (Or any other sudo user.)
  2. Revert the git-shared user’s shell to Bash (default) with sudo chsh git-shared --shell /bin/bash.
  3. Switch to the git-shared user’s shell with its own environment using su - git-shared. (No need for sudo since we’re switching from a sudo user. You’ll still be asked for the git-shared user’s password though.)
  4. … Do the administrative changes you need. …
  5. Switch back to git-shell with sudo chsh git-shared --shell $(which git-shell).
  6. Log out and try logging in again to see the changes.

You may also use the uglycat user to change files under the git-shared user’s home directory, but it might get a bit confusing every time you’re targeting the git-shared user’s home directory. Switching the login shell back and forth felt safer for me.

# login with another `sudo` account
ssh uglycat@your.server.ip.address

# you can't switch to `git-shared` user using `su - git-shared` for now

# revert the `git-shared` user's shell to Bash (default)
# (you're using `uglycat` user's sudo privileges)
sudo chsh git-shared --shell /bin/bash

# now you can switch user
su - git-shared

# do administrative stuff 

# ...
# ...
# ...

# don't forget to switch back to the `git-shell` when you're finished
sudo chsh git-shared --shell $(which git-shell)

Now we know how to resolve the lock in situation. Let’s enable the interactive git shell.

2.1: Enabling Interactive git> Shell with a help Command

git-shell can have a special directory named git-shell-commands under a user’s home directory. You may extend the command capabilities of git-shell using this directory. Now, as the git-shared user, create a directory right under the user’s home (~) directory. Just create an empty folder, return to the git-shell, and try to log in again to see what changed.

# create the special directory (name is reserved and should be the exact same)
# be sure `git-shared` user has both read and execute access on `~/git-shell-commands` directory as stated in the hint by the server message
mkdir ~/git-shell-commands

# re-enable `git-shell`
sudo chsh git-shared --shell $(which git-shell)

# exit & logout (exit to return back from `su - git-shared` session, logout to leave the server fron `uglycat` account, here the commands are interchangable no worries)
exit
logout

# try logging in again
ssh git-shared@your.server.ip.address

# git> 

# you should see a git shell (`git>`) instead of a connection refusal message

At this point, there is almost nothing you can do with this interactive shell. Let’s give some help to the visitors of this shell.

Type help in the git> interactive shell and see the error message unrecognized command 'help'.

help is another special naming convention for a file, like the git-shell-commands directory name. help will be invoked automatically when users ssh into the git> shell.

Exit from the git> shell and return back to the server.

# you may use exit command to leave `git>` shell as well
# git> 
exit

# do the change user stuff again
# 
# login with another sudo user
# change `git-shared` account's shell to Bash,
# switch to `git-shared` user

# create a file named `help` under the `git-shell-commands` directory
touch ~/git-shell-commands/help

# add some content into the help command
sudo nano ~/git-shell-commands/help

# add the simple "printf" command below to the `help` file to say hi right after login (without the initial "# " part of course) (also be careful with emojies in a git shell 😃)
# printf '%s\n' "Hi $USER! 👋"

# mark the file as executable
chmod +x ~/git-shell-commands/help

# re-enable `git-shell` and retry logging in from client

Now, when you try to log in to the server from a client, you should see the content from ~/git-shell-commands/help command:

# after swtiching to the `git-shell` and logging out form the server

# try logging in again
ssh git-shared@your.server.ip.address

# ...
# ...
# ...
# Hi git-shared! 👋
# # git> 

And you can run the help command again and again from the interactive git> shell:

# git>
help

# Hi git-shared! 👋

# git> 

You may have arbitrary commands in the same way along with the help command. Just create another file ~/git-shell-commands/some-random-command and call it in the same way from the git> shell. (Arbitrary commands will not auto-run; you need to call them manually, or as an argument to the connection command, or as a command in the authorized_keys options.)

2.2: Disabling Interactive git> Shell Completely

If you don’t want an interactive git> shell at all, another special file here to help, ~/git-shell-commands/no-interactive-login. The process is the same as with the help file.

You may use an empty file, or you may use a message here as well:

# do the change user stuff again from another sudo account and switch to the `git-shared` user

touch ~/git-shell-commands/no-interactive-login
sudo nano ~/git-shell-commands/no-interactive-login

# add message content like the one below (exit 128 is just to indicate something went wrong)
# #!/bin/sh
# printf '%s\n' "Hi $USER! 👋 You've successfully authenticated, but this server does not provide shell access."
# exit 128

chmod +x ~/git-shell-commands/no-interactive-login

# re-enable `git-shell` and retry logging in from client

ssh git-shared@your.server.ip.address

# ...
# ...
# ...
# Hi git-shared! 👋 You've successfully authenticated, but this server does not provide shell access.
# Connection to your.server.ip.address closed.

🎉 Tada!

No need to delete the previous help command with no-interactive-login. But keep in mind that commands are still callable without the interactive git> shell. So, if you’re revealing sensitive information, delete them as well.

If you forget to make a command file executable, you’ll get chmod: fatal: cannot exec 'git-shell-commands/no-interactive-login': Permission denied when you try to log in from the client.

2.3: Applying More Restrictions on Outsiders’ Public Keys

This is not a server security article, but one last restriction note before closing.

So far, we’ve managed to block direct access to the server for all users. However, some other access-related features are still available, such as port forwarding, which enables outsiders to access configured server services and applications within the local network setup of the server.

To restrict such access-related features mentioned above, add the restrict option directly before an outside user’s public key in the ~/.ssh/authorized_keys file of the git-shared user.

# login with another sudo user, change `git-shared` account's shell to Bash, switch to `git-shared` user

# edit `authorized_keys` of the `git-shared` user
sudo nano ~/.ssh/authorized_keys 

# right before each outside user's public key, add `restrict` keyword (with a space after)
restrict ssh-xxx AAABBBCCC...

The restrict keyword is a shortcut that enables all the related restrictions, such as port, agent, and X11 forwarding, PTY allocation, and execution of ~/.ssh/rc. (You can also re-enable wanted features by adding them after restrict like this: restrict,pty.)

And that’s the wrap for this section.

If you experience troubles when logging into the server after configuration changes, troubleshoot these:

  1. Check if you’ve switched back to the git-shell for the git-shared user.
  2. The -T flag of the ssh command and the restrict keyword preceding a public key disable pseudo-terminal allocation. Try not using them if you need the pty.
  3. Restricting pty (pseudo-terminal allocation) and not enabling git-shell will block the SSH connection onto the server.
  4. Emojis can cause problems. Avoid them if they are not necessary.
  5. git-shell also disables tab-completion on the client, so you need to know the full repository paths on the server.

And don’t forget that we made all these configurations for Git interactions. So, every time you change a configuration, check if you can still clone, pull, and push from/to the repositories.

The setup is more than enough for a small, trusted list of developers’ access controls. Many extra configurations are available with SSH and Git, but I have to stop here.

Extras 3: Possible Errors With Incorrect Directory Ownership Setup

This section is to prevent unnecessary copy & paste operations from unreliable resources. (Including this blog!)

Let’s say you’d used sudo privileges to create /srv/git/ and /srv/git/my-root-owned-repo.git/ directories on the server and initialized the my-root-owned-repo.git/ directory as a --bare repository in the same way.

When you try to clone such a repository, you’ll get a “dubious ownership” error:

git clone uglycat@your.server.ip.address:/srv/git/my-root-owned-repo.git/
# Cloning into 'my-root-owned-repo'...
# fatal: detected dubious ownership in repository at '/srv/git/my-root-owned-repo.git'
# To add an exception for this directory, call:

# 	git config --global --add safe.directory /srv/git/my-root-owned-repo.git
# fatal: Could not read from remote repository.

# Please make sure you have the correct access rights
# and the repository exists.

The error message (and the internet lookup) will lead you to add the repository directory to the Git global config variable safe.directory.

# login back to the server and do the recommended change 
# WITHOUT using trailing slahes at the end!
git config --global --add safe.directory /srv/git/my-root-owned-repo.git
git config --global --list

This change will allow you to clone the repository, but when you try to push a change from the client to the server, you’ll get an error:

# logout and run the clone command again
logout
git clone uglycat@your.server.ip.address:/srv/git/my-root-owned-repo.git/
# create a file and commit it in the client
touch README.md
git add .
git commit -m "Initial commit"

# try to push the change
git push
# ...
# ...
# Writing objects: 100% (3/3), 221 bytes | 221.00 KiB/s, done.
# ...
# error: remote unpack failed: unable to create temporary object directory
# To your.server.ip.address:/srv/git/my-root-owned-repo.git
#  ! [remote rejected] main -> main (unpacker error)
# error: failed to push some refs to 'your.server.ip.address:/srv/git/my-root-owned-repo.git'

Using the --shared flag with git init --bare --shared doesn’t matter in this case because the problem is still directory permissions on the server.

If you find yourself in such a situation, your solution is the same directory ownership change, this time providing the --recursive (or -R) flag with the already initialized repository.

# login back to the server

# in the server run `chown` command
sudo chown --recursive uglycat:uglycat /srv/git/my-root-owned-repo.git/

# logout and retry pushing in the client
logout

# then git push and you're good to go

You might want to cover all repositories under the /srv/git/ directory. In this case, run the same command for the parent directory:

# in the server run `chown` command
sudo chown --recursive  uglycat:uglycat /srv/git/

The purpose of this section was to prevent you from doing crazy stuff offered by the internet. Just don’t copy and paste commands blindly, even from this blog. Ugly and old-looking documentation about these kinds of things is usually the most reliable. Read them. Again and again, …

Extras 4: Manual Backups

Having Git repositories on the cloud gives us the comfort of availability, … like forever. In a local setup like the one explained here, you need to back up files manually, either with a daemon or a manual user interaction. I’m using a manual workflow for now:

  1. Log in to the server.
  2. Archive the /srv/git/ directory or a specific repository with the built-in tar utility. (Optionally password protect it.)
  3. Log out and copy the archive to the client.
  4. Upload the archive file to a secure environment or store them in a local storage.

Bare repositories are smaller in size since they don’t have .gitignore ignored files or dependencies such as node_modules. On the server, use the tar archiving utility to archive the my-backup-repo.git/ directory.

cd ~
tar -cavf backup.tar.gz --directory=/srv/git/ ./my-backup-repo.git

# this will create an arhive in the current directory with the name `backup.tar.gz`
# it will include a single target repository into the archive

# -c,--create: creates a new archive
# -a,--auto-compress: uses user provided suffix (`.tar.gz` here) to decide the compression algorithm
# -v,--verbose: enables verbose logging
# -f,--file: sets archive name to user provided value (backup.tar.gz here)

# switch to the client
logout

# after creating the archive, run the command below on the client to get the archived content
scp uglycat@your.server.ip.address:~/backup.tar.gz ~

# this command will copy the `backup.tar.gz` archive from server's ~ to client's ~ directory
# then you can store the backup in a secure location

You may also want to archive the entire /srv/git/ directory. In that case, use:

cd ~
tar -cavf backup.tar.gz --directory=/srv/ ./git

Extras 5: Quick Reminders on Useful Ubuntu Commands

Copying your public key from your client to a server:

# run this command on your client
ssh-copy-id uglycat@your.server.ip.address

Adding new users and giving them sudo privileges:

sudo adduser some_user_name
sudo usermod -aG sudo some_user_name

Deleting a user with all artifacts including the home directory:

sudo deluser --remove-all-files some_user_name

Switching user with the target user’s environment:

su - some_user_name
# no need for `sudo` if you're switching from a sudo user 

Listing groups of a user:

groups some_user_name

Listing users and groups:

cat /etc/passwd
cat /etc/group

Getting the current user name and details:

whoami
id

Logging out from the server:

logout

Getting SSH and Git versions:

ssh -V # capital "V"
git -v

Unsetting a global git configuration:

# single config item
git config --global --unset user.name
git config unset --global user.name # (git version 2.46.0 and above)


# multiple config items
git config --global --unset-all safe.directory
git config unset --global --all safe.directory # (git version 2.46.0 and above)