
How to Host Your Website on GitHub Pages While Keeping Source Code Private
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:
- Private Repository: Contains your source code, build tools, and GitHub Actions workflow
- 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:
bash1# 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(replaceyourusernamewith 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):
javascript1import 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):
javascript1/** @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):
javascript1import { 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.
bash1# 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:
- Go to your public repository (
yourusername.github.io) - Navigate to Settings โ Deploy keys โ Add deploy key
- Title:
GitHub Actions Deploy Key - Key: Paste the public key content
- โ Check "Allow write access"
- Click Add key
Add Secret to Private Repository:
- Go to your private repository
- Navigate to Settings โ Secrets and variables โ Actions
- Click New repository secret
- Name:
DEPLOY_KEY - Secret: Paste the private key content (including BEGIN/END lines)
- Click Add secret
Step 5: Create GitHub Actions Workflow
In your private repository, create .github/workflows/deploy.yml:
yaml1name: 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 directorypublish_branch: mainโ your target branch (usuallymain)
Step 6: Configure Package.json Scripts
Ensure your package.json has the necessary build script:
json1{ 2 "scripts": { 3 "dev": "vite dev", 4 "build": "vite build", 5 "preview": "vite preview" 6 } 7}
Step 7: Set Up GitHub Pages
- Go to your public repository (
yourusername.github.io) - Navigate to Settings โ Pages
- Source: Deploy from a branch
- Branch:
main(or whatever branch your workflow deploys to) - Folder:
/ (root) - Click Save
๐งช Testing Your Setup
- Make a commit to your private repository:
bash1cd my-website-private 2echo "# Test deployment" >> README.md 3git add . 4git commit -m "Test: trigger deployment" 5git push origin main
-
Monitor the workflow:
- Go to your private repository โ Actions tab
- Watch the deployment workflow run
-
Check your live site:
- Visit
https://yourusername.github.io - Your site should be live!
- Visit
๐ 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:
bash1npm 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_KEYsecret
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:
yaml1- 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
- Never commit secrets to your repository
- Use minimal permissions - the deploy key should only have access to the public repository
- Regularly rotate your deploy keys
- 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
bash1npm install @sveltejs/adapter-static 2# Configure svelte.config.js as shown above 3# Build output: ./build
Next.js with Static Export
bash1# Configure next.config.js for static export 2# Build command: npm run build 3# Build output: ./out
Vite/React
bash1# 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:
yaml1- 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! ๐