layout | title |
---|---|
post |
How I Built howlinbash.com |
TL/DR: I built howlinbash.com the long way.
I needed a blog.
I didn't spend too much time shopping before I settled with a self-hosted implementation of Jekyll. I'm a bit of a command line junkie. The draw of writing, formating and publishing each blogpost without having to touch a mouse was too enticing to pass.
The next step was to plan out a modular development structure for the website.
I decided to split the code into three repositories: blogposts, website and website theme. By isolating the blogposts, I could compensate my lack of a GUI by drafting posts from github.com if I needed to. By seperating the theme from the main site, I could switch to a new theme without having to refactor the entire codebase and I could potentially use either repo for future websites. I chose Hyde by @mdo as a base theme. Forked it, updated it and called it Heidi.
Commiting to this model, the challenge now was to figure out exactly what each repo would look like, how to develop each side-by-side and how to bring them together as the finished site.
After a little research, I decided to turn the blogpost repo into a submodule of the main site repo. A simple git submodule update
command would fetch the latest posts. However, the submodule approach proved too clumsy for the theme repo, so I decided instead to develop Heidi as a seperate ruby gem.
I had a number of requirements from my server:
- TLS encryption ( https:// instead of http:// )
- A staging domain to make sure the site works in a live environment
- A reverse-proxy to easy facilitate future capabilities
- A docker-centric workflow ( my latest infatuation )
I began developing the most excessive, scalable, docker server cluster I could imagine; With load balancing, continuous deployment, failover, all spread across as many virtual servers as I desired. By the time I was knee deep in iptables documentation, I realised I'd gone too far. Vowing to return one day to my mega cluster, I pushed on, building a simple NGiNX reverse-proxy server before finding a superb pre-built solution on GitHub.
The server now consists of five parts
- The main NGiNX reverse-proxy container
- A container that generates and updates the reverse-proxy configuration
- A container that handles the acquisition and renewal of the TLS encryption certificates
- A container with the staging website available at https://dev.howlinbash.com
- And finally a container with the live website: https://howlinbash.com
To build a seperate theme whilst building a website turned out to be my most disruptive decision. My most puzzling problem to solve.
- Which repo should I commit which code changes to?
- How can I make sure the changes I make to the theme will load on the site?
To clarify:
My goal was to build a website, it was not to build a theme. The theme would be a by-product of the website. Ideally as I built the website, the theme would magically build itself behind the scenes, without me having to think about it. This meant that I would need to develop the website from both the theme repo and the website repo almost simultaneously.
There were no two ways about it. I needed a bash script.
The script is run from main.sh
( which can be aliased to whatever you like ).
It takes a number of options initialised by a simple case statement and two of those options also take a further option, again determined by a case statement.
Below is a description of what each option does and how it works.
Move posts, pages & config from site repo to theme repo for development.
Although the code I commit will be pushed to my theme repo rather than my site repo, by initially loading the files and configuration specific to my website, my theme server will appear as if i'm working on my website code directly.
sed
comments out the howlinbash specific settings in the config filegit update-index --assume-unchanged
ignores the howlinbash specific files whilst developing
Serve and watch theme or site or preview blogpost.
The serve option itself has four options
theme
for theme focused developmentsite
for site focused developmentpost
to preview a draft blogpoststop
to stop a running site server
If I serve my theme, the script moves to the theme repo and runs bundle exec guard
to serve a live-reloading website to localhost.
However, if I serve my site, the script
- moves to the site repo
- starts a docker container that watches and builds the site to
/web
- starts a second container that watches and serves the same directory to localhost
To preview blogposts I decided to re-appropriate gits functionality to the needs of the script.
- the blogposts repo has 2 remote branches:
master
anddrafts
- each blogpost has it's own local branch that is merged with
drafts
each time a post is previewed
The script
- starts the above site server
- commits the post to the local blogpost branch
- merges the the blogpost branch to
drafts
which is then pushed to GitHub - switches to the site repo (which contains the blogposts repo as a submodule)
- switches the blogposts submodule branch from
master
todrafts
- pulls the latest draft blogpost
- the site server, started above, spots the change and rebuilds the site for preview
This process is resolved when a blogpost is posted to the website with the post
option, detailed below.
The stop
option simply stops and removes the containers that are serving the site locally.
Test theme gem by pushing, pulling and building from local gem server.
To avoid commiting broken code I have to be able to test my theme works when loaded as a gem.
The script
- loads any changes from the theme config file to the site config file
- starts a docker container with a local geminabox gem server
- switches the theme settings to test theme settings in the config file
- makes a test gem and pushes it to the geminabox server
- serves the test site in a docker container to localhost
- reverts all config settings back to the original theme settings
Update theme version number, commit changes and post gem to rubygems.org.
When I know that the theme works as a gem and I'm happy with my changes, I bump, commit and publish.
The script
- makes a gem
- bumps the version number in the relevant config files
- pushes the gem to rubygems
- commits the changes to GitHub
- interactively commits the config file to commit individual sections if needed
- tags the commit with the version number
Deploy code to howlinbash.com
The deploy option itself has three options
stage
pushes the latest code to staging @ dev.howlinbash.comlive
switches the live code ( @ howlinbash.com ) with the staged coderevert
reverses the switch just in case some bad code slips through
The deploy system relies mainly on docker tags to move and switch docker images.
The tags are:
next
this is the latest image. The latest code writtencurrent
this is the code you can currently see at howlinbash.comprevious
the last website that was live
While the above images live on docker hub and are pushed and pulled from my local machine to my server, the next two images only live on the server and determine what is viewable at dev.howlinbash.com and howlinbash.com
staging
the image viewable at dev.howlinbash.comlive
the image viewable at howlinbash.com
stage
- pulls the latest blogposts
- builds an image and tags it
next
- pushes the image to docker hub
- SSHes into the server
- pulls the
next
image and retags itstaging
- reboots the server to load image to dev.howlinbash.com
live
- switches the
current
image toprevious
on docker hub - pushes the recently built
next
image to docker hub ascurrent
- SSHes into the server
- retags the
staging
image aslive
- pulls the
previous
image from docker hub asstaging
- reloads the server to load images to dev.howlinbash.com and howlinbash.com
revert
- SSHes into the server
- switches the
staging
image with thelive
image on the server - reloads the server
- switches the
previous
image with thecurrent
image on docker hub
![deploy diagram]({{ site.url }}/assets/img/deploy.jpg)
Post blogpost to website.
Resolving the git appropriation from serve post
, the script pushes the completed post to master
deletes the draft from drafts
and then merges master
into drafts
to keep drafts
housing drafts and all complete posts and master
housing just the complete posts.
The script
- reads the post filename from the user
- checksout blogposts
master
- checksout the blogpost file from the blogpost branch
- commits to
master
and pushesmaster
to GitHub - merges
master
todrafts
and pushesdrafts
to GitHub - builds a
next
site image with the latest post from blogpostmaster
- switches
current
toprevious
on docker hub - pushes
next
image to docker hub ascurrent
- SSHes into server
- pulls
previous
asstaging
andcurrent
aslive
- reloads server