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.

Pulling code with a deploy keyServer keyed25519, no passphraseSSH config aliaspicks the right keyGit hostone repo, read-onlyRelease checkoutfetch and deployA deploy key opens exactly one repository; a leaked key cannot reach anything else.
  1. On the server, as the deploy user, generate a key: ssh-keygen -t ed25519 -f ~/.ssh/myapp_deploy -N "".
  2. Add the public key to the repository's deploy keys (GitHub: Settings → Deploy keys) without write access.
  3. Add an alias in ~/.ssh/config that uses that key with IdentitiesOnly yes.
  4. Add the host's verified key to ~/.ssh/known_hosts.
  5. Clone with the alias: git clone git@github-myapp:example/myapp.git.
  6. 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.
what a stolen key could reachPersonal key on the server✕every repository you can access✕usually with write access✕revoking it locks you out tooDeploy key✓one repository only✓read-only unless you tick write✓revoke it without touching anyoneOne key per server and repository keeps a breach smalldjango-deployment.com
A deploy key can only be attached to one repository, which is exactly the point.

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
one alias per repositoryHostgithub-myappHostName github.comUser gitIdentityFile ~/.ssh/myapp_deployIdentitiesOnly yes1231Clone git@github-myapp:org/myapp.git to pick this key.2The deploy key for this one repository.3Offer only this key, so another key cannot claim the connection first.Without an alias, the first key GitHub accepts wins, often the wrong repositorydjango-deployment.com
Add one Host block per repository when a server needs several.

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
does the key end up in the image?Key in image or historyCOPY id_ed25519✕yes, in a layerARG with the key✕yes, in the build historyRUN --mount=type=ssh✓no, forwarded for one stepDeleting the key in a later step does not remove it from earlier layersdjango-deployment.com
Build with docker build --ssh default and the key stays on the build host.
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

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.

Keep going