# ๐Ÿ”€ GitHub โ†’ GitLab Pages Pipeline: Deploying a Private Website from GitHub **Date:** July 17, 2026 **Tags:** Pipelines, CI/CD, GitHub, GitLab, GitLab Pages, Astro, DevOps, AI / Pipelines --- ## ๐ŸŽฏ Overview This guide documents how to run a **private website** sourced from **GitHub** but deployed through **GitLab Pages**. The trick: GitHub is the source of truth, but the *deployment* happens on GitLab's free Pages hosting (which can serve **private** sites to authenticated users โ€” something Cloudflare Pages does not do for free). The bridge is a **mirror push** from GitHub to GitLab, then a **GitLab CI** job that builds the static site and publishes it to GitLab Pages. The whole flow is: ``` git push origin main (you push to GitHub) โ”‚ โ–ผ GitHub repo (ryanthemanr0x/website) โ”‚ (mirror hook / scheduled push) โ–ผ GitLab repo (gitlab.com//website) โ”‚ detects new commit on main โ–ผ GitLab CI (.gitlab-ci.yml) โ”‚ runs: npm install โ†’ npm run build โ–ผ public/ (static output) โ”‚ โ–ผ GitLab Pages โ†’ https://website-6768cb.gitlab.io โ”‚ โ–ผ Private site (visible to project members / logged-in users) ``` > โš ๏ธ **No secrets in the build.** All tokens below are `` text. The only real secret is the **GitLab deploy key / personal access token** used for the mirror push โ€” that lives in a GitHub Actions secret or a local cron, never in the repo. --- ## ๐Ÿงฉ Why GitLab Pages for a Private Site | Host | Private site on free tier? | Source repo | |------|---------------------------|-------------| | **Cloudflare Pages** | โŒ No (public only on free) | GitHub direct | | **GitHub Pages** | โœ… Yes (private repo โ†’ private pages, limited) | GitHub only | | **GitLab Pages** | โœ… Yes (project set to internal/private) | Any repo via mirror | GitLab Pages is the cleanest way to get a **private** static site from a **GitHub** source: mirror the code over, let GitLab build + host it, and restrict visibility to project members. --- ## ๐Ÿ“‹ Prerequisites - A **GitHub** repo with the site source (e.g. `github.com/ryanthemanr0x/website`). - A **GitLab** account + empty project (e.g. `gitlab.com//website`). - A **GitLab Personal Access Token** (``) with `write_repository` scope, OR a **deploy key**. - A static site generator (this example uses **Astro**, but the CI is generator-agnostic). > ๐Ÿ” The GitHub repo stays the source of truth. The GitLab repo is a **mirror** โ€” never edit it directly. --- ## ๐Ÿ”ง Step 1 โ€” Create the GitLab Project 1. In GitLab, create a new project (do **not** initialize with a README โ€” it will be overwritten by the mirror). 2. Set **Visibility** to **Private** (or Internal) so the Pages site is not public. 3. Note the project path: `gitlab.com//website`. 4. Generate a PAT: **Preferences โ†’ Access Tokens โ†’** scope `write_repository`, expires never (or rotate). ```bash # Save the token somewhere safe (GitHub Actions secret, or a local env file) # GITLAB_PAT= ``` --- ## ๐Ÿ”ง Step 2 โ€” Add GitLab as a Mirror Remote on GitHub The simplest bridge is a **GitHub Actions workflow** that pushes every commit to GitLab. Store the GitLab PAT as a **GitHub Actions secret** named `GITLAB_PAT`. ```yaml # .github/workflows/mirror-to-gitlab.yml name: Mirror to GitLab on: push: branches: [main] jobs: mirror: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Push to GitLab env: GITLAB_PAT: ${{ secrets.GITLAB_PAT }} run: | git config --global user.email "bot@users.noreply.github.com" git config --global user.name "mirror-bot" git remote add gitlab \ https://oauth2:${GITLAB_PAT}@gitlab.com//website.git git push gitlab main --force ``` > ๐Ÿ’ก `--force` is safe here because GitLab is a pure mirror. If you prefer, use a **mirror push** (`git push --mirror`) to keep branches/tags in sync. --- ## ๐Ÿ”ง Step 3 โ€” GitLab CI Builds and Publishes to Pages GitLab Pages requires a `.gitlab-ci.yml` that builds the site into `public/` and uses the `pages` job artifact. The job name **must** be `pages` and the artifact path **must** be `public/`. ```yaml # .gitlab-ci.yml (in the GitLab mirror repo) stages: - build - deploy build-site: stage: build image: node:20 script: - npm install - npm run build artifacts: paths: - public only: - main pages: stage: deploy script: - echo "Publishing public/ to GitLab Pages" artifacts: paths: - public only: - main ``` > โš ๏ธ **Astro output:** Astro builds to `dist/` by default. Either set `outDir: 'public'` in `astro.config.mjs`, or add a `mv dist public` step in the CI script. The example assumes `public/` is the final output. --- ## ๐Ÿ”ง Step 4 โ€” Enable GitLab Pages (Private) 1. In GitLab: **Settings โ†’ Pages** โ†’ ensure **Pages** is enabled. 2. Because the project is **Private**, the Pages site at `https://website-6768cb.gitlab.io` is only visible to logged-in project members. 3. The live URL follows the pattern `https://-.gitlab.io` (GitLab auto-assigns the ``). ```bash # Verify the site is served curl -I https://website-6768cb.gitlab.io # -> 200 for authenticated members, 404/302 for anonymous (private) ``` --- ## ๐Ÿ”’ Making It Private (Key Difference vs Cloudflare) | Setting | Value | |---------|-------| | Project visibility | **Private** or **Internal** | | Pages access | Inherits project visibility โ€” non-members get nothing | | Custom domain | Optional (add under Settings โ†’ Pages โ†’ New domain) | | Force HTTPS | Enabled by default on `*.gitlab.io` | This is the whole reason to route through GitLab instead of Cloudflare: **free private Pages**. Cloudflare Pages only serves public sites on the free tier. --- ## ๐Ÿงช Smoke Test ```bash # 1. Push to GitHub git push origin main # 2. GitHub Action mirrors to GitLab (check Actions tab) # 3. GitLab CI runs build + pages job (check CI/CD โ†’ Pipelines) # 4. Site live (as a project member): open https://website-6768cb.gitlab.io # 5. Confirm privacy: open in incognito โ†’ should NOT show the site ``` --- ## ๐Ÿฉบ Troubleshooting | Symptom | Likely cause | Fix | |---------|-------------|-----| | Mirror push 401 | Bad/expired `GITLAB_PAT` | Regenerate PAT with `write_repository`; update GitHub secret | | Pages job missing | Job not named `pages` | Rename job to `pages`; artifact path must be `public` | | 404 on the site | Project is Public but Pages disabled | Enable Pages under Settings โ†’ Pages | | Site public when it should be private | Project visibility wrong | Set project to Private/Internal | | Build fails on `npm run build` | Node version / deps | Pin `image: node:20`; run `npm ci` | --- ## ๐Ÿ” Security Checklist - [ ] `GITLAB_PAT` is a GitHub Actions **secret**, never committed. - [ ] GitLab repo is a **mirror** โ€” no direct edits. - [ ] Project visibility set to **Private/Internal** for a private site. - [ ] No API keys in the static output (all ``). - [ ] `npm ci` (not `npm install`) in CI for reproducible builds. --- ## โœ… Summary You now have a **private website** deployed through a **GitHub โ†’ GitLab Pages** pipeline: - **GitHub** is the source of truth (`ryanthemanr0x/website`). - A **GitHub Action** mirrors every push to **GitLab** using a PAT secret. - **GitLab CI** (`.gitlab-ci.yml`) builds the static site into `public/`. - The `pages` job publishes it to **GitLab Pages** at `https://website-6768cb.gitlab.io`. - Because the GitLab project is **private**, the site is only visible to authenticated members โ€” free private hosting that Cloudflare Pages cannot do. This complements the [Cloudflare Pages pipeline](/blog/github-cloudflare-pipeline/) used for the public blog: public docs on Cloudflare, private sites on GitLab. ๐Ÿš€ --- *No API keys, tokens, or secrets are included in this guide โ€” every value is a `` you supply locally. Built with Astro, GitHub Actions, and GitLab CI/CD.*