October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
VGSources
Blog

Fix Git Deployment Permission Denied Errors on a Linux VPS

A practical, security-conscious guide to Git permission denied errors on Linux VPS deployments, covering SSH keys, deploy-key scope, sudo pitfalls, filesystem ownership, hooks, CI and rollback.
Length12 min Posted Quest giverVGSources Team
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Git deployment failures labeled “permission denied” come from two different places: the remote SSH connection or the VPS filesystem. Permission denied (publickey) means the Git server rejected authentication; a local Permission denied usually means the account running Git cannot traverse, write, or modify the checkout and its .git directory. Identify which class you have before changing permissions. Then run the deployment as a dedicated non-root account, verify the exact SSH key and repository scope, and repair only the ownership, modes, hooks, and destination paths that the process actually needs.

Start by classifying the error

Do not begin with chmod -R 777 or repeated sudo git pull commands. Capture the complete failing command, its output, and the Unix identity that executed it. The same words can describe unrelated faults.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Typical message Where the failure occurs First check
Permission denied (publickey) SSH authentication or authorization at the remote Git provider Remote URL, SSH user, selected private key, and the key’s account or repository scope
error: unable to create file ... Permission denied Local VPS filesystem Deploying identity, checkout ownership, directory modes, and parent-directory traversal
fatal: cannot create .git/index.lock Local .git directory, often ownership or a stale lock Who owns .git, whether another Git process is running, and whether the account can write there
rsync: ... Permission denied (13) Remote transfer destination or metadata operation SSH account, destination ownership, directory modes, and timestamp or metadata options

A remote authentication error is not fixed by changing the VPS checkout’s mode bits. Conversely, a key that authenticates successfully cannot make an account write to a directory it does not own.

Confirm the checkout, remote, and executing account

Record the identity that actually ran Git

Interactive shells, cron, service managers, bare-repository hooks, and CI sessions often use different accounts. In the failing context, run:

#1 Best Overall
id
whoami
pwd
git rev-parse --show-toplevel
git remote -v

If a hook or CI job fails, put equivalent logging in that job rather than testing only from your own shell. A successful manual git pull proves nothing about a hook running as a service account.

Inspect the remote URL

For an SSH remote, the usual form is git@host:owner/repository.git. GitHub’s documented convention is to connect as the git user; your personal account name is represented by the key, not by replacing git in the SSH URL. Check for a typo in the host, owner, repository, or protocol:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
git remote -v
git config --get remote.origin.url

An HTTPS remote has a different credential path and will not be diagnosed by an SSH key test. Make sure you are testing the same remote and host that the failing Git command uses.

Fix Permission denied (publickey)

Test the provider with verbose SSH output

Run the test as the same Unix account that performs deployment. For GitHub, the documented diagnostic command is:

ssh -vT [email protected]

Use the equivalent git user and host for another provider. The verbose trace shows which configuration files are read, which identities are offered, and whether the server accepts a key. It also exposes an unexpected hostname, a disabled key, or an agent offering the wrong identity.

Select a non-default key explicitly

If the deployment key is not the account’s default identity, test it directly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
ssh -i ~/.ssh/deploy_key -vT [email protected]

For repeatable deployments, define the key in the SSH configuration used by the deploy account:

Host production-git
    HostName github.com
    User git
    IdentityFile ~/.ssh/deploy_key
    IdentitiesOnly yes

Set the Git remote to the alias, for example git@production-git:owner/repository.git, or use an equivalent GIT_SSH_COMMAND for a one-off test. The private key must be readable by the deploying account and not by unrelated users. Its matching public key must be attached to the intended account or repository.

Check account and repository scope

A key can be valid yet unauthorized for the repository named in the remote URL. GitHub deploy keys are repository-scoped credentials: they are read-only by default, may be granted write access, and do not expire automatically. A key installed for one repository therefore does not automatically authorize another. Verify that the key is attached to the correct repository and that its read or write capability matches the operation.

For a deployment that must access many repositories, a GitHub App is the more managed model because it supports fine-grained installation permissions and controlled credential management. Whichever model you use, grant only the repositories and operations required.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check host keys without hiding the real problem

An unknown or changed host-key prompt is distinct from public-key authentication. Run the test interactively first so the account’s known_hosts file is populated and any host-key warning is visible. Do not “solve” an authentication failure by disabling host-key checking globally.

Stop mixing root and non-root Git environments

GitHub explicitly advises against using sudo or other elevated privileges with Git. Running sudo git pull changes more than the file owner: root may use /root/.ssh, a different SSH agent, different known-hosts, different Git configuration, and a different credential helper. It can also create root-owned files in a checkout that the normal deploy account must update later.

Create or select one dedicated deployment account and use it consistently for the checkout, SSH key, hooks, build steps, and application restart commands. If a narrowly controlled privileged operation is unavoidable, separate it from Git and make the environment explicit. For example, let the deploy account update a release directory, then use a restricted service-manager rule to restart the application; do not run the entire pull as root.

Diagnose local VPS filesystem permissions

Inspect every path component

Git needs to traverse every parent directory and write the files it changes. Inspect the project, its metadata, and the path leading to it:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
id deploy
stat -c '%A %U:%G %n' /srv /srv/www /srv/www/app /srv/www/app/.git
namei -l /srv/www/app
find /srv/www/app -maxdepth 2 -printf '%M %u:%g %pn' | less

namei -l is particularly useful when the checkout looks writable but a parent directory lacks the execute (traverse) bit. The account running Git must be able to enter each directory, read files it needs, and write the checkout and .git metadata.

Repair ownership for the intended account

After confirming the correct deployment path, assign ownership to the deploy account rather than making the tree world-writable:

sudo chown -R deploy:deploy /srv/www/app

Use the actual account and group names in your system. Apply this only after checking that the path is the intended checkout; a mistaken recursive ownership change can affect unrelated data. If a web server only reads the published files, give it group or read access instead of ownership and write access.

Use group access when two services genuinely need it

When a deploy process writes releases and a web service reads them, a shared group can avoid broad permissions:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo groupadd --system webdeploy
sudo usermod -aG webdeploy deploy
sudo usermod -aG webdeploy www-data
sudo chgrp -R webdeploy /srv/www/app
sudo find /srv/www/app -type d -exec chmod 2775 {} +
sudo find /srv/www/app -type f -exec chmod 0664 {} +

Reload the affected service or start a new session so supplementary groups take effect. Do not apply these example modes blindly to private keys, executable files, configuration containing secrets, or directories that should not be shared. A deployment layout with immutable, versioned releases is often safer than having the web server write into the Git checkout.

Check for stale locks and read-only filesystems

If the error names .git/index.lock, first ensure no Git process is running:

ps -ef | grep '[g]it'
ls -l .git/index.lock

Remove a stale lock only after verifying that no operation is active. If writes fail throughout the mount, inspect the mount state and available space:

findmnt -T /srv/www/app
df -h /srv/www/app
df -i /srv/www/app

A read-only mount, exhausted inodes, filesystem errors, ACLs, or mandatory access controls can look like ordinary mode-bit problems. Correct the underlying condition instead of widening permissions.

Make push-to-deploy hooks reliable

Install and execute the hook as the right user

A common design uses a bare repository on the VPS and a post-receive hook to check out the target branch into a web or release directory. The hook file must be executable and owned so the repository service can run it:

chmod +x /srv/git/app.git/hooks/post-receive
ls -l /srv/git/app.git/hooks/post-receive

Keep the hook owned by the account that receives the push, and ensure that account can write both the bare repository and the destination. A hook launched by SSH may run with a minimal environment, not the environment in your interactive shell.

Use absolute paths and explicit settings

A deterministic hook sets its path, branch, work tree, Git directory, and log destination. A minimal pattern is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#!/bin/sh
set -eu
PATH=/usr/bin:/bin
export PATH
LOG=/var/log/app-deploy.log
{
  echo "$(date -u +%FT%TZ) deploy start"
  /usr/bin/git --work-tree=/srv/www/app --git-dir=/srv/git/app.git checkout -f main
  /usr/bin/git --work-tree=/srv/www/app --git-dir=/srv/git/app.git rev-parse HEAD
  echo "$(date -u +%FT%TZ) deploy complete"
} >> "$LOG" 2>&1

Adjust paths and the branch to your layout. The hook’s account must be able to append to the log, write the work tree, and run any build or restart command. If a build creates root-owned artifacts, the next deployment can fail even though the checkout itself is correctly owned.

Verify the result instead of trusting the push

After a push, check the commit actually checked out and read the hook log:

git --work-tree=/srv/www/app --git-dir=/srv/git/app.git rev-parse HEAD
tail -n 50 /var/log/app-deploy.log
stat -c '%U:%G %a %n' /srv/www/app

For safer rollbacks, deploy into a new, versioned release directory and switch a symlink only after checkout and build steps succeed. This keeps an incomplete deployment from replacing a working live tree and provides a clear previous release to restore.

Fix CI and rsync permission failures

Match the key on both sides

For GitHub Actions or another CI runner connecting to the VPS, store the private key in the runner’s secret configuration and place the corresponding public key in the target VPS user’s ~/.ssh/authorized_keys. The private key must be supplied to the job with restrictive permissions, and the SSH command must use the intended user and host.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

On the VPS, check the SSH directory and file ownership and modes:

stat -c '%A %U:%G %n' ~/.ssh ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Do not overwrite an existing authorized-keys file without preserving other required keys. Confirm that the destination account, not root, owns the home directory and SSH files.

Separate transfer permission from metadata permission

An rsync transfer can copy file contents while failing to set timestamps, ownership, or other metadata. If the error is Permission denied (13), inspect the destination owner and the exact options used. When preserving timestamps is unnecessary and the filesystem rejects that operation, an option such as --no-times can avoid the metadata update; use it only when losing timestamp preservation is acceptable. Do not add --delete or ownership-preservation flags without understanding their effect on the destination.

Test the destination as CI does

Reproduce the connection with the same SSH user, key, working directory, and destination path. A directory may be writable while one file is immutable, owned by another account, or protected by an ACL. Use namei -l, stat, and a harmless test file in the destination to isolate the failing operation, then remove the test file.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose a deployment design that limits permission problems

Design Credential scope Account and write model Rollback and logging Main risk to control
Manual pull in a checkout One user’s SSH key and repository access Deploy account writes its own checkout Depends on shell history and local logs Operators accidentally use sudo or the wrong key
Bare repository with post-receive Push credential for the repository Hook account writes a work tree or release path Explicit hook logs; rollback requires a known commit or release Non-executable hooks, minimal PATH, or root-created files
CI SSH deployment Runner secret and VPS authorized key Dedicated remote account receives files or runs a script CI logs and recorded job commits Secret/key mismatch, wrong destination owner, metadata flags
Staged releases with a symlink switch Repository or CI credential, ideally least privilege Deploy account writes a new release; web service reads it Fast switch-back to the previous release and per-release logs Incorrect shared-group modes or a failed post-switch restart
Multi-repository application account Managed app-level permissions, such as an installed GitHub App One controlled service identity, not root Centralized deployment records and rotation procedures Granting more repositories or operations than required

Evaluate a design by repository scope, credential rotation and expiry, least-privilege controls, whether a dedicated account performs the work, rollback support, logging, and whether Git writes directly into the live web root. Direct live checkouts are simpler but make ownership drift and partial updates more visible; staged releases add setup but isolate the web service from Git metadata.

A repeatable end-to-end repair procedure

  1. Capture the failure. Save the complete output, command, host, path, and execution context. Record id and pwd from that context.
  2. Classify it. Treat (publickey) as remote SSH authentication; treat a path, .git, hook, or rsync destination error as local access until proven otherwise.
  3. Verify the remote. Run git remote -v, then test the same host with ssh -vT git@host and the exact key if needed.
  4. Verify authorization. Confirm the public key belongs to the intended account or repository and that a repository-scoped deploy key has the required read or write capability.
  5. Choose one deploy identity. Remove accidental sudo from Git operations and ensure the account’s SSH files, checkout, hooks, and logs are consistent.
  6. Inspect filesystem traversal and ownership. Use stat, namei -l, find, mount, space, and inode checks. Repair the specific owner, group, ACL, or mount issue.
  7. Validate hooks or CI separately. Confirm executable hooks, absolute paths, safe PATH, authorized keys, destination ownership, and acceptable rsync metadata options.
  8. Run a non-destructive test. Fetch or deploy a known commit, inspect the resulting commit and logs, and confirm that generated files have the intended owner.
  9. Document the working identity and paths. Record which account, key, repository scope, checkout or release directory, hook, and rollback command are supported. This prevents the next operator from reverting to root.

Common fixes that make the incident worse

  • Making everything world-writable: it hides the owner or group mistake and lets unrelated local users modify code or secrets.
  • Running one successful pull with sudo: it may use root’s key and leave root-owned files that break the next normal deployment.
  • Replacing the remote URL repeatedly: it cannot authorize a key that is attached to the wrong repository or account.
  • Deleting every lock file: an active Git process can corrupt the operation; identify the owner and process first.
  • Disabling host-key verification: it removes an important SSH integrity check and does not fix a rejected public key.
  • Assuming a shell test represents a hook or CI job: those environments commonly have different users, keys, PATH, home directories, and permissions.
  • Deploying directly into a mutable web root: application-generated or root-created files can make the next checkout inconsistent; staged releases reduce that coupling.

When the message still says permission denied

If the key is accepted but Git still cannot read or write, compare the failing process with a successful one line by line: Unix user and groups, HOME, SSH configuration, current directory, remote URL, mount, and environment PATH. Check POSIX ACLs with getfacl when mode bits appear correct, and inspect service or security logs if a mandatory access-control policy is enforcing a denial. For a hook, log the effective user, working directory, and command exit status before the failing step. For CI, print non-secret connection and destination details and test a harmless operation rather than exposing the private key.

Once the immediate operation succeeds, perform a second deployment from the real trigger—hook, scheduler, or CI runner—and then a rollback or redeploy of the previous known commit. Permission repair is complete only when the supported path works without elevated Git commands and leaves files owned for the next deployment.

Further reference

The Pro Git book’s server chapter explains SSH access, server-side repositories, authorized_keys, and executable hooks. Use it to understand the deployment model, but verify every account, path, key, and mode on the live VPS rather than copying a generic server layout unchanged.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.