DEV Community

Marek
Marek

Posted on • Originally published at nubecode.eu

The Right Way to Deploy Private GitHub Repos to Your VPS

Deploying code from a private repository to a VPS is something a lot of developers sort of know how to do — but most tutorials either rely on personal SSH keys or Personal Access Tokens. That works, but it gives your server much more access than it actually needs.

In this guide you’ll learn how to set up a repository-specific SSH deploy key for your VPS so you can:

  • Clone your private repository securely
  • Pull updates without storing personal credentials on the server
  • Keep production access scoped narrowly and safely

This method is slightly more advanced than a basic SSH clone, but it’s well worth it if you care about security and clean operations.

What you'll learn:

  • How to create repository-specific SSH deploy keys,
  • Why deploy keys are more secure than personal credentials,
  • How to configure SSH for multiple GitHub identities,
  • The right way to structure your deployment directory,
  • How to maintain and rotate deploy keys safely

Time required: ~10–20 minutes
Skill level: Beginner to intermediate (comfortable with SSH and basic Linux)
What you'll need: SSH access to your VPS with sudo privileges and administrator access to your GitHub repository
Prerequisites note: This tutorial assumes you have SSH access to your server. If you haven't set up your firewall yet, I recommend checking out my guide on securing SSH with UFW first.

Why This Matters
There are several ways to authenticate to GitHub from a server:

  • Using your personal SSH key (this is risky, because it ties the server to your personal account )
  • Using a Personal Access Token (it works, but provides overly broad access )
  • Using a Repository-Specific Deploy Key (it’s ideal because it provides scoped access)

Deploy keys follow the principle of least privilege: the server gets just enough access to pull the code it needs, no more.

Step 1 — Prepare Your Deployment Environment
First create a dedicated deployment user.
Rather than cloning as root or your own account, it’s better to use a dedicated user and by dedicated user, I mean a Linux system user created specifically to run and deploy a single application or repository — not a human login account. From my experience it is a good idea to use a name of the app/repository you’re going to deploy.

sudo adduser --system --group yourappname
sudo mkdir -p /opt/yourappname
sudo chown yourappname:www-data /opt/yourappname
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • Deployment files are owned by a service user
  • Permissions stay clear and restricted

Why /opt and not /home, /srv, or somewhere else?
TL;DR: /opt makes it immediately obvious what is system-owned and what you deployed yourself.

In Linux, different top-level directories have different intentions. Using the right one is about keeping your server understandable six months from now.

What /opt actually is
/opt stands for optional software.

Historically (and still today), it’s meant for:

  • software not managed by the OS package manager
  • applications you deploy yourself
  • self-contained services that live outside the base system That describes a manually deployed web application perfectly.

Why /opt is a good fit for deployed applications
Putting your application in /opt/yourappname has several advantages:

a. Clear separation from the operating system
Your app is not part of the OS.

  • /bin, /usr, /lib → operating system
  • /opt → things you added

That means:

  • OS upgrades won’t touch your app
  • you won’t accidentally overwrite system files
  • backups and restores are simpler

b. Clear separation from user home directories
Using /home for production apps is tempting, but misleading.
Home directories are meant for:

  • human users
  • shell config
  • personal files A deployment user is not a human.

By using /opt:

  • there’s no expectation of interactive usage
  • no confusion about “who owns what”
  • no accidental exposure via home-directory defaults

c. Predictable layout for multi-app servers
If you later deploy more applications, /opt scales cleanly:
/opt/
├── yourappname/
├── anotherapp-1/
├── anotherapp-2/

Each app:

  • has its own directory
  • has its own system user
  • can have its own service, ports, and permissions

This makes:

  • audits easier
  • troubleshooting faster
  • future automation simpler

d. Works well with permissions and service users
The combination /opt/yourappname owned by yourappname:www-data means:

  • the deploy user controls the code
  • the web server can read what it needs
  • root is not involved in daily operations

That’s exactly what you want on a production server.

Why not /srv?
You could use /srv, and some people do.
However:

  • /srv is traditionally used for data served directly (FTP, NFS, static web roots)
  • many distros barely use it in practice
  • fewer people recognize it instantly and /opt is more universally understood for application installs.

The key idea
The goal is to make it obvious what is system, what is application, and who owns it.
Using /opt helps you do exactly that.

Step 2 — Generate a Repository-Specific SSH Deploy Key

Switch to the deployment user:
sudo su - yourappname
Generate a new SSH key specifically for this repo:
ssh-keygen -t ed25519 -C "github-deploy-key-yourappname" -f ~/.ssh/id_ed25519_deploy_yourappname

What this does: Creates a modern ed25519 key (more secure than RSA) with a descriptive comment and a specific filename so it doesn't conflict with other keys.

When prompted for a passphrase, you can leave it empty for automated deployments — security comes from strict file permissions and the key being limited to a single repository.

Display the public key:
cat ~/.ssh/id_ed25519_deploy_yourappname.pub
Expected output: You should see something starting with ssh-ed25519 AAAA... followed by your comment.

Copy this entire output — you'll paste it into GitHub next.

Step 3 — Register the Deploy Key on GitHub

  1. Open your repository on GitHub
  2. Go to Settings → Deploy keys
  3. Click Add deploy key
  4. Paste the public key you generated
  5. Give it a sensible title, e.g., “VPS Deploy Key”
  6. Do NOT enable write access — this key should be read-only

This gives your VPS the ability to read (clone/pull) this repository only.

Step 4 — Configure SSH on the Server
On the yourappname user, create or edit the SSH config:

nano ~/.ssh/config
Add:

Host github-deploy
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_deploy_yourappname
    IdentitiesOnly yes
Enter fullscreen mode Exit fullscreen mode

This tells SSH to use the correct key just for this host alias.
Test it:
ssh -T github-deploy
If it says you’ve successfully authenticated (but can’t shell), you’re good.

Step 5 — Clone the Repository
If you’re not logged in as the deployment user, switch to it first using sudo su - yourappname.
As the deployment user:

cd /opt/yourappname
git clone git@github-deploy:yourusername/yourappname.git
Enter fullscreen mode Exit fullscreen mode

That command uses the custom github-deploy host alias — so Git uses the specific deploy key.

Step 6 — Verify Permissions and Security
Make sure the .ssh directory is locked down:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_deploy_yourappname
chmod 644 ~/.ssh/id_ed25519_deploy_yourappname.pub
chmod 600 ~/.ssh/config
Enter fullscreen mode Exit fullscreen mode

Now:
ls -la ~/.ssh/
You should see just the expected key, config, and correct permissions.

Step 7 — Pulling Updates Later
When you need to update the app:

sudo su - yourappname
cd /opt/yourappname
git pull origin main
Enter fullscreen mode Exit fullscreen mode

No passwords, no tokens — just the deploy key doing its job.

If you have a systemd service (e.g., Gunicorn – I’ll cover it in the next blog article), restart it after the pull:
sudo systemctl restart yourappname

Step 8 — Key Rotation and Maintenance
Over time, you may want to:

  • Rotate the deploy key (generate a new one and update GitHub)
  • Revoke older deploy keys from GitHub
  • Monitor your repository’s key list for unused entries Because this key is repo-specific, revoking it only affects this deployment — not your entire GitHub account.

Common Pitfalls & Quick Checks
Wrong remote URL?
Make sure the Git remote uses your custom host:
git remote -v
It should show:

origin git@github-deploy:yourusername/yourappname.git (fetch) 
origin git@github-deploy:yourusername/yourappname.git (push) 
Enter fullscreen mode Exit fullscreen mode

Should NOT show: git@github.com (this would use the wrong key)

SSH connection issues?
Run verbose SSH to debug:
ssh -vT github-deploy
What this command does:
-v → Verbose output — Shows what SSH is doing step by step: which key it tries, which config entry it uses, where authentication fails
-T → Disable pseudo-terminal allocation — Tells SSH "Don't try to open a shell, just test authentication"
This is important for GitHub because GitHub does not provide shell access. A successful connection will still exit immediately, but with a clear authentication message.

Successful output looks like:
Hi yourusername/yourappname! You've successfully authenticated, but GitHub does not provide shell access.

When to use this command:

  • SSH works locally but fails on the server
  • Git can't clone or pull
  • You suspect the wrong SSH key is being used
  • You want to confirm which identity file is selected

Common error messages and fixes
Permission denied (publickey) — The deploy key isn't registered on GitHub, or SSH is using the wrong key
Could not resolve hostname github-deploy — The SSH config file has a typo or wasn't saved properly
Host key verification failed — You need to accept GitHub's host key: run ssh-keyscan github.com >> ~/.ssh/known_hosts

Permission problems?
If you see "Permission denied" when trying to clone check file permissions:
ls -la ~/.ssh/

It should show:

drwx------  (700) for .ssh directory
-rw-------  (600) for private keys
-rw-r--r--  (644) for public keys
-rw-------  (600) for config file
Enter fullscreen mode Exit fullscreen mode

If permissions are wrong, fix them:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_deploy_yourappname
chmod 644 ~/.ssh/id_ed25519_deploy_yourappname.pub
chmod 600 ~/.ssh/config

Enter fullscreen mode Exit fullscreen mode

Wrapping Up
Using a repository-specific deploy key gives you a clean, secure way to pull private code onto a server without exposing broad access credentials. It's the professional default for small VPS deployments before you add CI/CD or automation.
The principle is simple: give each server exactly the access it needs, nothing more. If this deploy key is ever compromised, you can revoke it on GitHub without affecting your other repositories or your personal access.

Have you ever accidentally exposed credentials on a server? What's your preferred method for managing deploy access? Do you use deploy keys, CI/CD pipelines, or something else? Let me know in the comments!

If you found this helpful, you might also want to check out the other articles in this series:

Top comments (0)