Deploy Django from a Private Git Repository with Deploy Keys
Let a Django server pull code from a private GitHub, GitLab, or Bitbucket repository with read-only deploy keys: SSH config aliases, known_hosts, private dependencies, and alternatives.
Problem statement
The server needs your private repository to deploy, and the quick solutions are all bad: copying a developer's personal SSH key to the server gives it access to every repository that developer can reach; forwarding an SSH agent exposes the developer's key to anyone with root on the server while connected; embedding a personal access token in the remote URL leaves a broad credential in .git/config.
A deploy key is an SSH key attached to a single repository, read-only by default. If the server is compromised, the attacker can read one repository and nothing else, and you can revoke that key without affecting anyone. This guide sets one up, including servers that need several private repositories and private Python packages.
- On the server, as the deploy user, generate a key:
ssh-keygen -t ed25519 -f ~/.ssh/myapp_deploy -N "". - Add the public key to the repository's deploy keys (GitHub: Settings → Deploy keys) without write access.
- Add an alias in
~/.ssh/configthat uses that key withIdentitiesOnly yes. - Add the host's verified key to
~/.ssh/known_hosts. - Clone with the alias:
git clone git@github-myapp:example/myapp.git. - Use one deploy key per repository; for many repositories, consider a GitHub App or machine user instead.
Step-by-step solution
1. Generate the key on the server
sudo -iu deploy
ssh-keygen -t ed25519 -f ~/.ssh/myapp_deploy -C "deploy@web1 myapp" -N ""
cat ~/.ssh/myapp_deploy.pub
The key has no passphrase because automated deploys cannot type one; its protection is file permissions (600, owned by the deploy user) and the narrow scope of the key. Generate it on the server so the private key never leaves the machine.
2. Register it on the Git host
- GitHub: repository Settings → Deploy keys → Add deploy key. Leave Allow write access unticked.
- GitLab: project Settings → Repository → Deploy keys. Keys are read-only unless write access is granted.
- Bitbucket: repository Repository settings → Access keys. Access keys are read-only.
A GitHub deploy key can be attached to only one repository. That constraint is the reason the SSH config alias in the next step exists.
3. Configure an SSH alias
# ~/.ssh/config (deploy user)
Host github-myapp
HostName github.com
User git
IdentityFile ~/.ssh/myapp_deploy
IdentitiesOnly yes
IdentitiesOnly yes makes SSH offer only this key. Without it, SSH may offer another key first; GitHub accepts the first key it recognises, and if that key belongs to a different repository, the clone fails with "Repository not found".
chmod 600 ~/.ssh/config ~/.ssh/myapp_deploy
4. Trust the host key properly
ssh-keyscan fetches whatever key the host presents, including an attacker's in a man-in-the-middle scenario. Compare it with the fingerprints your Git host publishes (GitHub lists them in its documentation) before trusting it:
ssh-keyscan -t ed25519 github.com > /tmp/github_key
ssh-keygen -lf /tmp/github_key
# compare the SHA256 fingerprint with the published one, then:
cat /tmp/github_key >> ~/.ssh/known_hosts
Test the connection:
ssh -T git@github-myapp
GitHub responds with a greeting that names the repository the key belongs to, and exits without a shell.
5. Clone and deploy
git clone git@github-myapp:example/myapp.git /srv/myapp/repo
git -C /srv/myapp/repo remote -v
For an existing checkout, switch the remote:
git -C /srv/myapp/repo remote set-url origin git@github-myapp:example/myapp.git
git -C /srv/myapp/repo fetch
Deploy scripts then fetch and check out a tag or commit as usual. For automation, prefer fetching a specific ref over git pull, which merges whatever is on the branch.
6. Several repositories or private dependencies
If requirements.txt includes private packages from Git:
internal-lib @ git+ssh://git@github-internal-lib/example/internal-lib.git@v2.3.0
create a second key for that repository and a second alias:
Host github-internal-lib
HostName github.com
User git
IdentityFile ~/.ssh/internal_lib_deploy
IdentitiesOnly yes
Pin a tag or commit in the requirement, so a new push to the library does not change what production installs.
When the number of repositories grows, one key per repository becomes tedious. Alternatives:
- A GitHub App installed on the needed repositories, issuing short-lived installation tokens used over HTTPS. Best for CI-driven deploys.
- A machine user with read access to specific repositories and its own SSH key. Simple, but the key's scope is broader and the account needs managing.
- Build artifacts (Docker images or wheels built in CI) so servers never access Git at all.
7. Docker builds
Do not copy a key into an image. With BuildKit, forward it only for the step that needs it:
# syntax=docker/dockerfile:1
RUN --mount=type=ssh pip install -r requirements.txt
docker build --ssh default=$HOME/.ssh/internal_lib_deploy .
The key is available during that RUN step and is not stored in any layer. The build environment also needs the host key in known_hosts (for example with ssh-keyscan inside the step, after fingerprint verification, or by mounting a prepared file).
Verification check:
ssh -T git@github-myapp
git -C /srv/myapp/repo fetch --dry-run && echo "fetch ok"
ls -l ~/.ssh/
Keys should be 600 and owned by the deploy user, and the deploy key in the Git host's settings should show a recent "last used" time and no write access.
Explanation
SSH authentication to Git hosts identifies you by key. A deploy key is registered on one repository rather than on a user account, so possessing it grants exactly that repository's read access. The SSH config alias maps a made-up hostname to the real host plus a specific key, which is how one server can hold several single-repository keys without SSH offering the wrong one.
Edge cases / notes
Key rotation and revocation
When decommissioning a server, delete its deploy key on the Git host. Rotate keys periodically by adding the new key, switching the server, and removing the old key, which gives a zero-downtime rotation.
Write access
Deploy keys should stay read-only. A server that needs to push (for example, committing generated files) is a sign the process should move to CI.
When to automate this
Provisioning tools can generate the key on each new server, register it through the Git host's API, and write the SSH config, so a rebuilt server can deploy without manual steps. For teams, moving deploys to CI and shipping built artifacts removes Git access from servers entirely.
Next steps
- Use the checkout in Deploy Django with Gunicorn and Nginx on Ubuntu.
- Automate the release steps with Deploy Django with Fabric.
- Review SSH settings with the Linux Server Hardening Checklist for Django.
- If a private dependency is missing at runtime, see Fix ModuleNotFoundError When Starting Django with Gunicorn.
FAQ
Why does git clone say "Repository not found" with a deploy key?
SSH offered a different key that GitHub recognised first, often a deploy key for another repository. Use an SSH alias with IdentitiesOnly yes and the correct IdentityFile.
Can one deploy key access several repositories?
Not on GitHub; each deploy key is tied to one repository. Use one key per repository, a machine user, or a GitHub App.
Should a deploy key have a passphrase?
Automated deploys cannot enter one, so deploy keys usually have none. Limit the risk with read-only access, one repository per key, and strict file permissions.
Is SSH agent forwarding a safe way to deploy?
It is convenient but lets anyone with root on the server use your personal key while you are connected. Deploy keys avoid that exposure.