---
title: "GitHub Actions deploy to VPS: step-by-step pipeline | StreetHosting"
description: "Deploy to a VPS with GitHub Actions: non-root deploy user, dedicated SSH key, secrets, rsync upload, app restart and concurrency to avoid conflicts."
url: "https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps"
type: "page"
language: "en-US"
---

VPS · 10 min · Intermediate

Published on Sep 28, 2026 · Updated on Sep 28, 2026

# Deploy to your VPS automatically on every push with GitHub Actions

Every push to the main branch can build, upload and restart your application on the VPS without anyone opening a terminal. Here is how to set up that flow with a non-root deploy user, well-guarded secrets and protection against simultaneous deploys.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Linux administration](https://streethosting.com.br/en/guides/topics/linux) [Deploying and running apps](https://streethosting.com.br/en/guides/topics/deploy) [Automation and webhooks](https://streethosting.com.br/en/guides/topics/automation)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=GitHub%20Actions%20deploy%20to%20VPS%3A%20step-by-step%20pipeline&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps "Share on LinkedIn") [](https://wa.me/?text=GitHub%20Actions%20deploy%20to%20VPS%3A%20step-by-step%20pipeline%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fgithub-actions-deploy-to-vps "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps.md)

In this guide 8 sections

* [How the deploy works](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#como-funciona)
* [Deploy user and dedicated key](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#usuario-deploy)
* [Repository secrets](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#secrets)
* [The complete workflow](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#workflow)
* [Restarting without giving the pipeline root](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#reinicio)
* [Environments and concurrency](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#environments-concurrency)
* [Common errors](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#erros-comuns)
* [Which VPS to use](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#qual-vps)

Quick answer

To **deploy to a VPS with GitHub Actions**, create a non-root deploy user on the server with its own SSH key, store host, user, private key and known\_hosts as secrets and write a workflow that checks out, builds, uploads the files with rsync and restarts the application. Allow that user only the restart command in sudoers and use concurrency so two deploys never run at the same time.

## How the deploy works[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#como-funciona)

GitHub Actions runs the workflow on a temporary GitHub machine called a runner. It downloads the code, installs dependencies, runs the tests and produces the build. At the end, it opens an SSH connection to your VPS, copies the result and tells the application to restart. The VPS needs no Git, no GitHub credential and no build tool: it only receives finished files.

This design has two practical advantages. The heavy build processing stays off the server, which keeps serving users while the next version compiles. And the VPS never stores a token with access to the repository, so a break-in on the server does not turn into access to your code.

1. A push to the main branch triggers the workflow.
2. The runner checks out, installs dependencies, tests and builds.
3. The runner loads the deploy key from the secrets.
4. rsync sends the files to the VPS over the SSH connection.
5. A remote command installs the production dependencies and restarts the service.

The examples use a Node.js application running as a systemd service in `/srv/minha-app`, but the structure works for Python, PHP, Go or a static site: only the build and the restart command change. If the application does not run as a service yet, start with [PM2 or systemd to keep the application online](https://streethosting.com.br/en/guides/vps/pm2-vs-systemd-nodejs).

## Deploy user and dedicated key[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#usuario-deploy)

The pipeline must never log in as root. Create a dedicated user, with no password, that only accepts key login, and give it the application folder:

`sudo adduser --disabled-password --gecos "" deploy sudo mkdir -p /srv/minha-app sudo chown deploy:deploy /srv/minha-app`

On your computer, generate a key pair exclusively for GitHub. Do not reuse the key you use day to day: if it ever leaks through the pipeline, you revoke only that one.

`ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_github -N ""`

The command creates `deploy_github` (private, goes to GitHub) and `deploy_github.pub` (public, goes to the VPS). Install the public one on the deploy user:

`sudo mkdir -p /home/deploy/.ssh sudo nano /home/deploy/.ssh/authorized_keys # paste the contents of deploy_github.pub sudo chown -R deploy:deploy /home/deploy/.ssh sudo chmod 700 /home/deploy/.ssh sudo chmod 600 /home/deploy/.ssh/authorized_keys`

Test from your computer with `ssh -i ./deploy_github deploy@IP_DA_VPS`. If the key pair concept is still new to you, the guide on [passwordless SSH key on a VPS](https://streethosting.com.br/en/guides/vps/passwordless-ssh-login-vps) explains the public and private halves, and the one on [a sudo user on a Linux VPS](https://streethosting.com.br/en/guides/vps/create-sudo-user-linux-vps) shows how to take root out of daily use.

Password login must be disabled in SSH. A deploy user that accepts a password is as good a target as root. With a mandatory key and [Fail2ban protecting SSH](https://streethosting.com.br/en/guides/vps/fail2ban-ssh-vps-setup), the port can stay open to GitHub's runners, which use wide, changing IP ranges that are hard to list in a firewall.

## Repository secrets[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#secrets)

Secrets are encrypted on GitHub, only reach the workflow and show up masked in the logs. Register the four of them under Settings, Secrets and variables, Actions. If you are going to use a production environment, as the section further down shows, register them inside it.

| Secret               | Contents                                                 | How to get it                                           |
| -------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
| DEPLOY\_HOST         | IP or domain of the VPS                                  | VPS control panel or the domain's A record              |
| DEPLOY\_USER         | Name of the deploy user                                  | The user created in the previous step                   |
| DEPLOY\_SSH\_KEY     | The whole private key, including the BEGIN and END lines | Contents of the deploy\_github file                     |
| DEPLOY\_KNOWN\_HOSTS | Public key of the VPS's SSH server                       | Output of the keyscan below, verified on the VPS itself |

known\_hosts exists to avoid a very common dangerous shortcut: turning off host verification with `StrictHostKeyChecking=no`. Without that check, any machine impersonating your VPS receives the code and the session. Generate the line from a trusted network:

`# on your computer: fetch the server key and show its fingerprint ssh-keyscan -t ed25519 IP_DA_VPS ssh-keyscan -t ed25519 IP_DA_VPS | ssh-keygen -lf - # inside the VPS: the real fingerprint ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`

If the two fingerprints match, paste the line the first command printed into the secret. With SSH on another port, add `-p 2222` to the keyscan, because the stored line includes the port.

Host and user are not real secrets and could live in variables, which show up readable in the interface. Keeping all four as secrets also works and leaves everything in one place.

## The complete workflow[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#workflow)

Create the file `.github/workflows/deploy.yml` in the repository:

`name: Production deploy on: push: branches: [main] workflow_dispatch: permissions: contents: read concurrency: group: deploy-producao cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest environment: producao timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 cache: npm - name: Install, test and build run: | npm ci npm run test --if-present npm run build - name: Load SSH key env: SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} run: | install -m 700 -d ~/.ssh printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts - name: Upload files env: DEST: ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} run: | rsync -az --delete \ --exclude ".git" --exclude "node_modules" --exclude ".env" \ -e "ssh -i ~/.ssh/deploy_key" \ ./ "$DEST:/srv/minha-app/" - name: Restart the application env: DEST: ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} run: | ssh -i ~/.ssh/deploy_key "$DEST" \ "cd /srv/minha-app && npm ci --omit=dev && sudo systemctl restart minha-app.service"`

* **on:** fires on every push to main and adds a button to run it by hand from the Actions tab, useful for republishing without a new commit.
* **permissions:**the workflow's automatic token gets read access to the code only. The deploy does not need to write anything to the repository.
* **Load SSH key:** the secrets come in through environment variables instead of being interpolated straight into the script, which keeps a special character in the value from breaking or altering the command.
* **rsync with delete:** removes from the server whatever left the repository. The excludes protect the production `.env` and the dependencies folder from being deleted. Option details in [transferring files with SCP and rsync](https://streethosting.com.br/en/guides/vps/transfer-files-to-vps-scp-rsync).
* **Restart:**installs only the production dependencies and restarts the service. If any command fails, the job turns red and you get GitHub's notification.

The `@v7` versions of the checkout and Node actions were the latest as of September 2026. Check the README of each one before copying and use the same Node version on the runner that is installed on the VPS. For a Next.js project, the flow is the same; the build details are in [hosting Next.js on a VPS](https://streethosting.com.br/en/guides/vps/host-nextjs-on-vps).

## Restarting without giving the pipeline root[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#reinicio)

Restarting a systemd service requires privilege. The wrong way out is putting the deploy user in the sudo group, which turns the GitHub key into an administrator key. The right way is allowing exactly one command, with no password, in a dedicated sudoers file:

`sudo visudo -f /etc/sudoers.d/deploy-minha-app # file contents, a single line: deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart minha-app.service`

visudo validates the syntax before saving, which avoids a broken sudoers that locks everyone out of sudo. Check the result with `sudo -l -U deploy`. The rule only applies to the identical command: with the `.service` suffix it works, without it it does not, and any other command asks for a password the deploy user does not even have.

The service lives at `/etc/systemd/system/minha-app.service`. Adjust the Node path and the entry file to your project:

`[Unit] Description=My Node application After=network.target [Service] User=deploy WorkingDirectory=/srv/minha-app Environment=NODE_ENV=production ExecStart=/usr/bin/node dist/server.js Restart=on-failure [Install] WantedBy=multi-user.target`

Enable it once with `sudo systemctl daemon-reload && sudo systemctl enable --now minha-app.service`. From then on, the pipeline is what restarts it.

### Alternative with PM2[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#alternativa-pm2)

If the application runs under PM2 as the deploy user itself, no sudo rule is needed: the last step becomes `pm2 reload minha-app`. In cluster mode, reload swaps the processes one by one without dropping connections; in fork mode, it behaves like a restart. An administrator sets up `pm2 startup` and `pm2 save` once so the processes come back after a reboot.

## Environments and concurrency[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#environments-concurrency)

Two GitHub features turn a deploy script into a predictable process.

* **Environment:** the job declares `environment: producao`. The deploy secrets can live inside the environment, and then only jobs that declare it can see them. You can restrict which branches publish to it and, depending on the account plan, require approval from one person before the job starts. If the environment does not exist, the first run creates it with no rules at all; configure it under Settings, Environments.
* **Concurrency:** with the `deploy-producao` group, only one deploy runs at a time. If two pushes arrive in sequence, the second waits for the first to finish, instead of two rsyncs writing to the same folder and two restarts tripping over each other.

In the default behavior, the group keeps at most one pending run. If three pushes arrive while a deploy is running, the middle one is cancelled and only the most recent proceeds, which for deploys is usually what you want, since it contains the earlier commits. If you need to publish every commit in order, add `queue: max` to the block. Avoid `cancel-in-progress: true` in production: it interrupts an upload or a restart halfway through.

In a private repository, environments depend on your GitHub account plan: on the free plan, they only exist in public repositories, and rules such as required approval have even more restricted availability. Check the [GitHub environments documentation](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) before relying on a specific rule.

## Common errors[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#erros-comuns)

When something fails, the job log points to the exact step. These are the stumbles that show up most in the first week:

| Symptom                                   | Likely cause                                                               | Fix                                                                            |
| ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Host key verification failed              | known\_hosts empty, wrong or generated for another port                    | Generate it again with keyscan, including the port, and check the fingerprint  |
| Permission denied (publickey)             | Public key missing or wrong permissions on the .ssh folder                 | Review authorized\_keys, the deploy owner and modes 700 and 600                |
| npm: command not found in the remote step | Node installed with nvm does not load in a non-interactive SSH session     | Install Node from the system package or call the binary by its full path       |
| sudo: a password is required              | Command differs from the one allowed in sudoers                            | Use exactly the same path, verb and service name                               |
| Application comes up with old code        | Service points to another folder or the build landed in an excluded folder | Check the rsync destination and the service's WorkingDirectory                 |
| Uploads disappeared from the server       | rsync with delete and no exclude for the data folder                       | Exclude the folder from the upload or keep data outside the application folder |

On the VPS, `journalctl -u minha-app.service -n 50` shows why the service did not come up after the restart. Also note that this flow copies over the folder that is running: during the seconds rsync takes, the disk holds a mix of old and new files. Since the restart only happens at the end, this rarely causes a problem. To publish to a new folder, with a health check and automatic rollback, follow the guide on [how to set up CI/CD on a VPS](https://streethosting.com.br/en/guides/vps/set-up-ci-cd-on-vps).

## Which VPS to use[](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#qual-vps)

With the build running on GitHub, the VPS only needs resources to run the application and absorb the restart. For a Node API or site with the database on the same machine, 4 GB of RAM is a comfortable starting point; small projects fit in 2 GB.

* **Xeon E5-2680 v4 VPS:** 2 vCPU and 2 GB for R$ 26.00, or 3 vCPU, 4 GB and 40 GB NVMe for R$ 43.00. Delivers more vCPU per real spent, good for several applications and parallel workers.
* **Ryzen 9 9950X VPS:** 2 vCPU, 4 GB DDR5 and 40 GB NVMe for R$ 66.00, with clocks up to 5.7 GHz. Worth it when the response time of each request matters or when you plan to build on the VPS itself with a self-hosted runner.

Both lines are in São Paulo, with Anti-DDoS included, root access and activation within 60 seconds after payment by Pix, boleto or card. If the project grows, the upgrade is done through the control panel and charges only the prorated difference for the cycle. Compare the configurations on the [StreetHosting VPS page](https://streethosting.com.br/en/vps).

In this guide

* [How the deploy works](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#como-funciona)
* [Deploy user and dedicated key](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#usuario-deploy)
* [Repository secrets](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#secrets)
* [The complete workflow](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#workflow)
* [Restarting without giving the pipeline root](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#reinicio)
* [Environments and concurrency](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#environments-concurrency)
* [Common errors](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#erros-comuns)
* [Which VPS to use](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps#qual-vps)

## Frequently asked questions

Is it safe to store the SSH key in GitHub secrets?

It is the recommended path. Secrets are encrypted, show up masked in the logs and are not handed to workflows triggered by pull requests from forks. The rest of the risk is controlled on the VPS: the key must be exclusive to the deploy and belong to a non-root user, with sudo allowed only for the restart command.

Do I need to allow GitHub's IPs in the VPS firewall?

It is not practical. GitHub-hosted runners use wide IP ranges that change often. The usual approach is to keep the SSH port open with key-only login and Fail2ban active. If you need to close the port, use a self-hosted runner on the VPS itself, which only makes outbound connections.

Can I build on the VPS instead of on GitHub?

You can: the workflow just connects over SSH and runs git pull and the build on the server. The price is consuming the VPS's CPU and RAM during the build, which can slow the application down at that same moment, and keeping a read credential for the repository on the server. Building on the GitHub runner is usually cleaner.

What happens if two pushes arrive at the same time?

With concurrency configured, the second deploy waits for the first to finish. By default only one run stays pending: if several arrive, the intermediate ones are cancelled and the most recent one proceeds, already containing every commit. Without concurrency, two uploads and two restarts can overlap and leave the folder in a mixed state.

How do I roll back to the previous version if the deploy breaks?

In the simple flow from this guide, re-run the workflow of an earlier commit that worked from the Actions tab, or revert the commit and push. To roll back in seconds without a new build, use releases in separate directories with a symlink pointing to the active version, as shown in the CI/CD on a VPS guide.

Next step

See VPS plans

Root VPS in Brazil with NVMe and Anti-DDoS.

[See VPS plans](https://streethosting.com.br/en/vps)

[See Xeon VPS Xeon VPS for steady workloads, automation and long-running projects.](https://streethosting.com.br/en/vps/xeon) [See Ryzen VPS Ryzen 9 9950X VPS in São Paulo with root access, NVMe and gamer Anti-DDoS.](https://streethosting.com.br/en/vps/ryzen)

## Related guides

[VPS Intermediate How to set up CI/CD on a VPS with automatic deploys CI/CD does not require Kubernetes or an expensive tool. With a pipeline, a releases folder and a health check, an ordinary VPS ships every version safely and rolls back in seconds when something breaks. 10 min Read guide](https://streethosting.com.br/en/guides/vps/set-up-ci-cd-on-vps) [VPS Intermediate PM2 vs systemd: keep your Node app always online on a VPS Your app needs to come back on its own after a crash and start together with the VPS. PM2 and systemd solve this in different ways. Here is how to set up each one and which to pick. 3 min Read guide](https://streethosting.com.br/en/guides/vps/pm2-vs-systemd-nodejs) [VPS Intermediate How to create a sudo user on a Linux VPS Running as root all the time is risky: one wrong command affects everything. The best practice is to create a regular user with sudo and keep root for when you really need it. 3 min Read guide](https://streethosting.com.br/en/guides/vps/create-sudo-user-linux-vps)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
