When starting to develop a new website or product, we face some challenges so easy to understand but so hard to fix. Today, after 8 years of developing all kinds of products, I faced the most insignificant problem to be solved by a developer: configure your environment variables for a deploy.
Yesterday, working on my personal blog, I had an idea: why not configure a headless CMS to get my resume data to be easy to manage?
Thriving on the idea, in my mind I was "this is gonna last no more than 10 minutes. It's gonna be easy. 10 minutes" As a Backend Engineer with a reasonable DevOps background, this should be easy. But it was painfully hard to solve and understand WHAT THE HECK is happening.
What was I Expecting to do?
- Configure Contentful to retrieve my data (easy)
- Plug the Contentful package and consume the data (easy)
- Consume data locally (easy)
- Deploy using GitHub Actions on my GitHub Page (easy)
- Consume data using Contentful on my Github Page (Hard as F***)
That sounds like a piece of cake, sounds like a that simple task, popped into the Jira to change the color of the button - EASY - right?
Reality
When working at big companies — or companies that have an SRE/DevOps team allocated in the development workflow, in delivery part of the system — we start to become dangerously comfortable with our environment. Whatever deployment problem we have, we just open a ticket, assign it to the infrastructure team, and it's magically solved. We start to not care about the most important part of our development circle: the delivery.
Most of the time, we don't have to set up an entire pipeline from scratch because they're already there — battle-tested, documented, and stable for our use cases. Infrastructure as code? Someone else wrote it. CI/CD pipelines? Pre-configured and ready to use. Environment variable management? There's a corporate tool for that, and someone from platform engineering already set it up.
This comfort creates a dangerous illusion of competence. We know how to use Jenkins, but do we know how to configure it from scratch? We can deploy to Kubernetes, but can we set up the cluster? We understand the application code, but the delivery mechanism becomes a black box that "just works."
It's like being a chef who's only ever worked in a fully-equipped professional kitchen with prep cooks, sous chefs, and specialized equipment. Everything is where you expect it, ingredients are pre-prepared, and if the oven breaks, you call maintenance. Then one day, you try to cook a simple meal in a basic home kitchen. Suddenly, you realize you don't know where anything is, how the oven works, or even how to properly sharpen a knife.
The scariest part? You don't realize how much you've forgotten until you're alone with a terminal, staring at a failed GitHub Action, wondering why something that should take 10 minutes is approaching hour three.
The Deployment Reality Check
When we're using GitHub Actions, we have to understand two parts of the system and ecosystem.
To deploy a GitHub Page, you have to choose between two approaches:
Approach 1: Build and Push to gh-pages Branch
This is the "classic" approach where you:
- Build your site locally or when automated in GitHub Actions
- Push the compiled/built files to a special gh-pages branch
- Let GitHub's built-in automation handle the actual deployment
How it works:
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
env:
NEXT_PUBLIC_CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }}
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }}
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out # or ./dist, ./build - wherever your built files are
Pros:
- GitHub handles the web server configuration
- Simple deployment mechanism
- Clear separation between build and deploy
- Works great for static sites
Cons:
- Less control over the deployment process
- Environment variables can be tricky during build time
- Another branch to manage (gh-pages)
- Critical gotcha: Will fail silently if repository Pages settings aren't configured properly
Approach 2: Custom GitHub Actions Deployment
This approach gives you full control over both build and deployment, and is just remove all deploy part for Github work and back to our own pipeline work.
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
deployment
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
env:
NEXT_PUBLIC_CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }}
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }}
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './out' # your build output directory
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Pros:
- Full control over the deployment process
- More modern approach (uses GitHub's newer Pages deployment API)
- Better integration with GitHub's security model
- No need for a separate gh-pages branch
Which Approach Should You Choose?
Go with Approach 1 (gh-pages) if:
- You have a simple build process
- You're comfortable with managing an extra branch
- You want the "battle-tested" method
- You're using older documentation or tutorials (many still reference this method)
Go with Approach 2 (custom deployment) if:
- You want the most up-to-date GitHub Pages experience
- You prefer not to manage a separate branch
- You need more control over the deployment process
- You're starting fresh (this is becoming the recommended approach)
The Configuration Traps (And How to Avoid Them)
Both approaches have their own "gotcha" moments that can turn your 10-minute deployment into a debugging marathon. Here's what the tutorials don't tell you:
Approach 1: The Repository Settings Trap
Your GitHub Action might run perfectly, push files to the gh-pages branch successfully, but your site still shows a 404. Why? You forgot to configure your repository's Pages settings.
The solution:
- Go to your repository's Settings tab
- Scroll down to Pages in the left sidebar
- Under Source, select "Deploy from a branch"
- Choose gh-pages as the branch and / (root) as the folder
- Click Save
Without this configuration, GitHub doesn't know that your gh-pages branch contains your website. Your action succeeds, files get pushed, but nothing deploys.
Approach 2: The Environment Declaration Trap
As we discovered, missing environment: github-pages causes cryptic deployment token errors. But there's more to it:
The complete solution:
jobs:
build:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
The url part isn't strictly required, but it makes your deployment logs much more useful by showing you exactly where your site was deployed.
The GITHUB_TOKEN Permissions Issue
Both approaches can fail due to insufficient permissions. Make sure your workflow has the right permissions at the top:
For Approach 1
permissions:
contents: write # To push to gh-pages branch
For Approach 2
permissions:
contents: read
pages: write
id-token: write
Pro tip: If you're getting permission errors, double-check that Actions have write permissions in your repository settings under Settings → Actions → General → Workflow permissions.
The Environment Variable Challenge
Both approaches face the same fundamental challenge: how do you get API keys and secrets available during the build process?
This is where my 3-hour debugging session began. The issue isn't GitHub Secrets (that part actually works great) — it's understanding how your build tool handles environment variables.
The gotcha: Most modern frameworks (Vite, Next.js, Nuxt, etc.) have specific rules about which environment variables are available in the browser vs. the build process:
# This won't work in the browser (security risk)
CONTENTFUL_ACCESS_TOKEN=your_secret_token
# This works (Vite prefix)
VITE_CONTENTFUL_ACCESS_TOKEN=your_token
# This also works (Next.js prefix)
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN=your_token
# And this (Nuxt prefix)
NUXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN=your_token
The build process needs to know which variables are safe to "bake into" your static files. Without the proper prefix, your bundler ignores the environment variable entirely — even though it's perfectly available in the GitHub Actions environment.
Simple Page, Complex Variables
Here's where my "10-minute task" turned into a 3-hour debugging session.
For a simple static page, approach #1 works beautifully. But the moment you introduce a headless CMS like Contentful, you need API keys. And API keys mean environment variables. And environment variables in GitHub Actions mean... well, let's just say it's not as straightforward as import.meta.env.VITE_CONTENTFUL_API_KEY
.
# What I thought would work
- name: Build
run: npm run build
env:
CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }}
CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }}
Simple, right? Wrong.
The Devil in the Details: GitHub Environments
The problem isn't the GitHub Secrets part — that's actually straightforward once you know where to put them. The devil is in understanding that GitHub has two different concepts that sound similar but work completely differently:
- Repository Secrets (the simple ones)
- Environment Secrets (the tricky ones)
Creating GitHub Environments
Here's what the tutorials don't tell you: if you're using Approach 2 (custom deployment), you need to create a GitHub Environment first, then configure secrets within that environment.
Step-by-step GitHub Environment setup:
- Go to your repository on GitHub
- Click Settings tab
- In the left sidebar, scroll down to Environments
- Click New Environment
- Name it github-pages (this exact name matters)
- Click Configure Environment
- Add your secrets here (not in the main repository secrets!)
The Environment Declaration Trap
In your GitHub Actions workflow, you must declare the environment explicitly:
jobs:
build:
environment: github-pages # Must match your environment name exactly
runs-on: ubuntu-latest
Why this breaks without warning:
If you reference an environment that doesn't exist, GitHub Actions won't give you a clear error. Instead, you'll get cryptic messages like "deployment token not found" or permission errors that point you in completely wrong directions.
Repository Secrets vs Environment Secrets
This is where it gets confusing:
- Repository Secrets: Available to all workflows, accessed with ${{ secrets.SECRET_NAME }}
- Environment Secrets: Only available to jobs that declare the specific environment
If you put your Contentful API keys in Repository Secrets but your workflow declares environment: github-pages, those secrets won't be available unless you ALSO add them to the github-pages Environment Secrets.
The solution: Choose one approach and stick with it:
- Use Repository Secrets and don't declare an environment, OR
- Create an Environment and put all your secrets there
Most deployment failures happen because you mix the two approaches without realizing it.
The Comfort Zone Problem
This experience made me realize something important about our industry. When we work in well-established teams with dedicated DevOps support, we lose touch with the fundamentals. We become like passengers who never learned to drive — comfortable in the backseat but helpless when we need to take the wheel.
The irony is that these "simple" deployment tasks are the foundation of everything we do. Yet somehow, they become the most frustrating obstacles when we step outside our comfort zones.
Lessons Learned (The Hard Way)
- Read the framework documentation thoroughly — especially the parts about environment variables and build-time vs. runtime behavior
- Test your deployment pipeline early — don't wait until the very end to figure out environment configuration
- Keep deployment skills sharp — even if your day job doesn't require them
- Remember that "simple" is relative — what's simple with a full DevOps team might not be simple solo
The Silver Lining
Despite the frustration, there's something deeply satisfying about solving these problems yourself. When that deployment finally succeeds and your personal project goes live, it feels different than launching something through an enterprise pipeline. You own every piece of it.
Plus, now I have a perfectly configured blog with dynamic content from Contentful. Was it worth the 3-hour detour for a prefix? Ask me again when I'm updating my resume without touching any code.
Final Thoughts
Environment variables might seem like a trivial part of development, but they represent something bigger: the gap between enterprise comfort and independent problem-solving. The next time you're setting up a personal project and find yourself googling "how to deploy to GitHub Pages with environment variables," remember — you're not alone, and it's probably simpler than you think.
"It's just not as simple as it should be."
Have you fallen into the environment variable trap? Share your deployment horror stories in the comments — misery loves company, especially when it involves missing prefixes and 3 AM debugging sessions.
Top comments (0)