summaryrefslogtreecommitdiff
path: root/src/_posts/2021-01-23-goodbye-github-pages.md
blob: e85ca81fad77ec34e7a1350851ab8c30951096af (plain)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
---
title: >-
    Goodbye, Github Pages
description: >-
    This blog is no longer sponsored by Microsoft!
tags: tech
series: selfhost
---

Slowly but surely I'm working on moving my digital life back to being
self-hosted, and this blog was an easy low-hanging fruit to tackle. Previously
the blog was hosted on Github Pages, which was easy enough but also in many ways
restricting. By self-hosting I'm able to have a lot more control over the
generation, delivery, and functionality of the blog.

For reference you can find the source code for the blog at
[{{site.repository}}]({{site.repository}}). Yes, it will one day be hosted
elsewhere as well.

## Nix

Nix is something I'm slowly picking up, but the more I use it the more it grows
on me. Rather than littering my system with ruby versions and packages I'll
never otherwise use, nix allows me to create a sandboxed build pipeline for the
blog with perfectly reproducible results.

The first step in this process is to take the blog's existing `Gemfile.lock` and
turn it into a `gemset.nix` file, which is essentially a translation of the
`Gemfile.lock` into a file nix can understand. There's a tool called
[bundix][bundix] which does this, and it can be used from a nix shell without
having to actually install anything:

```
 nix-shell -p bundix --run 'bundix'
```

The second step of using nix is to set up a nix expression in the file
`default.nix`. This will actually build the static files. As a bonus I made my
expression to also allow for serving the site locally with dynamic updating
everytime I change a source file. My `default.nix` looks like this:

```
{
    # pkgs refers to all "builtin" nix pkgs and utilities. By importing from a
    # URL I'm able to always pin this default.nix to a specific version of those
    # packages.
    pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/cd63096d6d887d689543a0b97743d28995bc9bc3.tar.gz") {},
    system ? builtins.currentSystem,
}:

    let
        # bundlerEnv looks for a Gemfile, Gemfile.lock, and gemset.nix inside
        # gemdir, and derives a package containing ruby and all desired gems.
        ruby_env = pkgs.bundlerEnv {
            name = "ruby_env";
            ruby = pkgs.ruby;
            gemdir = ./.;
        };
    in
        {
            # build will derive a package which contains the generated static
            # files of the blog. It uses the build.sh file (provided below) to
            # do this.
            build = derivation {
                name = "mediocre-blog";

                # The build.sh file (source provided below) is executed in order
                # to actually build the site.
                builder = "${pkgs.bash}/bin/bash";
                args = [ ./build.sh ];

                # ruby_env is provided as an input to build.sh so that it can
                # use jekyll, and the src directory is provided so it can access
                # the blog's source files. system is required by the derivation
                # function, and stdenv provides standard utilities to build.sh.
                inherit ruby_env system;
                src = ./src;
                stdenv = pkgs.stdenv;
            };

            # serve will derive an environment specifically tailored for being
            # run in a nix-shell. The resulting shell will have ruby_env
            # provided for it, and will automatically run the `jekyll serve`
            # command to serve the blog locally.
            serve = pkgs.stdenv.mkDerivation {
                name = "mediocre-blog-shell";

                # glibcLocales is required so to fill in LC_ALL and other locale
                # related environment vars. Without those jekyll's scss compiler
                # fails.
                #
                # TODO probably get rid of the scss compiler.
                buildInputs = [ ruby_env pkgs.glibcLocales ];

                shellHook = ''
                    exec ${ruby_env}/bin/jekyll serve -s ./src -d ./_site -w -I -D
                '';
            };
        }
```

(Nix is a bit tricky to learn, but I highly recommend chapters 14 and 15 of [the
nix manual][manual] for an overview of the language itself, if nothing else.)

The `build.sh` used by the nix expression to actually generate the static files
looks like this:

```bash
# stdenv was given a dependency to build.sh, and so build.sh can use it to
# source in utilities like mkdir, which it needs.
source $stdenv/setup
set -e

# Set up the output directory. nix provides the $out variable which will be the
# root of the derived package's filesystem, but for simplicity later we want to
# output the site within /var/www.
d="$out/var/www/blog.mediocregopher.com"
mkdir -p "$d"

# Perform the jekyll build command. Like stdenv the ruby_env was given as a
# dependency to build.sh, so it has to explicitly use it to have access to
# jekyll. src is another explicit dependency which was given to build.sh, and
# contains all the actual source files within the src directory of the repo.
$ruby_env/bin/jekyll build -s "$src" -d "$d"
```

With these pieces in place I can easily regenerate the site like so:

```
nix-build -A build
```

Once run the static files will exist within a symlink called `result` in the
project's root. Within the symlink will be a `var/www/blog.mediocregopher.com`
tree of directories, and within that will be the generated static files, all
without ever having to have installed ruby.

The expression also allows me to serve the blog while I'm working on it. Doing
so looks like this:

```
nix-shell -A serve
```

When run I get a normal jekyll process running in my `src` directory, serving
the site in real-time on port 4000, once again all without ever installing ruby.

As a final touch I introduced a simple `Makefile` to my repo to wrap these
commands, because even these were too much for me to remember:

```
result:
	nix-build -A build

install: result
	nix-env -i "$$(readlink result)"

clean:
	rm result
	rm -rf _site

serve:
	nix-shell -A serve

update:
	nix-shell -p bundler --run 'bundler update; bundler lock; bundix; rm -rf .bundle vendor'
```

We'll look at that `install` target in the next section.

## nginx

So now I have the means to build my site quickly, reliably, and without
cluttering up the rest of my system. Time to actually serve the files.

My home server has a docker network which houses most of my services that I run,
including nginx. nginx's primary job is to listen on ports 80 and 443, accept
HTTP requests, and direct those requests to their appropriate service based on
their `Host` header. nginx is also great at serving static content from disk, so
I'll take advantage of that for the blog.

The one hitch is that nginx is currently running within a docker container,
as are all my other services. Ideally I would:

* Get rid of the nginx docker container.
* Build a nix package containing nginx, all my nginx config files, and the blog
  files themselves.
* Run that directly.

Unfortunately extracting nginx from docker is dependent on doing so for all
other services as well, or at least on running all services on the host network,
which I'm not prepared to do yet. So for now I've done something janky.

If you look at the `Makefile` above you'll notice the `install` target. What
that target does is to install the static blog files to my nix profile, which
exists at `$HOME/.nix-profile`. nix allows any package to be installed to a
profile in this way. All packages within a profile are independent and can be
added, updated, and removed atomically. By installing the built blog package to
my profile I make it available at
`$HOME/.nix-profile/var/www/blog.mediocregopher.com`.

So to serve those files via nginx all I need to do is add a read-only volume to
the container...

```
-v $HOME/.nix-profile/var/www/blog.mediocregopher.com:/var/www/blog.mediocregopher.com:ro \
```

...add a new virtual host to my nginx config...

```
server {
    listen       80;
    server_name  blog.mediocregopher.com;
    root         /var/www/blog.mediocregopher.com;
}
```

...and finally direct the `blog` A record for `mediocregopher.com` to my home
server's IP. Cloudflare will handle TLS on port 443 for me in this case, as well
as hide my home IP, which is prudent.

## Deploying

So now it's time to publish this new post to the blog, what are the actual
steps? It's as easy as:

```
make clean install
```

This will remove any existing `result`, regenerate the site (with the new post)
under a new symlink, and install/update that newer package to my nix profile,
overwriting the previous package which was there.

EDIT: apparently this isn't quite true. Because `$HOME/.nix-profile` is a
symlink docker doesn't handle the case of that symlink being updated correctly,
so I also have to do `docker restart nginx` for changes to be reflected in
nginx.

And that's it! Nix is a cool tool that I'm still getting the hang of, but
hopefully this post might be useful to anyone else thinking of self-hosting
their site.

[jekyll]: https://jekyllrb.com/
[bundix]: https://github.com/nix-community/bundix
[manual]: https://nixos.org/manual/nix/stable/#chap-writing-nix-expressions