Raspberry Pi Git Server over SSH: Secure Bare Repositories, Hooks, and systemd Deployment (2026)

Maintenance note (September 1, 2026): This page now begins with a current, reproducible guide. The complete 2019 source export remains unchanged in a labeled archive at the end, including obsolete branch assumptions, typographical command errors, private-LAN examples, and an attributed CSDN excerpt. Do not copy commands from the archive. The maintained commands use placeholders and official documentation; they have no affiliate links.

This design is appropriate for a small personal or trusted-team server on a Raspberry Pi OS machine. It gives Git clients SSH access to bare repositories without giving them an interactive shell. Two deployment choices are shown:

  • a short post-receive hook for a low-risk static directory; and
  • a systemd handoff that separates Git access from the account that publishes a release.

Neither design is a full forge: there is no web UI, pull-request workflow, per-repository authorization, or secret scanner. For several mutually untrusted users, use a maintained authorization layer such as Gitolite or a forge instead of sharing one unrestricted git account.

1. Decide the trust boundary before installing anything

Use the Pi on a trusted LAN or behind a VPN first. Do not forward SSH from the public internet merely to complete this tutorial. If internet access is required later, define firewall rules, patch ownership, monitoring, backups, and a key-revocation procedure before exposure.

The examples assume:

  • an administrative account named admin with sudo access;
  • a server hostname such as pi-git.local or another name you control;
  • repositories under /srv/git rather than a personal home directory;
  • a dedicated non-interactive account named git; and
  • an explicitly chosen default branch named main.

Replace names and paths consistently. Keep application secrets, uploads, databases, and mutable runtime state outside every deployed work tree.

2. Install Git and enable SSH

Raspberry Pi OS disables SSH by default on a fresh setup. Raspberry Pi Imager can enable public-key-only SSH during imaging; on an existing machine, Raspberry Pi documents raspi-config and the system service as supported routes.

On the Pi, as the administrative user:

sudo apt update
sudo apt install --yes git openssh-server
sudo systemctl enable --now ssh
git --version
systemctl is-active ssh

Apply security updates regularly through the operating system’s normal update policy. Confirm the Pi’s address and host identity through a trusted channel before accepting its SSH host key on a client.

3. Create a Git-only account

Create an account without a usable password, find git-shell, and make it the account’s login shell:

sudo adduser --disabled-password --gecos '' git
command -v git-shell
grep -Fx /usr/bin/git-shell /etc/shells || printf '%s\n' /usr/bin/git-shell | sudo tee -a /etc/shells
sudo chsh -s /usr/bin/git-shell git
getent passwd git

If command -v git-shell prints a path other than /usr/bin/git-shell, substitute that exact path in the following commands. Git documents git-shell as a restricted login shell that permits the server-side commands needed by push, fetch, and archive operations, but not a normal interactive shell.

Do not edit /etc/passwd manually for this change. chsh validates and records the shell without asking you to rewrite an account database by hand.

4. Add a restricted SSH key

Generate a separate key on each client. Current OpenSSH supports Ed25519, and Raspberry Pi’s SSH documentation names it as the stronger alternative to the RSA example in its basic walkthrough.

ssh-keygen -t ed25519 -a 64 -C 'laptop-to-pi-git'

Use a passphrase unless an unattended process has a documented key-protection strategy. Keep the private key on the client; only the .pub file belongs on the server.

Prepare the server directory:

sudo install -d -m 0700 -o git -g git /home/git/.ssh
sudo touch /home/git/.ssh/authorized_keys
sudo chown git:git /home/git/.ssh/authorized_keys
sudo chmod 0600 /home/git/.ssh/authorized_keys

From the client, prefix the public key with OpenSSH’s restrict option and append it through the administrative account:

{ printf 'restrict '; cat ~/.ssh/id_ed25519.pub; } \
  | ssh admin@pi-git.local \
      'sudo tee -a /home/git/.ssh/authorized_keys >/dev/null'

restrict disables forwarding, agent forwarding, X11 forwarding, PTY allocation, and ~/.ssh/rc for that key while still permitting Git’s required remote command. Give every person or automation job its own labeled key line so one credential can be removed without replacing all others.

Test the restriction:

ssh -T git@pi-git.local

A message that interactive Git shell access is not enabled is expected. Authentication should succeed, then the shell should refuse an interactive session.

Optional global SSH hardening

First install and test key login for the administrative account in a second terminal. Only then consider a local drop-in such as /etc/ssh/sshd_config.d/00-local-hardening.conf:

PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Validate both syntax and effective values before reloading:

sudo sshd -t
sudo sshd -T | grep -E 'passwordauthentication|kbdinteractiveauthentication|permitrootlogin'
sudo systemctl reload ssh

Keep the already authenticated administrative session open until a new key-authenticated session works. A syntax-valid file can still express the wrong access policy.

5. Create and test a bare repository

A bare repository stores refs, objects, and server-side hooks without a checked-out work tree. Choose the branch name explicitly; do not depend on a Git version’s default.

sudo install -d -m 0750 -o git -g git /srv/git
sudo -u git git init --bare --initial-branch=main /srv/git/sample.git
sudo -u git git --git-dir=/srv/git/sample.git config receive.denyNonFastForwards true
sudo -u git git --git-dir=/srv/git/sample.git config receive.denyDeletes true
sudo -u git git --git-dir=/srv/git/sample.git config --get-regexp '^receive\.'

On a client with an existing repository:

git remote add pi ssh://git@pi-git.local/srv/git/sample.git
git push -u pi HEAD:main
git ls-remote pi

Or clone it elsewhere:

git clone ssh://git@pi-git.local/srv/git/sample.git

The two receive settings protect ordinary branch history from force-pushes and deletion. They do not implement identity-based authorization: every accepted key for this shared git account can reach any repository the account can read or write.

6. Option A: a small static-directory hook

Use this only when every person allowed to push main is also allowed to publish the target. The target must contain deployable files only—never uploads, a database, or hand-edited configuration.

Create the target and server-owned hook:

sudo install -d -m 0755 -o git -g git /srv/www/sample
sudoedit /srv/git/sample.git/hooks/post-receive
#!/bin/sh
set -eu

repo=/srv/git/sample.git
target=/srv/www/sample
deploy_ref=refs/heads/main
lock=/srv/git/sample.deploy.lock

while read -r oldrev newrev refname
do
    [ "$refname" = "$deploy_ref" ] || continue

    if ! git --git-dir="$repo" cat-file -e "$newrev^{commit}" 2>/dev/null
    then
        printf '%s\n' 'The deploy branch was deleted or does not name a commit; skipping.' >&2
        continue
    fi

    (
        flock -x 9
        git --git-dir="$repo" --work-tree="$target" checkout --force main
    ) 9>"$lock"

    printf 'Deployed %s to %s\n' "$newrev" "$target"
done

Then:

sudo chown git:git /srv/git/sample.git/hooks/post-receive
sudo chmod 0755 /srv/git/sample.git/hooks/post-receive
command -v flock

The branch filter prevents tags and unrelated branches from deploying. Commit validation handles branch deletion. Quoted paths prevent word splitting, and flock serializes two nearly simultaneous checkouts.

Git invokes post-receive after refs have been updated. Its output is forwarded to the pushing client, but a non-zero exit cannot undo the accepted push. Therefore a line such as Permission denied means “push stored, deployment failed,” not “push rejected.” Use pre-receive or update for policy that must reject a push.

Hooks are server-side files and are not installed by an ordinary clone or push. Back them up and review them like infrastructure configuration.

7. Option B: queue the commit and let systemd publish it

For an application or a more valuable static site, keep SSH/Git access separate from release ownership. In this pattern, the hook only queues a validated commit ID. A systemd.path unit notices the queue and starts a sandboxed one-shot service under sample-deploy. The service exports a commit into a new immutable release directory and atomically changes a current symlink.

This example still assumes that anyone who can push main is authorized to request deployment. It does not run build scripts from the repository.

Create identities and directories

sudo groupadd --system sample-web
sudo useradd --system --home-dir /nonexistent --no-create-home \
  --shell /usr/sbin/nologin --gid sample-web --groups git sample-deploy
sudo usermod -aG sample-web www-data
sudo chmod -R g+rX /srv/git/sample.git
sudo install -d -m 2770 -o git -g git /var/lib/sample-deploy/queue
sudo install -d -m 0750 -o sample-deploy -g sample-web /srv/www/sample
sudo install -d -m 0750 -o sample-deploy -g sample-web /srv/www/sample/releases

Replace www-data with the actual read-only web-service user, and restart that service after changing its supplementary groups. Configure the server to read /srv/www/sample/current; do not give it write access to the releases.

Replace the simple hook with a queue-only hook

#!/bin/sh
set -eu

repo=/srv/git/sample.git
queue=/var/lib/sample-deploy/queue
deploy_ref=refs/heads/main
umask 027

while read -r oldrev newrev refname
do
    [ "$refname" = "$deploy_ref" ] || continue
    git --git-dir="$repo" cat-file -e "$newrev^{commit}" 2>/dev/null || continue

    temporary="$queue/.${newrev}.$$"
    printf '%s\n' "$newrev" >"$temporary"
    mv "$temporary" "$queue/$newrev"
    printf 'Queued deployment of %s\n' "$newrev"
done

Install the deployment program

Save the following as /usr/local/sbin/deploy-sample, owned by root and mode 0755:

#!/bin/sh
set -eu

repo=/srv/git/sample.git
queue=/var/lib/sample-deploy/queue
root=/srv/www/sample
releases=$root/releases
deploy_ref=refs/heads/main
temporary=
archive=

cleanup()
{
    [ -z "$archive" ] || rm -f -- "$archive"
    [ -z "$temporary" ] || rm -rf -- "$temporary"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

for job in "$queue"/*
do
    [ -f "$job" ] || exit 0
    commit=${job##*/}

    case "$commit" in
        ''|*[!0-9a-fA-F]*)
            printf 'Ignoring invalid deployment job: %s\n' "$commit" >&2
            rm -f -- "$job"
            continue
            ;;
    esac

    if ! git --git-dir="$repo" cat-file -e "$commit^{commit}" 2>/dev/null
    then
        printf 'Commit is unavailable: %s\n' "$commit" >&2
        rm -f -- "$job"
        continue
    fi

    current=$(git --git-dir="$repo" rev-parse "$deploy_ref^{commit}")
    if [ "$commit" != "$current" ]
    then
        printf 'Skipping superseded deployment: %s\n' "$commit"
        rm -f -- "$job"
        continue
    fi

    release=$releases/$commit
    if [ ! -d "$release" ]
    then
        temporary=$releases/.${commit}.$$
        archive=$releases/.${commit}.$$.tar
        install -d -m 0750 "$temporary"
        git --git-dir="$repo" archive --format=tar --output="$archive" "$commit"
        tar -xf "$archive" -C "$temporary"
        if find "$temporary" -type l -print -quit | grep -q .
        then
            printf '%s\n' 'Symlinks are not permitted in this static release.' >&2
            rm -f -- "$job"
            exit 1
        fi
        rm -f -- "$archive"
        archive=
        mv "$temporary" "$release"
        temporary=
    fi

    current=$(git --git-dir="$repo" rev-parse "$deploy_ref^{commit}")
    if [ "$commit" != "$current" ]
    then
        printf 'Branch advanced while preparing: %s\n' "$commit"
        rm -f -- "$job"
        continue
    fi

    next=$root/.current.$$
    ln -s "$release" "$next"
    mv -Tf "$next" "$root/current"
    rm -f -- "$job"
    printf 'Published %s\n' "$commit"
done

This exports tracked content rather than executing it. The two branch-tip checks discard superseded queue entries instead of letting hash-sorted filenames publish an older commit last. If a push lands during the final symlink switch, its queue entry remains and triggers another service run. If a build is required, add a separately reviewed build step with resource limits and an explicit threat model; a push is untrusted input even when the pusher is authenticated.

Add the path and service units

/etc/systemd/system/sample-deploy.path:

[Unit]
Description=Watch for queued sample deployments

[Path]
DirectoryNotEmpty=/var/lib/sample-deploy/queue
Unit=sample-deploy.service

[Install]
WantedBy=multi-user.target

/etc/systemd/system/sample-deploy.service:

[Unit]
Description=Publish a queued sample Git commit

[Service]
Type=oneshot
User=sample-deploy
Group=sample-web
SupplementaryGroups=git
ExecStart=/usr/local/sbin/deploy-sample
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadOnlyPaths=/srv/git/sample.git
ReadWritePaths=/srv/www/sample /var/lib/sample-deploy
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX

Validate and activate:

sudo systemd-analyze verify \
  /etc/systemd/system/sample-deploy.path \
  /etc/systemd/system/sample-deploy.service
sudo systemctl daemon-reload
sudo systemctl enable --now sample-deploy.path
sudo systemd-analyze security sample-deploy.service
sudo systemctl status sample-deploy.path

systemd-analyze verify catches unit-file errors; systemd-analyze security reports exposure implied by sandbox settings, but neither proves that the deployment program is correct. Test a real push and inspect:

sudo journalctl -u sample-deploy.service --since today
readlink -f /srv/www/sample/current

8. Verification and recovery checklist

After setup and after each meaningful change:

sudo sshd -t
sudo -u git git --git-dir=/srv/git/sample.git fsck --full
git ls-remote pi

Also verify these operational facts:

  • an interactive ssh git@host is refused;
  • a permitted push and clone work;
  • a tag or non-deploy branch does not publish;
  • deleting the deploy branch does not empty the site;
  • a hook or deployment error is visible in logs and does not masquerade as a rejected push;
  • the previous release can be selected by moving current back to its release directory;
  • bare repositories, hooks, SSH keys, unit files, and deployment scripts are included in backups; and
  • a restore has been tested on a separate path or machine.

What changed from the 2019 procedure

2019 archive detail 2026 maintained treatment
RSA key generated without a stated passphrase policy Use a dedicated Ed25519 key and document key ownership, passphrase use, restriction, and revocation
Direct editing of /etc/passwd Use chsh with the installed git-shell path
Repositories and deployment paths mixed among home directories Put bare repositories in /srv/git and deployments in /srv/www with explicit owners
master assumed everywhere Select main explicitly and filter the full ref name refs/heads/main
Placeholder GIT_DIR does not match the repository created earlier Use one consistent absolute repository path
A deployment permission error followed by a successful push line Explain that post-receive cannot roll back an accepted push
chown git:git offered as the general permission fix Limit that shortcut to low-risk static deployment; use a separate deployment identity for stronger isolation
Typographic dashes and quotes appear in copied command notes Preserve them only in the archive; maintained commands use literal ASCII options and quotes
Third-party host and private IP shown as copyable examples Treat them as historical strings, not live recommendations or endorsements

Authoritative references

Package versions and defaults change. Check the manuals installed on the Pi (man git-init, man githooks, man sshd_config, man systemd.exec) and validate against the actual versions before exposing or automating the server.

Complete 2019 source export (verbatim archive; do not execute)


1, Set up git envioronment

sudo apt-get install git

sudo adduser git


2, Generate public and private key

ssh-keygen -t rsa -C “user@lazying.art”


Client:

/home/pi/.ssh/id_rsa


 Server: add public key of each client to the file below

/home/git/.ssh/authorized_keys


3, Initialize server end envioronment

mkdir Git
cd Git
sudo git init –bare sample.git
sudo chown -R git:git sample.git

vim /etc/passwd

git:x:1001:1001:,,,:/home/git:/bin/bash
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell


4, Upload code to your github server

git config –global user.email “you@example.com”
git config –global user.name “Your Name”
git add .
git commit -m “init commit”
git push -u origin master


5, Set-up Hooks

vim sapmle.git/hooks/post-receive


 Add code below to post-receive

#!/bin/bash
TARGET=”/home/webuser/deploy-folder”
GIT_DIR=”/home/webuser/www.git”
BRANCH=”master”

while read oldrev newrev ref
do
# only checking out the master (or whatever branch you would like to deploy)
if [ “$ref” = “refs/heads/$BRANCH” ];
then
echo “Ref $ref received. Deploying ${BRANCH} branch to production…”
git –work-tree=$TARGET –git-dir=$GIT_DIR checkout -f $BRANCH
else
echo “Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed on this server.”
fi
done

chown git:git hooks/post-receive
chmod +x hooks/post-receive

Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 237 bytes | 23.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
remote: Ref refs/heads/master received. Deploying master branch to production…
remote: error: unable to create file main.py (Permission denied)
remote: error: unable to create file test.py (Permission denied)
remote: Already on ‘master’
To 192.168.1.108:/home/pi/Git/printer.git
2e6a796..6085cd0 master -> master


Causing of the hooks script cannot access deployment files, you should change the owner of deployment directory to git

chown git:git /path/to/deployment/directory/

Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 239 bytes | 21.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
remote: Ref refs/heads/master received. Deploying master branch to production…
remote: Already on ‘master’
To 192.168.1.108:/home/pi/Git/printer.git
6085cd0..b83c0d5 master -> master


________

1.在文件所在位置打开终端,输入如下命令,进行git全局设置:

git config –global user.name “用户名”
 git config –global user.email “用户邮箱”

1. 安装如下命令创建新的仓库,网址为自己新建项目的网址,cd转到自己要上传的项目文件夹:

git clone https://git.aiiage.com:9999/song.yl/ReID.git
 cd ReID
 touch README.md
 git add README.md
 git commit -m “add README”
 git push -u origin master

1. 对已经存在的文件夹进行操作, 可以不用cd命令转到文件夹,直接到文件夹下打开终端执行如下命令,其中git commit命令后引号里面的内容可以自己命名:

cd existing_folder
 git init
 git remote add origin https://git.aiiage.com:9999/song.yl/ReID.git
 git add .
 git commit -m “Initial commit”
 git push -u origin master

1. 对存在的git仓库进行操作。依然可以直接在所在文件夹直接打开终端执行命令:

cd existing_foloder
 git remote rename origin old-origin
 git remote add origin https://git.aiiage.com:9999/song.yl/ReID.git
 git push -u origin —all

git push -u origin –tags

## 作者:yllifesong
 来源:CSDN
 原文:https://blog.csdn.net/yllifesong/article/details/81041156
 版权声明:本文为博主原创文章,转载请附上博文链接!

Leave a Reply