Back to Blogs
How to Host Your Website on GitHub Pages While Keeping Source Code Private
May 30, 2025
Tutorial

How to Host Your Website on GitHub Pages While Keeping Source Code Private

GitHub PagesGitHub ActionsWeb DevelopmentDeploymentTutorial

GitHub Pages is an excellent free hosting service for static websites, but there's one catch: your repository needs to be public to use the free tier. What if you want to keep your source code private while still leveraging GitHub Pages for hosting?

In this guide, I'll show you how to set up a deployment pipeline that builds your private source code and deploys it to a public GitHub Pages repository, giving you the best of both worlds: private source code and free GitHub Pages hosting.

๐ŸŽฏ The Strategy

We'll use a two-repository approach:

  1. Private Repository: Contains your source code, build tools, and GitHub Actions workflow
  2. Public Repository: Serves as your GitHub Pages site (receives only the built static files)

The magic happens through GitHub Actions running in your private repository, which builds your site and deploys the static files to your public repository.

๐Ÿ“‹ Prerequisites

  • A GitHub account
  • Node.js project (works with React, Vue, Svelte, Next.js, etc.)
  • Basic knowledge of Git and GitHub Actions

๐Ÿš€ Step-by-Step Setup

Step 1: Create Your Repositories

Create the Private Repository:

bash
1# Create your private repository for source code 2git clone https://github.com/yourusername/my-website-private.git 3cd my-website-private

Create the Public Repository:

  • Go to GitHub and create a new repository named yourusername.github.io (replace yourusername with your actual GitHub username)
  • Make sure it's public
  • Initialize with just a README or license

Step 2: Prepare Your Project for Static Deployment

Ensure your project can build to a static output. Here are configurations for popular frameworks:

For SvelteKit (svelte.config.js):

javascript
1import adapter from "@sveltejs/adapter-static"; 2 3const config = { 4 kit: { 5 adapter: adapter({ 6 pages: "build", 7 assets: "build", 8 fallback: "404.html", 9 }), 10 paths: { 11 base: process.env.NODE_ENV === "production" ? "" : "", 12 }, 13 }, 14}; 15 16export default config;

For Next.js (next.config.js):

javascript
1/** @type {import('next').NextConfig} */ 2const nextConfig = { 3 output: "export", 4 trailingSlash: true, 5 images: { 6 unoptimized: true, 7 }, 8}; 9 10module.exports = nextConfig;

For React/Vite (vite.config.js):

javascript
1import { defineConfig } from "vite"; 2import react from "@vitejs/plugin-react"; 3 4export default defineConfig({ 5 plugins: [react()], 6 base: "/", 7 build: { 8 outDir: "build", 9 }, 10});

Step 3: Generate SSH Deploy Keys

SSH keys allow your private repository to securely push to your public repository.

bash
1# Generate SSH key pair 2ssh-keygen -t rsa -b 4096 -C "github-actions-deploy" -f ~/.ssh/github_deploy_key -N "" 3 4# Display the public key (copy this) 5cat ~/.ssh/github_deploy_key.pub 6 7# Display the private key (copy this too) 8cat ~/.ssh/github_deploy_key

Step 4: Configure Repository Access

Add Deploy Key to Public Repository:

  1. Go to your public repository (yourusername.github.io)
  2. Navigate to Settings โ†’ Deploy keys โ†’ Add deploy key
  3. Title: GitHub Actions Deploy Key
  4. Key: Paste the public key content
  5. โœ… Check "Allow write access"
  6. Click Add key

Add Secret to Private Repository:

  1. Go to your private repository
  2. Navigate to Settings โ†’ Secrets and variables โ†’ Actions
  3. Click New repository secret
  4. Name: DEPLOY_KEY
  5. Secret: Paste the private key content (including BEGIN/END lines)
  6. Click Add secret

Step 5: Create GitHub Actions Workflow

In your private repository, create .github/workflows/deploy.yml:

yaml
1name: Deploy to GitHub Pages 2 3on: 4 push: 5 branches: ["main"] 6 workflow_dispatch: 7 8jobs: 9 build-and-deploy: 10 runs-on: ubuntu-latest 11 steps: 12 - name: Checkout 13 uses: actions/checkout@v4 14 15 - name: Setup Node.js 16 uses: actions/setup-node@v4 17 with: 18 node-version: "20" 19 cache: "npm" 20 21 - name: Install dependencies 22 run: npm ci 23 24 - name: Build 25 run: npm run build 26 27 - name: Deploy to GitHub Pages 28 uses: peaceiris/actions-gh-pages@v3 29 with: 30 deploy_key: ${{ secrets.DEPLOY_KEY }} 31 external_repository: yourusername/yourusername.github.io 32 publish_dir: ./build # Change based on your build output directory 33 publish_branch: main 34 # Uncomment if you have a custom domain: 35 # cname: yourdomain.com

Important: Update these values in the workflow:

  • yourusername/yourusername.github.io โ†’ your actual repository
  • ./build โ†’ your actual build output directory
  • publish_branch: main โ†’ your target branch (usually main)

Step 6: Configure Package.json Scripts

Ensure your package.json has the necessary build script:

json
1{ 2 "scripts": { 3 "dev": "vite dev", 4 "build": "vite build", 5 "preview": "vite preview" 6 } 7}

Step 7: Set Up GitHub Pages

  1. Go to your public repository (yourusername.github.io)
  2. Navigate to Settings โ†’ Pages
  3. Source: Deploy from a branch
  4. Branch: main (or whatever branch your workflow deploys to)
  5. Folder: / (root)
  6. Click Save

๐Ÿงช Testing Your Setup

  1. Make a commit to your private repository:
bash
1cd my-website-private 2echo "# Test deployment" >> README.md 3git add . 4git commit -m "Test: trigger deployment" 5git push origin main
  1. Monitor the workflow:

    • Go to your private repository โ†’ Actions tab
    • Watch the deployment workflow run
  2. Check your live site:

    • Visit https://yourusername.github.io
    • Your site should be live!

๐Ÿ› Common Issues and Solutions

Issue 1: "Dependencies lock file is not found"

Cause: The workflow can't find package-lock.json Solution: Ensure your private repository has the lock file committed:

bash
1npm install # This creates package-lock.json 2git add package-lock.json 3git commit -m "Add package-lock.json"

Issue 2: "Permission denied (publickey)"

Cause: SSH key not properly configured Solution:

  • Verify the deploy key is added to the public repository with write access
  • Ensure the private key is correctly added as DEPLOY_KEY secret

Issue 3: "Build fails with path issues"

Cause: Incorrect base path configuration Solution: Check your framework's base path settings match your deployment URL

Issue 4: Custom Domain Not Working

Solution: Add a CNAME file or use the cname option in the workflow:

yaml
1- name: Deploy to GitHub Pages 2 uses: peaceiris/actions-gh-pages@v3 3 with: 4 deploy_key: ${{ secrets.DEPLOY_KEY }} 5 external_repository: yourusername/yourusername.github.io 6 publish_dir: ./build 7 publish_branch: main 8 cname: yourdomain.com # Add this line

๐Ÿ”’ Security Best Practices

  1. Never commit secrets to your repository
  2. Use minimal permissions - the deploy key should only have access to the public repository
  3. Regularly rotate your deploy keys
  4. Monitor workflow runs for any suspicious activity

๐ŸŽ‰ Benefits of This Approach

  • โœ… Keep source code private while using free GitHub Pages
  • โœ… Automated deployments on every push
  • โœ… No manual build steps required
  • โœ… Works with any static site generator
  • โœ… Custom domain support
  • โœ… Free SSL certificates through GitHub Pages

๐Ÿ“š Framework-Specific Examples

SvelteKit with Adapter Static

bash
1npm install @sveltejs/adapter-static 2# Configure svelte.config.js as shown above 3# Build output: ./build

Next.js with Static Export

bash
1# Configure next.config.js for static export 2# Build command: npm run build 3# Build output: ./out

Vite/React

bash
1# Standard Vite build 2# Build command: npm run build 3# Build output: ./dist

๐ŸŒ Advanced Configurations

Multiple Environment Deployments

You can extend this setup to deploy to different repositories based on branches:

yaml
1- name: Deploy to Staging 2 if: github.ref == 'refs/heads/develop' 3 uses: peaceiris/actions-gh-pages@v3 4 with: 5 deploy_key: ${{ secrets.STAGING_DEPLOY_KEY }} 6 external_repository: yourusername/staging.yourusername.github.io 7 publish_dir: ./build 8 9- name: Deploy to Production 10 if: github.ref == 'refs/heads/main' 11 uses: peaceiris/actions-gh-pages@v3 12 with: 13 deploy_key: ${{ secrets.DEPLOY_KEY }} 14 external_repository: yourusername/yourusername.github.io 15 publish_dir: ./build

๐ŸŽฏ Conclusion

This setup gives you the perfect balance between privacy and free hosting. Your source code remains private and secure, while your website is automatically built and deployed to GitHub Pages whenever you push changes.

The two-repository approach might seem complex initially, but once set up, it provides a robust, automated deployment pipeline that costs nothing and keeps your intellectual property protected.

Happy deploying! ๐Ÿš€