Containers from scratch

Building docker container for Lua development

Story

For the past few weeks we’ve been designing and building a project in Lua. To speed up our work flow, we needed to set up CI pipelines. Unfortunately available Lua images on docker hub are mostly unmaintained and out of date.

That is why we’ve built our own image, and decided to document the process!

Crafting Dockerfile

Let’s start with base image. Alpine has a small size and great package manager, so it seemed like a perfect choice.

FROM alpine:latest

A good idea is to start with Alpine, then upgrade to something more feature full (like Debian or Ubuntu), in case Alpine is missing features we need.

Next, we define “ARG” variables for lua and luarocks versions. ARG instructions define variables that are passed at build-time. They will not be available in the resulting container.

ARG LUA_VERSION
ARG LUA_ROCKS_VERSION

We use these later to download specific versions of all binaries.

Some utilities to build our binaries:

RUN apk add make gcc musl-dev openssl wget unzip

Then we download, build and install our chosen Lua version:

RUN wget http://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz && \
    tar xf lua-${LUA_VERSION}.tar.gz && \
    cd lua-${LUA_VERSION} && \
    make all test && \
    ln -s /lua-${LUA_VERSION}/src/lua /usr/bin/lua

Same idea for luarocks:

RUN wget https://luarocks.org/releases/luarocks-${LUA_ROCKS_VERSION}.tar.gz && \
    tar zxpf luarocks-${LUA_ROCKS_VERSION}.tar.gz && \
    cd luarocks-${LUA_ROCKS_VERSION} && \
    ./configure --with-lua-include=/lua-${LUA_VERSION}/src/ && \
    make bootstrap

We end our container with a default command. Technically this is not required, but we consider it a good practice.

CMD ["lua", "-v"]

Here is the final Dockerfile. It should easily allow us to build any Lua version we need.

FROM alpine:latest

ARG LUA_VERSION
ARG LUA_ROCKS_VERSION

RUN apk add make gcc musl-dev openssl wget unzip

RUN wget http://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz && \
    tar xf lua-${LUA_VERSION}.tar.gz && \
    cd lua-${LUA_VERSION} && \
    make all test && \
    ln -s /lua-${LUA_VERSION}/src/lua /usr/bin/lua

RUN wget https://luarocks.org/releases/luarocks-${LUA_ROCKS_VERSION}.tar.gz && \
    tar zxpf luarocks-${LUA_ROCKS_VERSION}.tar.gz && \
    cd luarocks-${LUA_ROCKS_VERSION} && \
    ./configure --with-lua-include=/lua-${LUA_VERSION}/src/ && \
    make bootstrap

CMD ["lua", "-v"]

To make sure it works, save it as “Dockerfile”, then run docker build -t .

Crafting CI pipelines

We are ready to define pipelines that will build our image. Then it will tag the image, and push it to docker hub.

Since we need docker for container operations, set base image as “docker:latest” and set up “dind” service (docker in docker).

image: docker:latest

services:
  - docker:dind

Most important part of the pipelines is our variables. Specifically LUA_VERSION and LUA_ROCKS_VERSION. They will allow us to define versions of the binaries inside the container. Additionally LUA_VERSION will allow us to tag our container when we push it to docker hub.

variables:
  CI_REGISTRY: "docker.io"
  CI_REGISTRY_IMAGE: "index.docker.io/antfarminteractive/lua"
  LUA_VERSION: "5.4.2"
  LUA_ROCKS_VERSION: "3.4.0"

We add “build” stage to build our container, tag it, login and push it to dockerhub. Below is the complete .gitlab-ci.yaml. Notice there are two build stages. test-build only builds a container (doesn’t tag or push it), and is used for merge requests.

image: docker:latest

services:
  - docker:dind

variables:
  CI_REGISTRY: "docker.io"
  CI_REGISTRY_IMAGE: "index.docker.io/antfarminteractive/lua"
  LUA_VERSION: "5.4.2"
  LUA_ROCKS_VERSION: "3.4.0"

test-build:
  stage: build
  script:
    - docker build --build-arg LUA_VERSION=$LUA_VERSION --build-arg LUA_ROCKS_VERSION=$LUA_ROCKS_VERSION -t "$CI_REGISTRY_IMAGE:$LUA_VERSION" .
  except:
    - master

build:
  stage: build
  script:
    - docker build --build-arg LUA_VERSION=$LUA_VERSION --build-arg LUA_ROCKS_VERSION=$LUA_ROCKS_VERSION -t "$CI_REGISTRY_IMAGE:$LUA_VERSION" .
    - docker tag "$CI_REGISTRY_IMAGE:$LUA_VERSION" "$CI_REGISTRY_IMAGE:latest"
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker image push --all-tags $CI_REGISTRY_IMAGE
  only:
    - master

There is one last step to complete the puzzle. We need to create an access token on dockerhub, so gitlab runner is allowed to login and push our container. Here are instructions from docker docs for managing access tokens.

Go to Repository > Settings > CI / CD > Variables and add:

  • CI_REGISTRY_USER - Put your dockerhub username here.
  • CI_REGISTRY_PASSWORD - Put your newly created access token.

Gitlab Variables

Conclusion

With this simple setup, we’ve created a pipeline. It builds a docker container for specific version of Lua interpreter (and luarocks as a bonus).

When new version of Lua is released, LUA_VERSION update will build a new container and automatically push it to dockerhub.

Latest code for all files in this post can be found at: https://gitlab.com/antfarm-interactive/lua-container.