The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Automatic deployment is a push-triggered process that takes a selected Git branch, updates your application on a VPS, runs the build and database steps it needs, checks that the service is healthy, and reloads it. For a single server, you have two sound designs: a bare Git repository with a server-side post-receive hook, or a GitHub Actions workflow that connects to the VPS over SSH. Use the hook for a compact, server-centered setup; use Actions when you need builds, tests, encrypted secrets, approvals, deployment history, or more than one environment.
Need a server to try this on? Antihost offers Linux VPS plans with NVMe storage, full root access and a 14-day free trial.
This guide prepares the server, shows both implementations, and adds the controls that keep a failed commit from becoming an outage.
As an Amazon Associate I earn from qualifying purchases.
Choose the deployment architecture first
| Decision point | Bare repository and post-receive hook |
GitHub Actions over SSH |
|---|---|---|
| Where the trigger runs | On the VPS immediately after a push to its bare repository | In a GitHub-hosted workflow after a push, manual dispatch, schedule, or repository-dispatch event |
| Build and test support | Possible, but consumes VPS resources and must be scripted in the hook | Build and test before connecting to production; artifacts can be copied to the VPS |
| Credentials | The person or automation pushing to the server needs write access to the bare repository | A dedicated private key stays in a repository secret; only its public key is installed on the VPS |
| Audit and approvals | Shell logs and Git history on the server; no built-in approval gate | Workflow runs, environments, branch restrictions, approvals, secrets, and concurrency controls |
| Rollback | Requires your own release directories or a known-good commit procedure | Can redeploy a recorded commit or artifact, with workflow history showing who initiated it |
| Best fit | One VPS, a small site, and a direct push-to-server path | Applications that need CI, production protection, multiple environments, or a clear release trail |
Neither design is automatically safer. A hook is smaller and has fewer moving parts, but a mistake in its shell script runs directly on the server. Actions adds a runner and SSH boundary, while making it easier to require tests and approval before production.
Prepare the VPS and deployment account
Confirm the runtime and web entry point
Start with a Linux VPS that you can reach with SSH and identify how the application is served. A static site may be copied to a web-server document root. PHP usually needs the web server and PHP-FPM. Node.js, Python, and similar services generally need a process manager or a system service. Record the service name, listening port, runtime version, and the command that performs a graceful reload or restart; these values are application-specific.
Create a least-privilege account
Do not deploy as root. Create a dedicated account such as deploy, give it ownership of the deployment directory and only the service permissions it needs, and keep the web server’s read access separate where practical. If a restart requires elevated privileges, grant that one command through a narrowly scoped sudoers rule rather than unrestricted sudo.
# Run as an administrator; replace values with yours
adduser --disabled-password --gecos "" deploy
install -d -o deploy -g deploy /srv/myapp
# If the web server needs read access, use a group or ACL instead of making deploy root
Choose an explicit path such as /srv/myapp. Avoid relative paths: hooks and CI runners do not necessarily start in the directory you expect.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesVerify SSH and firewall assumptions
- Log in as the deployment account and confirm that its shell, home directory, and permissions work.
- Allow SSH only as required by your firewall. If Actions will connect from changing runner addresses, use key-based authentication and host-key verification rather than assuming a fixed source IP.
- Confirm that the target directory is writable by the deployment account and readable by the process that serves the application.
- Back up the current application and database before the first automated production release.
Option A: deploy from a bare repository and a post-receive hook
A bare repository contains Git data but no checked-out working tree. It acts as a receiving endpoint. Its hooks/post-receive program runs after a push and can check which reference changed, check out the intended branch into a separate target directory, then run application-specific commands.
1. Create the bare repository
sudo -u deploy mkdir -p /srv/git/myapp.git
sudo -u deploy git init --bare /srv/git/myapp.git
sudo -u deploy mkdir -p /srv/myapp
Give the repository a remote URL that your developers or build system can reach over SSH. The account that pushes needs write access to the bare repository; it should not need shell access beyond what your SSH policy allows.
Rank #2
2. Install a branch-checked hook
Create /srv/git/myapp.git/hooks/post-receive, make it executable, and replace every example value. The hook below rejects every branch except main, checks out into an explicit directory, and stops if a command fails.
#!/usr/bin/env bash
set -Eeuo pipefail
EXPECTED_REF="refs/heads/main"
GIT_DIR="/srv/git/myapp.git"
TARGET="/srv/myapp"
while read -r oldrev newrev refname; do
[[ "$refname" == "$EXPECTED_REF" ]] || continue
git --git-dir="$GIT_DIR" --work-tree="$TARGET" checkout -f main
# Adapt these commands to your stack; do not copy them blindly.
# npm ci --omit=dev
# npm run build
# python -m venv .venv && .venv/bin/pip install -r requirements.txt
# php composer.phar install --no-dev --classmap-authoritative
# Example only: replace with your service's validated command.
# sudo systemctl reload myapp.service
done
Use set -Eeuo pipefail so an unset variable or failed command does not look like a successful deployment. A push to another branch is intentionally ignored. If you prefer an explicit failure for an unexpected ref, replace continue with an error and exit status, but make sure the pusher receives a clear message.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →3. Add build, migration, and service steps deliberately
The checkout itself is not a complete deployment. Add only the commands your application needs, in an order that leaves the service in a known state.
- Static HTML: check out the files, or build them first if a generator produces a separate output directory, then atomically update the web-server target.
- PHP: install locked production dependencies, run any asset build, apply migrations with the framework’s non-interactive production command, and reload PHP-FPM or the web server if configuration changed.
- Node.js: install from the lockfile, run the production build, migrate only with an intentional production command, and restart or reload the process manager after the new release is ready.
- Python: create or update a virtual environment from pinned dependencies, run migrations, collect static assets when applicable, and restart the WSGI/ASGI service.
These are categories, not universal commands. A migration can be destructive, and a restart command varies by supervisor. Keep the exact commands in version-controlled scripts so reviewers can see what a commit is allowed to do.
Rank #3
4. Make the hook observable
Write deployment output to the system journal or a log owned by the deployment account, and include the commit ID, start time, and result. Do not print private keys, tokens, or full environment files. Test with a harmless change and verify the push client reports the hook’s failure when checkout, build, migration, or reload fails.
Option B: deploy with GitHub Actions over SSH
GitHub documents custom continuous-deployment workflows directly in a repository. A workflow can run on a push to a selected branch, build and test the project, and then copy files or an artifact to the VPS. It can also be started manually, on a schedule, or by a repository-dispatch event when those triggers suit your release process.
1. Create a dedicated deployment key
- On a trusted administration machine, generate a key pair used only for this deployment. Protect the private key with a passphrase if your runner design supports it.
- Install only the public key in the deployment account’s
~/.ssh/authorized_keyson the VPS. Set restrictive ownership and permissions on the SSH directory and file. - Store the private key as an encrypted repository or environment secret. Never commit it, paste it into workflow output, or echo it for debugging.
- Record the VPS host key through a trusted channel. A convenience command such as
ssh-keyscancan collect a key, but accepting an unverified result defeats host authentication. Compare the fingerprint with one obtained from the server console, provider control plane, or another trusted administrator before placing it inknown_hosts.
2. Restrict the workflow trigger
Use the exact release branch rather than every push. This example runs on pushes to main and can be started manually.
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: production-deploy
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install and test
run: |
# Replace with commands for your stack.
npm ci
npm test
npm run build
- name: Install SSH material
shell: bash
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
run: |
install -m 700 -d "$HOME/.ssh"
printf '%sn' "$DEPLOY_KEY" > "$HOME/.ssh/id_ed25519"
chmod 600 "$HOME/.ssh/id_ed25519"
printf '%sn' "$KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 644 "$HOME/.ssh/known_hosts"
- name: Copy release
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
run: |
rsync -az --delete ./ "$DEPLOY_USER@$DEPLOY_HOST:/srv/myapp/"
ssh "$DEPLOY_USER@$DEPLOY_HOST" 'cd /srv/myapp && ./ops/deploy.sh'
The version of actions/checkout shown is an example action reference; review action versions in your repository’s normal maintenance process. Replace the sample Node commands, destination, and script with your application values. If the build output is all that production needs, copy that output instead of the entire repository.
Rank #4
3. Keep deployment logic on the server or in a reviewed script
A short workflow is easier to audit when the VPS runs a versioned script such as /srv/myapp/ops/deploy.sh. That script can verify the current user, install production dependencies, perform a migration, run a health check, and reload the service. Make it fail closed and return a non-zero status on any unsuccessful step so the workflow is marked failed.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
For sensitive applications, avoid allowing arbitrary pull-request code to invoke production SSH. A workflow triggered from an untrusted branch should build and test without access to production secrets; the production job should require the protected environment and the approved branch.
Use releases and health checks instead of overwriting production blindly
Release-directory pattern
Copying directly into a live directory can leave a mixture of old and new files if a transfer or build stops halfway. A safer pattern creates a uniquely named release, prepares it completely, then switches a stable symlink.
/srv/myapp/releases/20261014T120000Z/
/srv/myapp/current -> /srv/myapp/releases/20261014T120000Z
- Create the new release directory and copy or check out the intended commit there.
- Install dependencies and build assets inside that directory.
- Run non-destructive checks and the application’s health endpoint locally or through the service.
- Change the
currentsymlink atomically to the new directory. - Reload the service so new workers use the new path, then perform an external health check.
- Retain several known-good releases and remove old ones only after the new release has stayed healthy for your chosen window.
If a migration changes the database schema, design it to remain compatible with both the old and new application during the switch. Code rollback is not a database rollback; keep a tested database backup and a specific reverse-migration plan.
Best Value
Health-check requirements
- Check the process or service status after reload.
- Request a local health endpoint that exercises required dependencies without exposing secrets.
- For a public site, perform a separate request through the real web server and TLS configuration.
- Set a timeout and fail the deployment if the check does not return the expected status.
Production controls for GitHub Actions
GitHub environments provide fine-grained deployment control. Put production secrets in the production environment, restrict which branches may use that environment, and add required reviewers when a human approval is appropriate. Use a concurrency group so two production deployments cannot change the same target at once; decide whether a newer run should wait or cancel an older one based on your release policy.
Recommended Free Tools
- Branch restriction: allow only the release branch or a protected tag to reach production.
- Approval: require a reviewer for high-risk services or database changes.
- Concurrency: serialize deployments to one target and prevent overlapping migrations.
- Secret scope: keep production credentials out of build jobs that do not need them.
- Permissions: set the workflow’s
GITHUB_TOKENpermissions to the minimum, such as read-only contents access when no write operation is required. - Logging: mask secrets and avoid commands that print environment variables or private files.
Secure the SSH boundary
- Use a separate key per repository or environment when practical, and rotate it when staff, runners, or server access changes.
- Authorize the key only for the deployment account. SSH restrictions such as a forced command can further limit what that key may do, but test them against your deployment protocol before enabling them.
- Verify the server host key through a trusted channel. Do not treat an automatically scanned key as proof of identity.
- Keep the private key only in the CI secret store or a protected administration system. Never place it in the repository, an artifact, or a world-readable file.
- Review the deployment script as code. A compromised commit should not be able to silently add unrestricted shell access or change the destination to an unintended path.
- Give the deployment account only the filesystem and service permissions it needs. Separate application data, uploads, backups, and secrets from the release directory.
Rollback and failure recovery
When a hook or workflow fails before activation
If checkout, dependency installation, build, or migration fails before the release switch, leave the previous release active. Inspect the hook log or workflow log, correct the command, and redeploy the same commit after review. Do not hide an error with || true unless the command is intentionally optional and its consequence is documented.
When the new release is unhealthy
- Stop or pause further automatic runs with the workflow’s concurrency or branch controls.
- Use the last known-good release directory and move the stable symlink back to it.
- Reload the service and verify the health endpoint and public request.
- Record the failing commit, migration state, and recovery action.
- Fix forward or redeploy the known-good commit only after determining whether the database requires a separate recovery.
When the server itself is unavailable
CI cannot repair a powered-off or unreachable VPS. Keep console access, backups, infrastructure credentials, and a documented manual deployment procedure separate from the normal SSH key. Automatic deployment should reduce routine work, not become the only way to recover the host.
Test the path before calling it production-ready
- Push a harmless change to a non-production branch and confirm that it does not trigger the production job or hook.
- Push a harmless change to the selected branch and verify the commit ID, target path, build output, service reload, and health check.
- Intentionally make a build or health check fail in a safe environment; confirm that the deployment reports failure and leaves the previous release available.
- Run the rollback procedure and verify that the service returns to the earlier version.
- Confirm that logs contain useful status information but no private key, token, or secret value.
- Document who can approve production, rotate keys, access the VPS console, and restore backups.
Which approach should you use?
Choose a bare repository hook when one Linux VPS serves a small application, the team wants a direct push endpoint, and builds and tests are simple enough to run on that server. Choose GitHub Actions when the repository should be built and tested before release, when secrets must be separated from developers’ machines, or when protected environments, approvals, concurrency, artifacts, and an auditable deployment history justify the additional setup. In either design, branch validation, least-privilege SSH, explicit stack commands, health checks, and a tested rollback are the controls that determine whether automatic deployment is dependable.
Quick Recap
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.




