Um dos principais problemas ao desenvolver páginas para o GitHub Pages, utilizando o Jekyll, é testar as alterações nas paginas, antes de publicá-las, uma vez que, até hoje, não existe uma área de “espera” (staging area), onde é possível visualizar as páginas sem publicá-las. Uma solução para esse prbolema é executar um container com podman, criando, na máquina de desenvolvimento, um ambiente semelhante ao oferecido pelo GitHub.
O Jekyll é um sistema de geração de sites estáticos, com suporte a blog, implementado em Ruby, e que permite uma série de customizações com relação a plugins para modificar o comportamento ou a forma como as páginas são geradas. Os plugins são gems Ruby, e alguns destes plugins requerem ferramentas de desenvolvimento local (como o toolchain GCC) para a sua compilação e instalação.
A diferença de versões de Ruby, ferramentas de desenvolvimento, ou gems instaladas na máquina de desenvolvimento, dificultam a criação do ambiente para gerar e testar, localmente, as páginas editadas, apesar da ferramenta bundle, a qual já facilita muito esse processo.
Uma forma de obter um ambiente estável e reproduzível, mesmo em plataformas diferentes, é o uso de containers, popularizados a partir do Docker.
Containers fornecem uma forma de criar um ambiente isolado sobre o sistema operacional, isolando as camadas de software necessária para um ambiente específico. Esse isolamento de container, permite limitar o uso de CPU, armazenamento, e memória, além de isolar todo o stack de software utilizado do sistema hospedeiro.
Definimos o ambiente do container a partir de um arquivo (Dockerfile), onde descrevemos todos os recursos de hardware e software que serão utilizados. É a partir deste arquivo de descrição que será criada uma imagem para o container.
Toda imagem é, normalmente, criada a partir de uma imagem pré-existente.
Selecionamos a imagem base a partir da diretiva FROM
. Para criar uma
imagem que nos permita simular o ambiente do GitHub Pages, executando o
Jekyll, utilizamos uma imagem oficial da liguagem de programação
Ruby.
FROM ruby:alpine
Precisaremos de um toolchain GCC para compilar alguns módulos (gem)
que não são 100% Ruby, e para isso, executamos o comando apk
, que é o
gerenciador de pacotes do Alpine Linux,
utilizando a diretiva RUN
.
RUN apk add --no-cache make gcc g++ libc-dev openssl-dev
Ao executar o container precisamos instalar as gems utilizadas pelo
site, e iniciar o servidor do Jekyll. Como esse processo envolve dois
programas em execução, definimos o ENTRYPOINT
como:
ENTRYPOINT ["/bin/sh", "-c", \
"bundle install && \
bundle exec jekyll server --trace --host 0.0.0.0" \
]
O servidor do Jekyll executa na porta 4000 (TCP), então temos que expor essa porta do container:
EXPOSE 4000
Ainda precisamos nos preocupar com um ponto muito importante: como o Jekyll irá acessar o codígo fonte (Markdown) do site?
Para isso configuramos um VOLUME
, que permitirá que um diretório no
host seja compartilhado com o container:
VOLUME ["/srv/jekyll"]
O mapeamento do diretório compartilhado será feito na chamada de execução do container.
Um último problema a ser solucionado é que uma vez criada a imagem,
todas as alterações da imagem feitas pelo bundle install
são perdidas,
quando o container termina a execução, e a instalação do bundle deve
ser refeita sempre que o container inicializar, incluindo acessar os
repositórios remotos de código para as gems.
Para evitar isso, podemos configuar a imagem para utilizar um diretório específico para armazenar as gems e todos os plugins utilizados pelo Jekyll, e associar esse diretório com um diretório do host, criando um cache para os arquivos.
Essa configuração envolve uma série de variáveis de ambiente, que são
definidas com a diretiva ENV
.
Abaixo encontra-se o arquivo Dockerfile
completo para a criação do
container:
FROM ruby:alpine
RUN apk add --no-cache make gcc g++ libc-dev openssl-dev
VOLUME ["/srv/jekyll"]
EXPOSE 4000
ENV GEM_HOME="/usr/local/bundle"
ENV BUNDLE_PATH=/usr/local/bundle \
BUNDLE_SILENCE_ROOT_WARNING=1 \
BUNDLE_APP_CONFIG=/usr/local/bundle
ENV PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN mkdir -p "/usr/local/bundle"
WORKDIR "/srv/jekyll"
ENTRYPOINT ["/bin/sh", "-c", \
"bundle install && \
bundle exec jekyll server --trace --host 0.0.0.0" \
]
Agora podemos criar a imagem para o container, e associar a ela um TAG
(_my_user_name/site_server_):
podman build -t "my_user_name/site_server" /path/to/Dockerfile/directory
Uma vez que a imagem é criada, podemos inicir um container, definindo o diretório com os arquivos fontes do site, e o diretório que será utilizado como cache (lembre-se de criar o diretório antes, caso ele não exista):
podman run \
--volume "/path/to/website:/srv/jekyll:Z" \
--volume "/path/to/website/.vendor/bundle:/usr/local/bundle:Z" \
-p 4000:4000 \
"my_user_name/site_server"
Agora, com o container executando, é possivel acessar o site gerado pelo Jekyll no endereço http://localhost:4000.
Agora que temos um ambiente reproduzível, precisamos configurar os plugins de forma a simular o ambiente do GitHub Pages.
As versões utilizadas pelo GitHub são públicas
e basta que as versões definidas no Gemfile
limitem as versões das
gems a essas versões disponíveis no Github.
Abaixo, está a contfiguração utilizada nesse site, na data que esse post foi escrito:
source "https://rubygems.org"
gem "jekyll", "~> 3.9.3"
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
gem "kramdown", "~> 2.3.2"
gem "kramdown-parser-gfm"
gem "webrick", "~> 1.7"
gem "ffi", "~> 1.15.5"
Manter uma configuração para utilizar o Jekyll em diferentes ambientes e diferentes sistemas operacionais pode ser muito trabalhoso, principalmente por causa das versões e dependências da linguagem Ruby e das gems utilizadas pelo Jekyll.
Utilizando containers, podemos criar um ambiente que pode ser reproduzido com certa facilidade, mesmo em sistemas operacionais diferentes, e mantendo a compatibilidade com o GitHub Pages, de forma a permitir a visualização das páginas editadas antes da sua publicação.
Na elaboração deste site, eu utilizo um script que me permite criar a imagem do container apenas se necessário, e iniciar o container sem me preocupar com a configuração necessária na linha de comando, permitindo que o processo seja apenas copiar o repositório e executar o script.
Abaixo você encontra esse script, que foi testado apenas no Linux,
mas deve funcionar em qualquer shell razoavelmente compatível com o
Bash (como o zsh
do macOS).
Para executar o script, basta que ele esteja no mesmo diretório do Dockerfile.
#!/bin/sh
SCRIPTDIR="$(realpath $(dirname "$0"))"
TOPDIR="$(dirname "${SCRIPTDIR}")"
TAG="$(whoami)/site_server"
[ -d "${TOPDIR}/.vendor/bundle" ] || mkdir -p "${TOPDIR}/.vendor/bundle"
existing=$(podman images -f reference="localhost/${TAG}" --format "{{ .Repository }}")
[ -z "${existing}" ] && podman build -t "${TAG}" "${SCRIPTDIR}"
podman run \
--volume "${TOPDIR}:/srv/jekyll:Z" \
--volume "${TOPDIR}/.vendor/bundle:/usr/local/bundle:Z" \
-p 4000:4000 \
"${TAG}"