Migrating from GitHub Pages to GitLab Pages with HTTPS

Background

I wanted HTTPS for my GitHub Pages blog. CloudFlare helped, but browsers still complained about the certificate. GitHub Pages only supported HTTPS on the default github.io domain at the time (custom domains are supported now), so I moved to GitLab Pages, which lets you upload your own cert.

My blog runs on Hexo.

Migrating the Pages project

Sign up for GitLab and create a project named yourname.gitlab.io. You can make it private—nice compared to GitHub’s public-only limitation for free users.

GitLab can import GitHub repositories, but do not simply import your old Hexo repo; GitLab and GitHub treat Hexo differently. Build a fresh Hexo project locally instead, then copy over the source folder and _config.yml. The last missing piece is .gitlab-ci.yml.

(The official Hexo template repository on GitLab was empty when I migrated, so cloning it went nowhere.)

.gitlab-ci.yml

GitHub Pages relies on hexo g + hexo d: generate the static files locally and push them. GitLab Pages can generate the static site on the server by running the scripts defined in .gitlab-ci.yml. In theory that is convenient; in practice the shared runners can be slow, taking hours to rebuild minor changes.

Here is the config I used (adapted from a community example—the official snippet included an unnecessary hexo deploy):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Full project: https://gitlab.com/pages/hexo
image: node:6.10.0

pages:
script:
- npm install
- ./node_modules/hexo/bin/hexo generate
artifacts:
paths:
- public
cache:
paths:
- node_modules
key: project
only:
- master

Push content

Add your SSH key in GitLab and push the repository. No need to run hexo locally because the pipeline generates public/ for you. If you copied directories from the old repo, make sure to clear their original git history—otherwise Git will refuse to push.

Pipelines

After pushing, GitLab runs the pipeline defined in .gitlab-ci.yml. Check the Pipelines tab for status: pending, running, passed, or failed. pending and running can take a while. Click a job to view its console output (use the Raw button for plain text).

If a job fails, read the log. For example, Git complained about missing user info:

1
2
3
4
5
6
*** Please tell me who you are.

Run

git config --global user.email "[email protected]"
git config --global user.name "Your Name"

Set those values in the pipeline before continuing.

Configure a custom domain and HTTPS

DNS

Add a CNAME (www) pointing to yourname.gitlab.io. GitLab provides verifiable DNS records; use them to prove domain ownership.

Let’s Encrypt validation

I generated certificates with Certbot on a VPS that held my DNS zones:

sudo ./letsencrypt-auto certonly --manual -d yoursite.com -d www.yoursite.com

Certbot prints the token needed for the HTTP-01 challenge. There are two ways to publish it.

Method 1 (one-off): add commands to .gitlab-ci.yml that create the challenge file under public/.well-known/acme-challenge/. Commit, push, wait for the pipeline, then continue the verification.

Method 2 (easier to reuse): install hexo-processor-static so everything under source/_static is copied verbatim to public/.

Add to .gitlab-ci.yml:

- npm install hexo-processor-static --save

Create source/_static/.well-known/acme-challenge/<token> containing the validation string, then push. Once the pipeline finishes, rerun the validation on the server.

Enable HTTPS in GitLab

Certbot stores the cert and key at:

1
2
/etc/letsencrypt/live/YOURDOMAIN/fullchain.pem
/etc/letsencrypt/live/YOURDOMAIN/privkey.pem

In your GitLab project go to Settings → Pages, remove the old domain if necessary, and add it again with HTTPS enabled. Copy the contents of fullchain.pem into “Certificate (PEM)” and privkey.pem into “Key (PEM)”. After a short delay https://YOURDOMAIN should be live.

Hexo theme tweaks

Force HTTPS redirects in the NexT theme by editing layout/_layout.swig and adding inside <head>:

1
2
3
4
5
<script type="text/javascript">
var host = "yoursite.com";
if ((host == window.location.host) && (window.location.protocol != "https:"))
window.location.protocol = "https";
</script>

Tell search engines to prefer HTTPS with a canonical link:

<link rel="canonical" href="https://YOURDOMAIN/specific/page" />

Commit and push to trigger another build.

Renew certificates

Let’s Encrypt certificates expire every 90 days. Repeat the validation steps to renew, or automate it with gitlab-letsencrypt:

1
2
3
4
5
6
7
8
npm install -g gitlab-letsencrypt

gitlab-le --email [email protected] --token ... \
--domain yoursite.com www.yoursite.com \
--repository https://gitlab.com/you/blog \
--path source/_static/.well-known/acme-challenge

# Run with --production after confirming the dry run works

References:

  1. GitLab Pages documentation
  2. https://zongren.me/2016/07/20/add-tls-to-gitlab-pages/》
  3. Tutorial: Securing your GitLab Pages with TLS and Let’s Encrypt
  4. 为Github的Hexo博客启用SSL/TLS