Environments and Protection Rules in GitHub Actions
Use GitHub Actions environments to gate production deploys behind required reviewers, wait timers, branch restrictions and isolated environment secrets.
Netbay Infrastructure Team
Netbay Engineering
On this page
Secrets that are visible to every job are a quiet disaster waiting to happen. GitHub Actions solves this with environments: named containers (staging, production) that bundle their own secrets, their own protection rules, and their own deployment history. When a workflow references an environment, the runner fetches only that environment's secrets and obeys its rules — nothing else sees them. This post shows you how to model deployment gates with environments and protection rules.
What an environment actually gives you
An environment is a named resource you create at the repository or organization level. Here is what attaching a job to a production environment buys you:
- Secret isolation: each environment has its own secret set. A production API key lives only in the production environment, never exposed to pull requests or CI.
- Protection rules: required reviewers, a mandatory wait timer, and branch restrictions that must be satisfied before the job runs.
- Deployment tracking: GitHub records every release into the environment, visible on the environment page.
- Deployment branches: you can restrict an environment so only specific branches or tags can deploy to it.
Required reviewers and the approval gate
The reviewer rule is the headline feature. Mark an environment as "required reviewers" and list the people or teams who must approve before the job runs. When the workflow reaches that job, GitHub pauses it and pages the reviewers with an approval prompt in the UI and via notifications. The job does not start until an approver clicks the button. This is the cleanest way to make "no deploys without sign-off" an enforced platform rule rather than a good intention.
You can also add a wait timer — for example, 15 minutes — that delays the job after approval, giving you a window to cancel if you spot a problem.
Branch protection for environments
Beyond reviewers, an environment can restrict which branches may deploy to it. Set the deployment branches rule to a branch name or a tag pattern. A job targeting a pull request branch will be blocked from a protected environment entirely. Combine this with the standard branch protection rules on the code itself, and you get a two-layer gate: the code must be mergeable, and the deploy must be approved.
Wiring an environment into a workflow
Environments are referenced per job, not per workflow. Add an environment key to the job, then reference that environment's secrets in its steps. A typical production deploy might look like this:
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist
deploy:
runs-on: ubuntu-22.04
needs: build
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- name: Deploy to server
env:
PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
PROD_HOST: ${{ secrets.PROD_HOST }}
run: |
echo "deploying dist to $PROD_HOST"
eval "$(ssh-agent -s)"
echo "$PROD_SSH_KEY" | ssh-add -
rsync -avz --delete dist/ deploy@$PROD_HOST:/var/www/appNotice env values are loaded from the production environment's secrets. If you also had a staging job referencing environment: staging, it would pull staging's secrets instead — the same variable names, different values, zero cross-talk.
Secrets scoping: environment vs repository vs org
Secrets can live at three levels. Repository secrets are available to every job in the repository, environment secrets only to jobs targeting that environment, and organization secrets to any repository in the org that lists them. Prefer environment secrets for anything production-shaped and keep repository secrets minimal. This way a failed CI run cannot leak a production key, because CI jobs never target the production environment.
Deployment concurrency and queue behavior
Two runs racing to the same environment are a classic footgun. Environments can enforce concurrency: set jobs with the same concurrency group, and GitHub will either cancel the in-flight run or queue the newcomer, depending on the cancel-in-progress flag. For deploys you usually want the newest run to cancel the older one so an old build does not overwrite a fresh deploy:
jobs:
deploy:
runs-on: ubuntu-22.04
environment: production
concurrency:
group: production-deploy
cancel-in-progress: false
steps:
- run: echo deployingWith cancel-in-progress false, a new deploy waits for the current one to finish before running, preventing two deploy tools from fighting over the same server.
Takeaway
Environments are the correct home for environment-specific secrets and human gatekeeping, not an afterthought. Isolate production credentials in a production environment with required reviewers, restrict the branches that can deploy, and let concurrency rules serialize releases. When a deploy clears all those gates and needs a real server, deploy to an Ubuntu VPS you create on Netbay in less than a minute at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance