A GitHub environment is a named deployment target: staging,
production, preprod. It carries its own secrets, its own variables,
its protection rules, and it turns an ordinary job into a tracked
deployment in the repository's Deployments tab. This page shows how to create
one, how a job attaches to it, and why isolating secrets per environment beats
repository secrets prefixed with PROD_.
What you will learn
- Create an environment and understand the constraints on its name
- Attach a job to an environment, in the short and the long form
- Publish the deployment URL and know where GitHub displays it
- Isolate the secrets per target rather than by a naming convention
- Spot the plan limit that makes environments inert on a free private repository
Why not simply repository secrets?
The most widespread practice is to store every secret at repository level and
tell them apart with a prefix: STAGING_DEPLOY_TOKEN, PROD_DEPLOY_TOKEN. It
works, and it protects nothing.
A repository secret is reachable by every workflow of the repository, so by
every job, so by every step. A test workflow installing a compromised dependency
can read PROD_DEPLOY_TOKEN as easily as the deployment job. The prefix is a
human convention; it does not exist for the execution engine.
An environment secret changes the nature of the control: it is only delivered to jobs that declare the environment, and only after that environment's protection rules have been satisfied. The prefix becomes a real boundary.
| Repository secret | Environment secret | |
|---|---|---|
| Who can read it | Every job of the repository | The jobs declaring the environment |
| When | From the moment the job starts | After the protection rules are satisfied |
| staging/prod separation | By a naming convention | By the GitHub engine |
| Trace | None | A recorded deployment, with its state |
Creating an environment
The environment is created in the interface, not in the YAML. That is deliberate: it carries governance rules, which must not be editable by anyone pushing a commit to a branch.
-
Open the repository settings, the Settings > Environments section.
-
Create the environment with New environment, then give it a name.
Names are not case sensitive, never exceed 255 characters and are unique within the repository.
Productionandproductiontherefore designate the same environment. -
Configure the protection rules if the target deserves them (approvals, a delay, allowed branches). The detail of those four rules is the subject of the next page.
-
Add the secrets and variables specific to that target, in the Environment secrets and Environment variables sections.
Attaching a job to an environment
Two forms exist. The short form is enough when the deployment exposes no URL:
jobs: deploy-staging: runs-on: ubuntu-24.04 environment: staging steps: - name: Deploy to staging run: ./deploy.sh env: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}The DEPLOY_TOKEN read here is the one from the staging environment, not the
repository's. The same workflow, with environment: production, reads the
production token under the same variable name. That is what lets you write a
single deployment job and parameterise it by its target.
The long form adds the deployment URL:
jobs: deploy-staging: runs-on: ubuntu-24.04 environment: name: staging url: ${{ steps.deploy.outputs.url }} steps: - name: Deploy to staging id: deploy run: | ./deploy.sh echo "url=https://staging.my-app.example.com" >> "$GITHUB_OUTPUT"Where the URL appears
Filling in url: is not cosmetic, it is what ties the deployment to a reachable
address. GitHub shows it in three places:
- on the repository's Deployments page, next to the environment;
- in the visualisation graph of the workflow run;
- as a View deployment button in the pull request timeline, where it applies.
That last point is the most useful day to day: the reviewer of a pull request reaches the preview environment without leaving the pull request, and without anyone pasting the URL into a comment.
Environment variables and secrets: what goes where?
Both mechanisms carry a value specific to the target, but they do not protect the same thing. The rule is binary: if leaking the value has an impact, it is a secret; otherwise it is a variable.
| Environment variable | Environment secret | |
|---|---|---|
| Visible in the logs | Yes, in clear text | No, masked by GitHub |
| Readable after creation | Yes | No |
| Examples | API URL, bucket name, region, log level | Deployment token, signing key, password |
jobs: deploy: runs-on: ubuntu-24.04 environment: production steps: - name: Deploy run: ./deploy.sh --region "$AWS_REGION" --bucket "$BUCKET" env: AWS_REGION: ${{ vars.AWS_REGION }} # a variable, not sensitive BUCKET: ${{ vars.BUCKET }} # a variable, not sensitive DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} # a secret, maskedNote the systematic use of an env: block: a secret interpolated straight into
the command line shows up in the debug logs, and the automatic masking does
not catch a value transformed along the way.
The order in which secrets resolve
The same name can exist at three levels. GitHub applies a precedence, from the most specific to the most general:
Environment secret (the most specific, wins) | vRepository secret | vOrganisation secret (the most general)That precedence is the tool that lets you override cleanly: an organisation
defines a shared REGISTRY_TOKEN, a repository replaces it for its particular
case, and the production environment replaces it again with a token carrying
wider rights. The workflow, meanwhile, does not change a line.
The downside is a classic diagnosis trap: a secret that "does not take" is often a secret defined at the wrong level, masked by a more specific namesake. When in doubt, temporarily renaming one of the two removes the ambiguity in a single run.
The plan limit to check before building on it
Protection rules and environment secrets are not available on every plan. On a Free plan, they only work on public repositories. A private or internal repository requires at least GitHub Pro for a personal account, or a Team or Enterprise plan for an organisation.
The consequence is sneaky: on a free private repository, the environment is created, the job runs, the deployment appears in the history, but the approval never fires. You believe you have a door, you have a sign. Check the plan before founding a production governance on this mechanism.
Check your understanding
Make sure the essentials of this guide are yours. The questions only cover what was explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Skills profile
What to do next
Resources to go further
Hints before you try again?
A new full quiz with random questions
Work again only on the questions you missed
Key points
- An environment is a named deployment target carrying its secrets, its variables and its rules, and turning a job into a tracked deployment.
- A repository secret is readable by every job of the repository; an environment secret is only delivered to the jobs declaring the environment, after its rules are satisfied.
- The
environment:key exists in a short form (a name) and a long form (name:plusurl:). - The deployment URL appears on the Deployments page, in the run graph, and as a View deployment button in the pull request.
- An environment named but non-existent is created automatically with no protection: create it first in Settings > Environments.
- Secrets resolve from the most specific to the most general: environment, then repository, then organisation.
- On a Free plan, environment protections and secrets only exist on public repositories: a private one requires Pro, Team or Enterprise.
Next steps
- Promoting an artifact: taking the same binary through staging then production, without ever rebuilding it in between.
- Releases and packages: publishing to GHCR and attaching artifacts to a GitHub release once the target is validated.
- Rollback and concurrency: what environment isolation buys you the day you have to go back.