Home / Alt manpages / git-daemon(1)

  • git-daemon(1)
  • User command
  • linux

Serve Read-Only Git Repositories with git daemon

You want to clone a repository between two machines on the same network, with no SSH keys and no hosting account. git daemon is the unauthenticated protocol built for exactly that. This guide builds a small export service, proves the access boundary works, then verifies it from a client. The example uses a temporary directory, port 19418, and Git 2.43.0 from the installed git-man package 1:2.43.0-1ubuntu7.3.

Allow about fifteen minutes. You need Git on the server and a shell on a client, or both roles on one test machine.

Nothing here needs elevated privileges: the commands use a temporary directory and a port above 1024. A real service on port 9418 may need service supervision, firewall changes and a dedicated account; those are deployment decisions, not reasons to run the daemon as root.

1. Check the installed command

Confirm the version and the subcommand syntax before building the test service. This is an ordinary read-only check:

$ git --version
git version 2.43.0
$ dpkg-query -W -f='${Package} ${Version}\n' git-man
git-man 1:2.43.0-1ubuntu7.3
$ git daemon -h

The installed manual describes git daemon as a simple TCP server, normally listening on port 9418. Its default service is upload-pack, which supports fetches, pulls, clones and git ls-remote. It is built for read-only use.

Checkpoint

Keep the version output. Option details can differ between Git releases, and this guide is written against the local 2.43.0 installation.

2. Create a repository and mark it for export

Make a bare repository inside a temporary export tree. A bare repository has the objects and refs directories the daemon expects, but no working tree:

$ export GIT_DAEMON_ROOT="$(mktemp -d /tmp/git-daemon-demo.XXXXXX)"
$ mkdir -p "$GIT_DAEMON_ROOT/repos"
$ git init --bare "$GIT_DAEMON_ROOT/repos/project.git"
$ touch "$GIT_DAEMON_ROOT/repos/project.git/git-daemon-export-ok"
$ find "$GIT_DAEMON_ROOT/repos/project.git" -maxdepth 1 -type f -printf '%f\n' | sort
HEAD
config
description
git-daemon-export-ok

That marker file is the important boundary. Without git-daemon-export-ok, the daemon refuses by default to export the repository, so creating a repository under an export root does not publish it by accident.

This example changes state under /tmp. When you have finished testing, remove the temporary tree only after checking the variable points at the expected path:

$ printf 'temporary tree: %s\n' "$GIT_DAEMON_ROOT"
$ test -n "$GIT_DAEMON_ROOT" && case "$GIT_DAEMON_ROOT" in /tmp/git-daemon-demo.*) echo 'path looks correct' ;; *) echo 'refusing cleanup' >&2; exit 1 ;; esac
$ rm -rf -- "$GIT_DAEMON_ROOT"
$ unset GIT_DAEMON_ROOT

Warning

The final command is destructive for that temporary tree. Do not adapt it to a broad directory or a production repository.

3. Start a constrained foreground daemon

Start the daemon in the foreground so its log stays visible and the process is easy to stop with Ctrl-C. The base path maps a client request for project.git to the repository below it, and the directory argument limits which paths can be served:

$ git daemon --reuseaddr --verbose \
    --base-path="$GIT_DAEMON_ROOT/repos" \
    --port=19418 "$GIT_DAEMON_ROOT/repos"
[PID] Ready to rumble

Leave this terminal running. --verbose reports connections and requested files, --reuseaddr allows a quick restart after a stop, and the custom port avoids competing with another Git daemon or needing a privileged port. The daemon still serves only repositories marked for export.

Open a second terminal and recreate the variable there with the actual printed path, for example:

$ export GIT_DAEMON_ROOT='/tmp/git-daemon-demo.A1b2C3'
$ test -d "$GIT_DAEMON_ROOT/repos/project.git"
$ git ls-remote "git://127.0.0.1:19418/project.git"
$ printf 'client status: %s\n' "$?"
client status: 0

An empty ls-remote result is normal for a newly created bare repository with no refs. The status is what matters here. In the daemon terminal, expect a request mentioning upload-pack and /project.git; the exact process IDs and connection ports vary.

4. Prove the export boundary

Stop the foreground daemon with Ctrl-C, remove the marker from the test repository, and start the same command again. This is a deliberate temporary change to demonstrate the default access check:

$ rm -- "$GIT_DAEMON_ROOT/repos/project.git/git-daemon-export-ok"
$ git daemon --reuseaddr --verbose \
    --base-path="$GIT_DAEMON_ROOT/repos" \
    --port=19418 "$GIT_DAEMON_ROOT/repos"
$ git ls-remote "git://127.0.0.1:19418/project.git"
fatal: remote error: access denied or repository not exported: /project.git

The wording may vary with the client, but the request should fail. Restore the marker before continuing:

$ touch "$GIT_DAEMON_ROOT/repos/project.git/git-daemon-export-ok"
$ git ls-remote "git://127.0.0.1:19418/project.git"
$ printf 'restored status: %s\n' "$?"
restored status: 0

Tip

The daemon's default error is intentionally vague. --informative-errors gives clients more detail, but can reveal whether an unexported repository exists, so leave it off on a service where repository-name disclosure matters.

5. Keep the service read-only

  • Leave receive-pack off. The manual says it permits anonymous pushes with no authentication, so anyone who can reach the service could push or remove refs. It suits only a closed LAN where that risk is accepted, and it is disabled by default.
  • upload-pack stays enabled by default. That is the read-only fetch service you actually want.
  • upload-archive, which serves remote git archive requests, is disabled by default too. Enable it for one repository only, in that repository's own config file:
    [daemon]
            uploadarch = true
    Record why it is needed, test it from an untrusted client, and undo it by removing the setting or setting it to false.
  • Do not use --export-all casually. It bypasses the per-repository marker and exports every directory under the daemon's allowed paths that looks like a Git repository.

6. Choose a production boundary

For a real read-only service, prefer an explicit repository root and a dedicated unprivileged account.

  • --base-path=/srv/git makes a request for team/project.git resolve under /srv/git/team/project.git.
  • --strict-paths makes requests match the supplied directory arguments exactly; it refuses to start if none are supplied.
  • --user=git and the optional --group=git make the daemon switch identity before serving requests. The names are looked up through the system account databases, not treated as numeric IDs. If you use this, make sure the account can read the repositories, and remember Git programs do not automatically get a reset HOME: set one explicitly if configuration in that account's home directory is required.
  • --inetd is incompatible with --detach, --port, --listen, --user and --group. Use a service manager or inetd only after the foreground test works.

If you detach, logging defaults to syslog. For a first deployment, foreground logging or an explicitly chosen log destination is easier to diagnose.

Done means

  • Versions checked. Git 2.43.0 and the installed git-man package were confirmed.
  • Export was deliberate. The repository was exported only after creating git-daemon-export-ok.
  • Client access proven. git ls-remote reached the daemon and returned status 0.
  • Boundary tested. Removing the marker made the same request fail, and restoring it fixed the request.
  • Write access stayed off. receive-pack and --export-all were left disabled.
  • Cleaned up. The foreground daemon was stopped before the temporary tree was removed.