6 min read

Categories

  • 技术

My personal website, pursuer.me, was built using Padrino and hosted under Linode. I had been thinking about rebuilding it for a while, as it was fairly aged in terms of its design and technology. I want to refresh it with a modern web framework and host it using more advanced technologies, preferably container + Kubernetes. Kubernetes might be an overkill for personal websites like this, but it’d be a great practice if I could try it out.

After much consideration, I decided to kick off the rebuild project last weekend. I picked a web framework called Jekyll, which is being actively developed, offers lots of functionality and provides plenty of customization options.

For hosting, currently I’m using a temporary solution utilizing my home NAS. I’m not using Kubernetes yet, given the complexity to set it up. Docker is used. The Docker image contains the static pages generated by Jekyll and a simple web server, nginx. The Docker image runs on my home NAS. Another nginx instance for the entire NAS is used as the reverse proxy to redirect external traffic to the Docker image.

Installing Ruby and Jekyll

Jekyll is built using Ruby. I need to install a Ruby environment first. I used RVM:

$ curl -sSL https://get.rvm.io | bash -s stable
# You can pick a different version
$ rvm install 3.3.0

and then install Jekyll:

$ gem install jekyll bundler
# Create the web app
$ jekyll new pursuer-me-v2
$ cd pursuer-me-v2/
# Set up RVM for the app's folder
$ rvm use --create 3.3.0@pursuer-me-v2
$ bundle install
# Try out the initial set up
# The website should be available at http://127.0.0.1:4000/ if the following command succeeds
$ bundle exec jekyll serve

Configuring

Theming

Jekyll offers plenty of themes. For my personal website I picked So Simple. Installing a new theme involves adding the theme gem to Gemfile and running bundle install afterwards:

# Remove the old theme and add the new
- gem "minima", "~> 2.5"
+ gem "jekyll-theme-so-simple", "~> 3.2.0"

The new theme needs to be registered in _config.yml by setting the theme option:

theme: jekyll-theme-so-simple

Navigation is configured based on the instructions under So Simple theme’s README.

Page titles and SEO

To generate the titles for each page, I found the plugin jekyll-seo-tag pretty handy. To install the plugin, add gem "jekyll-seo-tag" to Gemfile and add jekyll-seo-tag to the list of gems under _config.yml .

Pagination

To enable more pagination capabilities, I strongly recommend the plugin jekyll-paginate-v2 instead of jekyll-paginate . Follow the instructions under Jekyll site for more detailed guidance.

The page navigation bar needs to be styled. Navigation bar styling is already provided by So Simple theme. To enable it, I added additional HTML to the home page, similar to how it is configured in So Simple theme.

Google Analytics

Google is retiring Universal Analytics in favor of Google Analytics 4. With a theme that hasn’t updated for more than 3 years, I needed to customize the site’s footer to replace the original scripts template provided by the theme. The scripts template is referred to by the default page layout, thus included in every page.

This is done by creating a new file: _includes/scripts.html and filling in the Javascript scripts following Google Analytics’s instructions:

<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
<script>
    window.dataLayer = window.dataLayer || [];

    function gtag() {
        dataLayer.push(arguments);
    }
    gtag('js', new Date());
    gtag('config', '{{ site.google_analytics }}');
</script>

Creating a customized scripts.html provides other benefits too: I can upgrade javascript libraries’ versions to the latest instead of using the old version that was published >3 years ago.

Other options

A few other options can be configured as well, such as read_time and google_fonts . See the guide for Jekyll and the theme’s Github page for more options.

Migration

My previous personal website categorizes the posts under a few labels, including “思行”, “往昔” and “技术”. The updated website follows the same structure. This is done by creating specific pages using the category layout (provided by So Simple theme, source code), such as:

<!-- experiencing.md -->
---
layout: category
permalink: /experiencing/
title: Experiencing - 往昔
taxonomy: 往昔
---

Posts are added to the _posts folder using post layout with categories attribute set to the desired labels. For example:

<!-- _posts/2011-06-26-立志、努力、为公.md -->
---
layout: post
title: "立志、努力、为公"
date: 2011-06-26 01:00:00 UTC
categories: 思行
---
<!-- Post content -->

Deployment

Automated deployment

Automated deployment is set up for my new personal website. A typical deployment contains the following steps:

  1. For any new development, a new git branch is created for any updates to the website. Commits are uploaded to a private repo (I used Bitbucket).
  2. Once the branch is ready to be published, create a pull request to merge the branch into master branch.
  3. Continuous integration (CI) is set up, such that whenever there is a new commit under the master branch, a Docker build is triggered. I used CircleCI for CI and DockerHub for Docker image hosting. CircleCI’s config file looks like the following:
version: 2.1
jobs:
  build-and-push:
    docker:
      - image: cimg/base:2022.09
        auth:
          username: $DOCKERHUB_USERNAME
          password: $DOCKERHUB_PASSWORD
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          keys:
            - v1-{{ .Branch }}
          paths:
            - /caches/app.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/app.tar | true
      - run:
          name: Build and push application Docker image
          command: |
            TAG=0.1.$CIRCLE_BUILD_NUM
            docker build -t $DOCKERHUB_USERNAME/pursuer-me-v2-nginx:$TAG -t $DOCKERHUB_USERNAME/pursuer-me-v2-nginx:latest .
            echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
            docker push --all-tags $DOCKERHUB_USERNAME/pursuer-me-v2-nginx

workflows:
  deploy:
    jobs:
      - build-and-push:
          filters:
            branches:
              only:
                - master

and the Dockerfile looks like the following:

# Muitistage Dockerfile to first build the static site, then using nginx serve the static site
FROM ruby:latest as builder
WORKDIR /usr/src/app
COPY Gemfile ./
RUN bundle install
COPY . .
RUN JEKYLL_ENV=production bundle exec jekyll build

# Copy _sites from build to Nginx Container to serve site
FROM nginx:latest
COPY --from=builder /usr/src/app/_site /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

On my home NAS, I use Docker watchtower to monitor Docker image updates on pursuer91/pursuer-me-v2-nginx:latest and automatically restart the server when there is a new image. Here’s the config for Docker Compose:

services:
  pursuer_me:
    container_name: pursuer_me-compose
    image: pursuer91/pursuer-me-v2-nginx:latest
    restart: unless-stopped
    ports:
      - "9023:80"
  watchtower:
    container_name: watchtower-compose
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: pursuer_me-compose --interval 600

During the deployment, the site will be down for a few seconds. Thanks to the page caching provided by Cloudflare, the actual downtime should be much less.

Reverse proxy and DNS

Pursuer.me is hosted by Cloudflare. I use ddclient to periodically update the DNS record under Cloudflare and Let’s Encrypt for HTTPS.

Nginx is used as the reverse proxy so the public traffic on pursuer.me will be redirected to the Docker instance. The Nginx configuration looks like the following:

server {
    server_name pursuer.me;
    location / {
            proxy_pass http://127.0.0.1:9023;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    access_log /var/log/nginx/localhost.access_log main;
    error_log /var/log/nginx/localhost.error_log info;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/pursuer.me/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/pursuer.me/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Note that the line marked as # managed by Certbot are added by Let’s Encrypt for HTTPS certification management.

What’s next

The next step is to set up Kubernetes under my VPS to better automate the deployment process and avoid site downtime during the deployment.