This page looks best with JavaScript enabled

Deploying a Hexo Static Site with a Container Image

 ·  ☕ 4 min read

1. Why Deploy Independently with an Image

  • Better SEO

With GitHub Pages, deploying a static site is very convenient. Configure it once, and every commit deploys and updates automatically. Add jsDelivr and Cloudflare acceleration on top, and among free options it is already production-ready.

Unfortunately, Baidu’s search engine indexes GitHub Pages sites very slowly, or not at all. Even using Cloudflare caching — and Cloudflare partners with Baidu — actively submitting a sitemap, and adding a push script to every page, it still had no effect. Google and Bing index far more, 4-5 times as much as Baidu. Another option is multi-line DNS resolution, giving Baidu’s crawler a dedicated IP for SEO, but that means deploying another whole service.

Deploying on a dedicated host is friendlier to SEO.

  • Closer to cloud native

On a physical machine, environment setup scripts have poor compatibility and services are hard to maintain. In the cloud-native era, containers are the obvious first choice — you just package the service into an image.

The image build can be configured to run automatically in CI, while deployment is just managing one container.

Easy to use, easy to maintain — that is cloud native.

2. Packaging a Static File Service

Static site frameworks like Hexo, Jekyll, and Hugo render Markdown into HTML to serve content. For deployment, all you need is Nginx serving the static files.

2.1 Add a Dockerfile

Add a Dockerfile at the project root with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM node:10 as builder
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org && \
    cnpm i -g hexo-cli
ADD ./ /home
WORKDIR /home
RUN cnpm i && hexo g

FROM nginx:1.19
ADD ./conf/nginx/default.conf /etc/nginx/conf.d/
ADD ./conf/cert/* /etc/nginx/certs/
COPY --from=0 /home/public /var/www/

Multi-stage builds effectively reduce the image size. The files referenced by the ADD instructions are described in detail below.

2.2 Add Nginx Configuration Files

Add the following three files under the project:

1
2
3
4
5
6
7
8
9
tree -L 2 conf
conf
├── cert
│   ├── 1_www.chenshaowen.com_bundle.crt
│   └── 2_www.chenshaowen.com.key
└── nginx
    └── default.conf

2 directories, 3 files

The content of conf/nginx/default.conf is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
    listen 80;
    server_name chenshaowen.com www.chenshaowen.com;
    server_tokens off;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name chenshaowen.com www.chenshaowen.com;
    server_tokens off;

    ssl_certificate /etc/nginx/certs/1_www.chenshaowen.com_bundle.crt;
    ssl_certificate_key /etc/nginx/certs/2_www.chenshaowen.com.key;

    location / {
        root   /var/www;
        index  index.html;
    }

    error_page  404              /404.html;
}

This forwards HTTP to HTTPS and bare-domain requests to www.

The .crt and .key files are the domain’s certificate files; many cloud vendors offer free HTTPS certificates. Tencent Cloud’s free HTTPS certificate is recommended here — simple validation, fast issuance, and it is downloadable.

3. GitHub Actions Builds the Image Automatically

3.1 Add a Makefile

Content as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
build:
	docker build -f Dockerfile -t ghcr.io/shaowenchen/documents:latest .

install:
	yum install -y yum-utils
	yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
	yum install  -y docker-ce-19.03.8-3.el7
	systemctl start docker
	systemctl enable docker
	yum install -y python3-pip
	pip3 install docker-compose

The Makeffile defines two targets: build builds the image, and install sets up the Docker runtime on a physical machine.

3.2 Define the CI Build-and-Push Pipeline

Add the file .github/workflows/build.yaml at the project root with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: build

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Build image
        run: make build

      - name: Login Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GHCR_TOKEN }}

      - name: Push image
        run:
          docker push ghcr.io/shaowenchen/documents:latest

The secrets.GHCR_TOKEN here needs to be created on the https://github.com/settings/tokens/new page.

Then add it to the project’s Settings as the value of the GHCR_TOKEN variable.

4. Deploying the Service

In the example I use the ghcr.io registry; if it is not fast enough to access from servers in China, you can switch to an Alibaba Cloud registry.

After committing the configuration above, GitHub Actions automatically builds and pushes the image, as shown below.

You can see the image under Packages on your profile page, as shown below.

Once the image is set to Public, it can be pulled without logging in. Below, the service is deployed with docker-compose.

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
version: "3"

services:
  documents:
    restart: always
    container_name: documents
    image: ghcr.io/shaowenchen/documents:latest
    ports:
      - 80:80
      - 443:443

Run the command to start the service:

1
docker-compose up -d

To update the service, pull the image first, then stop the service and start it again:

1
2
3
docker-compose pull
docker-compose down
docker-compose up -d

Since this is just an Nginx serving static files, starting and stopping the service is very fast.


微信公众号
WRITTEN BY
微信公众号