A simple git server via ssh by dropbear
  • Shell 76.5%
  • Dockerfile 23.5%
Find a file
gobro 396b36129d
All checks were successful
ci/crow/push/crow Pipeline was successful
ci: add cosign.pub for verification
2026-06-13 19:53:00 -04:00
.crow.jsonnet ci: add dry_run test for crow ci to generate badge 2026-06-13 19:11:39 -04:00
.dockerignore ci: add cosign.pub for verification 2026-06-13 19:53:00 -04:00
.env.example feat: first public version 2026-06-12 23:03:54 -04:00
.gitignore feat: first public version 2026-06-12 23:03:54 -04:00
compose.yml feat: first public version 2026-06-12 23:03:54 -04:00
cosign.pub ci: add cosign.pub for verification 2026-06-13 19:53:00 -04:00
Dockerfile feat: add crow ci workflow 2026-06-13 00:57:38 -04:00
entrypoint.sh feat: first public version 2026-06-12 23:03:54 -04:00
LICENSE feat: first public version 2026-06-12 23:03:54 -04:00
README.en.md ci: add cosign.pub for verification 2026-06-13 19:53:00 -04:00
README.md ci: add cosign.pub for verification 2026-06-13 19:53:00 -04:00

Simple Git Server

status-badge

English | 简体中文

A minimal, self-hosted Git server over SSH, built on Alpine Linux + Dropbear. The whole image weighs in under 10 MB and runs comfortably in a 100 MB memory environment, which makes it ideal for serverless platforms that scale to zero (Azure Container Apps, Google Cloud Run) as well as home routers and embedded devices.

Background and a full deployment walk-through: https://blog.recursion-link.eu.org/simple-git-server

Why this exists

Hosting a private Git repository usually means paying for an always-on VM. By packaging a tiny SSH + Git service into a container that scales to zero, you can host private repos on a cloud free tier and pay essentially nothing:

  • Cheap — runs on Azure / Google Cloud Run free compute; storage for a typical source-code repo costs well under $1/month.
  • Persistent — your code lives on a mounted volume, so it survives container restarts and scale-to-zero events.
  • Scale to zero — the container shuts down when idle and spins back up when an SSH connection arrives, so you are billed only for active time.

Why Alpine + Dropbear (instead of Ubuntu + OpenSSH)

  • Alpine base image is ~5 MB vs ~70 MB+ for Ubuntu, dramatically improving serverless cold starts (faster pull and decompress).
  • Dropbear is a lightweight SSH server designed for embedded systems. It lives in a single small binary and uses only a few MB of memory per connection.

The result is a container image under 10 MB that still serves Git over SSH normally.

How it works

  • A git user (UID 1000) is created with /usr/bin/git-shell as its login shell, so SSH sessions can only run Git commands — no interactive shell.
  • On startup, entrypoint.sh:
    1. Installs the SSH host keys from the RSA_KEY / ED25519_KEY env vars (base64-decoded), or generates fresh ones if none are provided.
    2. Writes your authorized public keys from AUTHORIZED_KEYS into /auth/authorized_keys (kept outside /home/git so the home directory can be a mount).
    3. Initializes each repository listed in REPO_NAMES as a bare repo under /home/git/<name>.git (only if it does not already exist).
    4. Launches Dropbear in the foreground with password login, root login, and port forwarding all disabled.

Persisting RSA_KEY / ED25519_KEY across restarts is what keeps the host key stable, so clients don't see "host key changed" warnings every time the container scales from zero.

Container image

forgejo.goba.ip-dynamic.org/gobro/simple-git-server:latest

Multi-arch images (linux/amd64, linux/arm64) are built and pushed by the Crow CI pipeline on tag events, with an SPDX SBOM attestation and a cosign signature attached to the image index.

The public key used for signing lives in this repository at cosign.pub. Its SHA-256 fingerprint is:

c52fc605506bb1b8f2e39d57ac7810935311af6feab400b904e3083b951b8314

You can reproduce the fingerprint to confirm the key is authentic:

openssl pkey -pubin -in cosign.pub -outform DER | shasum -a 256
# Verify the signature (also covers the attached SBOM).
# From a clone, the key is right here:
cosign verify --key cosign.pub forgejo.goba.ip-dynamic.org/gobro/simple-git-server:latest

# Or fetch the key straight from the repo, without cloning:
cosign verify \
  --key https://forgejo.goba.ip-dynamic.org/gobro/simple-git-server/raw/branch/main/cosign.pub \
  forgejo.goba.ip-dynamic.org/gobro/simple-git-server:latest

# Inspect the SBOM
docker buildx imagetools inspect forgejo.goba.ip-dynamic.org/gobro/simple-git-server:latest --format '{{ json .SBOM }}'

Configuration

All configuration is via environment variables (see .env.example):

Variable Required Default Description
AUTHORIZED_KEYS Yes One or more SSH public keys allowed to connect (newline-separated). Without it the server warns and accepts no logins.
ED25519_KEY No Base64-encoded Dropbear ed25519 host private key. Set this to keep a stable host key across restarts.
RSA_KEY No Base64-encoded Dropbear RSA host private key.
REPO_NAMES No repo Space-separated list of repositories to create (e.g. repo1 repo2.git). A .git suffix is added if missing.
GIT_PORT No 22 Port Dropbear listens on inside the container.

Repository names are validated against ^[a-zA-Z0-9._-]+$; anything else is skipped.

Generating persistent host keys

Host keys must be in Dropbear format (not OpenSSH format). Generate and base64-encode them:

dropbearkey -t ed25519 -f dropbear_ed25519_host_key
dropbearkey -t rsa     -f dropbear_rsa_host_key

base64 -i dropbear_ed25519_host_key   # -> ED25519_KEY
base64 -i dropbear_rsa_host_key       # -> RSA_KEY

Paste the base64 output into ED25519_KEY and RSA_KEY in your .env.

Quick start (Docker Compose)

  1. Copy the example env file and fill it in:

    cp .env.example .env
    # edit .env: set AUTHORIZED_KEYS to your public key, optionally ED25519_KEY / RSA_KEY / REPO_NAMES
    
  2. The bundled compose.yml builds the image locally and maps container port 22 to host 2222:

    services:
      simple-git:
        build: .
        ports:
          - "2222:22"
        volumes:
          - git_repo:/home/git
        env_file: ".env"
    
    volumes:
      git_repo:
    

    To use the published image instead of building locally, replace build: . with:

        image: forgejo.goba.ip-dynamic.org/gobro/simple-git-server:latest
    
  3. Start it:

    docker compose up -d
    

Usage

Test connectivity (the host below is localhost for the Compose setup above):

ssh -T -p 2222 -i ~/.ssh/id_ed25519 git@localhost

A correct setup responds with:

fatal: Interactive git shell is not enabled.
hint: ~/git-shell-commands should exist and have read and execute access.

This is expected — git-shell refuses interactive logins but allows Git operations. Clone and push as usual:

git clone ssh://git@localhost:2222/home/git/repo1.git
cd repo1
# ... make changes ...
git push origin main

Deploying to Azure Container Apps

  • Storage — create an Azure File Share and mount it to /home/git so repositories persist across scale-to-zero events.
  • Ingress — Transport: TCP, Target port: 22, Exposed port: 22 (or another unused port).
  • Scale to zero — configure scale rules so the container starts on incoming SSH connections and shuts down when idle.

FAQ

  • 100.100.x.x IPs that connect and immediately disconnect — is this an attack? No. Those are Azure ingress health-check probes verifying port reachability. Log lines like Exit before auth from them can be safely ignored.
  • How long is a cold start? Roughly 1530 seconds for the first connection after scale-from-zero; subsequent operations are fast.
  • What does it actually cost? Azure's free tier includes 180,000 vCPU-seconds/month (~200 active hours at 0.25 vCPU). Storage for a ~1 GB repo is around $0.06/month.

Repository

Source: https://forgejo.goba.ip-dynamic.org/gobro/simple-git-server

License

GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE.