Git vs GitHub vs a self-hosted Git server
Git is a program that runs offline. GitHub is one remote. A VPS can hold a second. What separates them, and the SSH steps to add that remote.
Git vs GitHub vs a Git server: the short answer
Git is a program installed on a computer. It runs with no account and no network. GitHub is a hosted service that keeps a copy of a Git repository and wraps a website around it. A Git server is any machine the developer controls that keeps another copy, reached over SSH (secure shell). All three carry the word "git", which is why a first-year student meets them in the same lab session and leaves thinking they are one thing.
The difference stays academic for a while. It becomes real on four specific days: the day a client requires the code to sit on infrastructure they control, the day the continuous integration (CI) allowance runs out, the day GitHub cannot be reached and a commit still has to be made, and the day one account holds the only copy of four years of coursework. This guide separates the three, then walks the path from one GitHub account to a second remote on a virtual private server (VPS), using SSH keys rather than passwords.
What Git does on a laptop with the network off
git init creates a single hidden directory named .git at the top of the project folder. Every commit, branch, tag and the whole history live inside that directory, on that disk. Nothing is sent anywhere.
mkdir demo && cd demo
git init
git config user.name "Example Student"
git config user.email "student@example.com"
echo "hello" > README.md
git add README.md
git commit -m "First commit"
git log --onelinegit log --oneline prints one line holding a short commit hash and the message. That works with the wifi switched off, in a lab with the cable unplugged, on a train. No step in that block opens a network socket, so no step can fail because of the network.
A "remote" is a saved name for a URL where another copy of the same repository lives. A fresh repository has none, and git remote -v prints nothing at all. GitHub enters the picture only when someone adds a remote and runs git push. That single fact removes most of the confusion. Git is complete without GitHub, and GitHub is one of many places a Git repository can be copied to.
Distributed means every clone is a full repository. A laptop that cloned a project last week still holds the entire history of that project, including every commit made before the clone happened. It is not a thin checkout of a central copy in the way older version control systems worked.
What GitHub adds on top of Git
Pull requests, issue tracking, review threads, Actions workflows, releases, forks, organisation permissions and dependency alerts. None of these are Git features. Git has no concept of a pull request. The closest thing it ships is git request-pull, which prints a plain text summary meant to be pasted into an email. The review interface, the merge button and the comment threads are GitHub's product, built above Git rather than inside it. what GitHub is as a hosted service covers that side in detail.
The storage half is ordinary Git. A git clone of a GitHub URL produces a repository that no longer needs GitHub for commits, branches, merges or history. What a clone does not carry is the discussion around the code, because that never lived in Git in the first place.
What a GitHub student account includes, as of August 2026
GitHub Free includes private repositories, so coursework does not have to be public to be stored. Public repositories run GitHub-hosted CI runners at no charge, and private repositories draw on a monthly included allowance instead.
The exact allowances for Actions minutes and package storage change, and any number printed in a tutorial is wrong within months. GitHub's own pricing page at https://github.com/pricing is the figure worth trusting, and this guide deliberately does not repeat those numbers.
The GitHub Student Developer Pack at https://education.github.com/pack adds more once enrolment is verified. Verification asks for evidence that names the student and the current academic term, usually an institutional email address, with an uploaded document as the fallback when the email domain is not recognised. The offers inside the pack come from other companies and the list changes often, so the pack page is the only reliable inventory of it.
One warning matters more than any allowance. A student account is a single account. A lost multi-factor authentication (MFA) device, a suspended account or a mistaken organisation transfer removes access to every private repository at once. GitHub issues recovery codes when two-factor authentication is switched on, and keeping those codes somewhere other than the laptop is the difference between a delay and a permanent lockout. A second remote is the other half of that insurance.
Why the difference stops being academic
A client requires the code to stay on infrastructure they control. Some contracts and most public sector tenders name where source code and build artefacts may sit. A hosted account cannot satisfy a clause that names a server and a country. A bare repository on a VPS in that country can, and the developer keeps working the same way.
The CI allowance runs out. Workflows on private repositories stop starting once the included minutes are spent, until the next billing period or until a paid spending limit is raised. The repository itself is untouched. Only the automation stops. One fix keeps GitHub as the remote and moves the compute somewhere cheaper: a self-hosted GitHub Actions runner on a VPS executes the same workflow files on a machine billed by the month rather than by the minute.
GitHub is unreachable. During an outage, or on a campus network that blocks the outbound connection, git commit keeps working because commit is a local operation. Only git push and git fetch fail. Work continues and the commits queue up locally until the network returns. A second remote on a VPS turns that queue into a push that still succeeds.
One account holds the only copy. Two remotes mean two independent copies with two independent failure modes. Nothing about the daily workflow changes except one extra push.
How to add a second remote on a VPS
The smallest useful Git server is OpenSSH plus the git package. There is no web interface and no database. Git speaks its own protocol over an SSH connection, so an account that can already log in over SSH can already host repositories.
On the VPS, running Ubuntu 24.04, as a user with sudo:
sudo apt update && sudo apt install -y git
sudo adduser --disabled-password --gecos "" git
sudo install -d -m 700 -o git -g git /home/git/.ssh
sudo install -d -m 755 -o git -g git /home/git/repos--disabled-password creates the account with no usable password, so the only way in is a key. Next comes the developer's public key. The .pub file is the public half and is safe to copy around. The file without .pub never leaves the laptop.
sudo tee -a /home/git/.ssh/authorized_keys < /tmp/id_ed25519.pub > /dev/null
sudo chown git:git /home/git/.ssh/authorized_keys
sudo chmod 600 /home/git/.ssh/authorized_keysThose modes are required. sshd runs with StrictModes enabled by default, so it ignores the key file if /home/git or /home/git/.ssh is writable by group or others. The client then sees Permission denied (publickey). with no explanation at all, while the server log states the real reason: Authentication refused: bad ownership or modes for directory /home/git/.ssh.
Create the repository as a bare repository:
sudo -u git git init --bare /home/git/repos/project.gitBare means no working tree. The directory holds what a .git folder normally holds, and nothing else. That matters, because Git refuses to push into the branch a normal repository has checked out, and prints remote: error: refusing to update checked out branch: refs/heads/main. A bare repository has no checked out branch, so every push is accepted.
Back on the laptop, inside the existing project:
git remote -v
git remote add vps git@203.0.113.10:repos/project.git
git push vps --all
git push vps --tags
git ls-remote vpsgit remote -v before the change prints the GitHub remote twice, once marked (fetch) and once (push). After the change it prints four lines. git ls-remote vps prints one line per ref on the server with its full hash, which is the proof that the objects landed. Empty output means nothing was pushed.
The path repos/project.git is relative to the git user's home directory, so it resolves to /home/git/repos/project.git. An absolute path works as well. Note that git push vps --all sends every branch but not tags, which is why the tag push is a separate command.
If the VPS listens on a port other than 22, the short git@host:path form cannot carry it. The full URL form can: ssh://git@203.0.113.10:2222/home/git/repos/project.git. Moving that port is a common hardening step, and changing the SSH port on a SELinux and firewalld system covers the part that is easy to miss on Rocky and AlmaLinux.
Why SSH keys replace passwords for both remotes
SSH (secure shell) is the protocol both remotes use here, and how SSH authenticates a connection explains the handshake underneath it. The practical summary: a key pair is generated once, the public half is handed to each server, and the private half proves identity without ever being transmitted.
ssh-keygen -t ed25519 -C "student@example.com"
cat ~/.ssh/id_ed25519.pubThe GitHub side takes that same public key, pasted into the SSH keys page of the account settings. The test is one command:
ssh -T git@github.comA working key prints Hi username! You've successfully authenticated, but GitHub does not provide shell access. That is success, not an error. GitHub deliberately gives no shell on that account.
Passwords over HTTPS stopped working for Git operations on 13 August 2021, and the message says so exactly: remote: Support for password authentication was removed on August 13, 2021. HTTPS now needs a personal access token, which is a password with a scope list and an expiry date attached to it. Keys avoid that renewal cycle. Several machines and several servers turn key handling into its own bookkeeping problem, which keeping SSH keys organised across machines works through.
Many campus and office networks block outbound port 22. The symptom is ssh: connect to host github.com port 22: Connection timed out. GitHub answers SSH on port 443 as well, so four lines in ~/.ssh/config fix it:
Host github.com
Hostname ssh.github.com
Port 443
User gitssh -T git@github.com then succeeds over 443. When the key looks correct but the connection is still refused, the specific causes behind Permission denied (publickey) lists the ones that look identical from the client side.
A VPS with SSH open to the internet receives login attempts within hours of first boot. Two settings matter most: PasswordAuthentication no in a file under /etc/ssh/sshd_config.d/, and fail2ban configured for SSH on Ubuntu 24.04 to drop hosts that keep trying.
How to push to GitHub and the VPS in one command
Two remotes with two names means two pushes. Git can also send a single push to several URLs under one remote name:
git remote set-url --add --push origin git@github.com:student/project.git
git remote set-url --add --push origin git@203.0.113.10:repos/project.git
git remote -vgit remote -v now shows two (push) lines for origin. git push origin main writes to both, in the order listed. The GitHub URL has to be added explicitly as the first one, because adding any push URL replaces the implicit default that came from the fetch URL. Fetching still uses the fetch URL only, so this is a write fan-out and not a two-way sync.
A mirror is the stricter option when the second copy exists purely as a backup:
git clone --mirror git@github.com:student/project.git project.git
cd project.git
git remote set-url --push origin git@203.0.113.10:repos/project.git
git fetch -p origin
git push --mirrorThat copies every ref, including branches and tags nobody has checked out. --mirror also deletes refs on the destination that no longer exist at the source, which is correct for a backup and destructive if the direction is ever reversed by accident.
What breaks, and the message printed each time
A wrong directory. fatal: not a git repository (or any of the parent directories): .git means the command ran outside any repository. Nothing is broken and the network is not involved.
Name resolution failed. fatal: unable to access 'https://github.com/student/project.git/': Could not resolve host: github.com is DNS (domain name system). Commits still work; only the transfer failed.
The key was not accepted. git@203.0.113.10: Permission denied (publickey). means the server rejected every key the client offered. ssh -v git@203.0.113.10 lists which key files were tried. On the server, sudo journalctl -u ssh -n 50 names the real cause, most often the directory modes described earlier.
The login worked and the path did not. fatal: 'repos/project.git' does not appear to be a git repository, followed by fatal: Could not read from remote repository., means either git init --bare never ran or the path is wrong relative to the git user's home directory.
The remote is ahead. ! [rejected] main -> main (fetch first) means the server holds commits the local clone does not. A git fetch, then a merge or a rebase, has to happen before the push.
The target is not bare. remote: error: refusing to update checked out branch: refs/heads/main means the server repository was created with git init instead of git init --bare.
What a self-hosted Git server does not give
A bare repository over SSH gives storage and access control by key. It gives no issue tracker, no pull request interface, no web view of the code and no CI. Those come from software installed on top of it. Gitea and Forgejo are small and run in one container. GitLab is much larger and expects far more memory. cgit and Gitweb publish a read-only view and nothing more. the self-hosted Git server options and what each one costs to run compares them properly.
Ownership also means backups, and here that part is genuinely easy, because a bare repository is a plain directory:
sudo tar czf /root/project-$(date +%F).tar.gz -C /home/git/repos project.gitCopy that file off the box and the repository is recoverable in full. What does not come for free is uptime, disk space and security updates. There is no support team behind it. For a server that should answer no inbound connection at all, publishing a service without opening any inbound port is the other direction, and it moves that trust to the tunnel provider rather than removing it.
For most people the honest answer is both. GitHub keeps the collaboration tools and the free CI for public work. The VPS keeps a copy that no account suspension and no outage can take away. A reader who wants to own the whole thing, review interface included, should start from the comparison of self-hosted Git server options and pick the one that matches the hardware already available.
FAQ
Is Git the same thing as GitHub?
No. Git is a version control program installed on a computer, and it works with no account and no network connection. GitHub is a company's hosted service that stores a copy of a Git repository and adds pull requests, issues and Actions around it. A repository cloned from GitHub keeps every commit and every branch even if GitHub disappears, because each clone holds the full history.
Can a student use GitHub without paying?
Yes. GitHub Free includes private repositories, and the GitHub Student Developer Pack at https://education.github.com/pack adds more offers once enrolment is verified with a document or email that names the student and the current term. The included Actions minutes and storage change over time, so https://github.com/pricing is the number to check rather than any figure quoted in a guide. This page was written in August 2026 and leaves those figures out on purpose.
Does git commit work without an internet connection?
Yes. Commit writes objects into the local .git directory and moves one branch pointer. Nothing leaves the machine. Only git push, git fetch, git pull and git clone need the network, so an outage delays sharing the work rather than doing it.
Why does the repository on the server have to be bare?
Because a normal repository has a branch checked out, and Git refuses to change files under a working tree that someone might be editing. The push is rejected with remote: error: refusing to update checked out branch: refs/heads/main. git init --bare creates a repository with no working tree, so pushes to any branch are accepted.
Should the VPS remote be called origin?
Remote names are local to each clone, so this is a matter of habit rather than a rule. Keeping origin on whichever remote collaborators treat as authoritative avoids surprises in shared instructions, and naming the second one vps or backup makes the intent obvious in git remote -v.