- Shell 76.5%
- Dockerfile 23.5%
|
|
||
|---|---|---|
| .crow.jsonnet | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| compose.yml | ||
| cosign.pub | ||
| Dockerfile | ||
| entrypoint.sh | ||
| LICENSE | ||
| README.en.md | ||
| README.md | ||
Simple Git Server
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
gituser (UID 1000) is created with/usr/bin/git-shellas its login shell, so SSH sessions can only run Git commands — no interactive shell. - On startup,
entrypoint.sh:- Installs the SSH host keys from the
RSA_KEY/ED25519_KEYenv vars (base64-decoded), or generates fresh ones if none are provided. - Writes your authorized public keys from
AUTHORIZED_KEYSinto/auth/authorized_keys(kept outside/home/gitso the home directory can be a mount). - Initializes each repository listed in
REPO_NAMESas a bare repo under/home/git/<name>.git(only if it does not already exist). - Launches Dropbear in the foreground with password login, root login, and port forwarding all disabled.
- Installs the SSH host keys from the
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)
-
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 -
The bundled
compose.ymlbuilds the image locally and maps container port22to host2222: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 -
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/gitso 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.xIPs that connect and immediately disconnect — is this an attack? No. Those are Azure ingress health-check probes verifying port reachability. Log lines likeExit before authfrom them can be safely ignored.- How long is a cold start? Roughly 15–30 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.