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
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:
- 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).
- Once the branch is ready to be published, create a pull request to merge the branch into
master
branch. - 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.