Deploy a Private GitHub Repository to an Ubuntu VPS
Learn how to connect an Ubuntu VPS to a private GitHub repository using SSH deploy keys, clone the project into /opt, and prepare it for Docker Compose deployment.
1. Why Private Repository Deployment Needs Care
A private GitHub repository is usually the source of truth for your production application. When a VPS needs to pull that code, the server needs a secure way to authenticate without depending on a developer's personal laptop.
The clean approach for a single repository is usually a GitHub deploy key. A deploy key is an SSH key attached directly to one repository. The public key is added to GitHub, and the private key stays on the server.
GitHub deploy keys are read-only by default. For normal production deployments, keep them read-only so the VPS can pull code but cannot push changes back to the repository.
This guide assumes you already have an Ubuntu VPS with Docker installed and a deploy user prepared from the earlier articles in this series.
2. Confirm the Deployment User
Log in as the deploy user you created in the Docker setup guide:
ssh deploy@YOUR_VPS_IPConfirm the current user:
whoamiExpected output:
deployAlso confirm that Docker commands work for this user:
docker ps
docker compose versionDo not clone production code as root. Keep application files owned by the deploy user so CI/CD and manual maintenance behave predictably.
3. Choose the Production Directory
Production projects should live in a predictable server location. A common pattern is to keep application repositories under /opt.
For this guide, we will use:
/opt/myappIf the directory does not exist yet, create it from an administrator account:
sudo mkdir -p /opt/myapp
sudo chown -R deploy:deploy /opt/myappThen return to the deploy user and check ownership:
ls -ld /opt/myappThe directory should be owned by deploy.
4. Generate a GitHub Deploy Key on the VPS
While logged in as deploy, create a dedicated SSH key for this one repository:
ssh-keygen -t ed25519 -C "myapp-vps-deploy-key" -f ~/.ssh/myapp_deploy_keyWhen asked for a passphrase, you can leave it empty for automated server pulls. Protect the server account carefully because this private key can read the repository.
This creates two files:
~/.ssh/myapp_deploy_key
~/.ssh/myapp_deploy_key.pubNever copy the private key into GitHub, a chat message, documentation, or a public repository. Only the .pub file belongs in GitHub.
Set strict permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/myapp_deploy_key
chmod 644 ~/.ssh/myapp_deploy_key.pub5. Add the Public Key to GitHub
Print the public key from the VPS:
cat ~/.ssh/myapp_deploy_key.pubCopy the full output. In GitHub, open the repository, then go to Settings, Deploy keys, Add deploy key.
Use a clear title, for example:
Production VPS - myappPaste the public key into the Key field and leave write access disabled unless your deployment process intentionally needs to push to the repository.
A deploy key can only be used with one repository. If one server needs access to multiple private repositories, create a separate key for each repository.
6. Configure SSH to Use the Deploy Key
Create or edit the deploy user's SSH config:
nano ~/.ssh/configAdd this configuration:
Host github.com-myapp
HostName github.com
User git
IdentityFile ~/.ssh/myapp_deploy_key
IdentitiesOnly yesProtect the config file:
chmod 600 ~/.ssh/configThe alias github.com-myapp lets this repository use the dedicated deployment key without changing global SSH behavior.
7. Test GitHub SSH Access
Test the SSH connection using the alias:
ssh -T git@github.com-myappOn first connection, SSH may ask you to trust GitHub's host key. Confirm the fingerprint against GitHub's published SSH key fingerprints before accepting.
A successful test usually says that authentication worked and that GitHub does not provide shell access.
GitHub's SSH test command may return a non-zero shell exit code even when authentication succeeds. Read the message, not only the exit code.
If you see Permission denied (publickey), check that the public key was added to the correct repository and that the SSH config points to the matching private key.
8. Clone the Private Repository into /opt
Move to the parent directory and clone the private repository. Replace OWNER and REPO with your real GitHub owner and repository name.
cd /opt
git clone git@github.com-myapp:OWNER/REPO.git myappThen enter the project directory:
cd /opt/myappConfirm the remote URL uses the SSH alias:
git remote -vYou should see a remote similar to:
origin git@github.com-myapp:OWNER/REPO.git (fetch)
origin git@github.com-myapp:OWNER/REPO.git (push)9. Set Ownership and File Permissions
If the repository was cloned as deploy into a directory owned by deploy, permissions may already be correct. It is still worth checking:
pwd
whoami
ls -laFrom an administrator account, you can repair ownership if needed:
sudo chown -R deploy:deploy /opt/myappAvoid broad permissions such as chmod 777. Production deployment problems should be fixed through ownership and clear service boundaries, not by making every file writable by everyone.
10. Create the Production Environment File
Most Docker Compose applications need environment variables. Keep real production secrets on the server, not committed to Git.
If the project includes an example file, copy it:
cp .env.example .env
nano .envFill in production values for domains, API keys, database passwords and service secrets.
Do not commit .env files containing real secrets. The repository should include .env.example for structure, while the production .env stays on the VPS.
Restrict the environment file:
chmod 600 .env11. Verify Docker Compose Files
Before starting anything, confirm that Docker Compose can read the project configuration:
docker compose configThis expands and validates the Compose file. It is useful for catching indentation errors, missing environment variables and invalid service definitions.
If the project uses Makefile commands, inspect the available deployment targets:
make helpFor a Docker-first project, the deploy user should be able to run the production build/start commands without installing application dependencies directly on the host.
12. Pull Updates Safely
Once the repository is cloned, future updates should be predictable and fast-forward only unless you deliberately choose another release strategy.
cd /opt/myapp
git fetch origin main
git checkout main
git pull --ff-only origin mainThe --ff-only flag prevents Git from creating merge commits during production pulls. If the server branch has diverged, stop and fix the deployment state intentionally.
A simple manual deployment after pulling might look like:
docker compose up -d --build
docker compose psLater, CI/CD can run the same small set of commands over SSH.
13. Troubleshooting GitHub Access
If cloning fails with Permission denied (publickey), start with these checks:
- the public key was added to the correct GitHub repository
- the deploy key is enabled in repository settings
- the SSH config points to the matching private key
- the remote URL uses the alias github.com-myapp
- the private key file is readable only by the deploy user
ssh -vT git@github.com-myappThe verbose SSH output shows which identity file SSH is trying. That is often the fastest way to spot a wrong key path or alias.
If port 22 is blocked by a network provider, GitHub also documents SSH access over port 443 through ssh.github.com. Use that only when normal SSH connectivity is unavailable.
14. Final Checklist
At this point, the VPS can securely pull your private repository and is ready for the next deployment step: running the production Docker Compose stack behind a reverse proxy with HTTPS.
Continue the Ubuntu VPS Series
Next Article
Caddy on Ubuntu: Reverse Proxy, Domain & Automatic HTTPS
Coming SoonAlso Coming