Creating a Lab Container

2025-04-02

I tend to create environments for my labs using containers. These containers would hold the resources I need for a certain job. Here's a guide on how I usually create them from scratch.

Project Structure

Let’s assume we have a folder named lab-container with the following structure:

lab-container/ ├── Dockerfile ├── build │ └── binaries.sh ├── .dockerignore ├── Makefile ├── .github │ └── workflows │ └── ci.yml ├── README.md └── zshrc

We'll walk through each file step by step.


The Dockerfile

Here’s a multi-stage Dockerfile that compiles and strips binaries in a builder stage, then copies them into a smaller final image:

FROM alpine:3.19 AS builder

WORKDIR /build

ARG TERRAFORM_VERSION="1.3.9"
ARG VAULT_VERSION="1.12.4"
ARG HELM_VERSION="3.11.2"
ARG KUBECTL_VERSION="v1.26.1"
ARG KIND_VERSION="v0.17.0"
ARG TRAEFIK_VERSION="2.9.6"
ARG TASK_VERSION="3.19.0"

RUN apk update && apk add --no-cache \
      bash \
      curl \
      wget \
      unzip \
      git \
      tar \
      ca-certificates \
      upx \
      binutils  # `strip` and `upx` to reduce binary sizes

COPY build/binaries.sh /build/binaries.sh
RUN chmod +x /build/binaries.sh && /build/binaries.sh

# Optionally strip and compress
RUN strip --strip-unneeded /usr/local/bin/* || true
RUN upx --best --lzma /usr/local/bin/* || true

################################################################################
# Final image
################################################################################
FROM alpine:3.19

LABEL maintainer="YourName" \
      description="A lightweight DevOps toolbox container."

WORKDIR /labs
ENV PATH="/labs/bin:$PATH"

RUN apk update && apk add --no-cache \
      bash \
      ca-certificates \
      python3 \
      py3-pip \
      aws-cli \
      zsh \
      jq \
      fzf \
      eza

COPY --from=builder /build/bin/ /usr/local/bin/

COPY zshrc /root/.zshrc

SHELL ["/bin/zsh", "-c"]

CMD ["zsh"]

Explanation

Builder Stage:

  • Installs dependencies (bash, curl, tar, etc.) needed to fetch and install binaries.

  • Copies binaries.sh which downloads and installs Terraform, Vault, Helm, etc.

  • Uses strip and upx to reduce final binary sizes.

Final Stage:

  • Starts with a fresh Alpine base.

  • Installs additional packages (e.g., AWS CLI, zsh, jq, fzf).

  • Copies the compiled binaries from the builder stage.

  • Sets up a default shell (zsh) and copies a zshrc config file for a nice shell environment.

build/binaries.sh

This script downloads and installs each DevOps tool. For example:

#!/usr/bin/env bash
set -euxo pipefail

# In this script, you can use $TERRAFORM_VERSION, $VAULT_VERSION, etc.
# to download each tool. For instance:
wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip
unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin
rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip

# Similarly for Vault:
wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
unzip vault_${VAULT_VERSION}_linux_amd64.zip -d /usr/local/bin
rm vault_${VAULT_VERSION}_linux_amd64.zip

# ... repeat for Helm, kubectl, kind, Traefik, Task, etc.
# e.g. for kubectl:
curl -L "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl
chmod +x /usr/local/bin/kubectl

# For kind:
curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
chmod +x /usr/local/bin/kind

# For Task CLI:
curl -L https://github.com/go-task/task/releases/download/v${TASK_VERSION}/task_linux_amd64.tar.gz | tar -zx -C /usr/local/bin

Tip: Keep the script idempotent. If you run it twice, it shouldn’t break anything.

.dockerignore

The .dockerignore helps exclude files you don’t want in your build context (like .git, logs, local environment files, etc.):

.env
.DS_Store
*.log
.git
.gitignore
README.md
node_modules/

Adjust to your needs.

Makefile

A simple Makefile can streamline common Docker tasks:

IMAGE_NAME = your-dockerhub-username/devops-toolbox
VERSION = v1.0.0
LATEST = latest

.PHONY: build
build:
	docker build -t $(IMAGE_NAME):$(LATEST) .

.PHONY: run
run:
	docker run --rm -it $(IMAGE_NAME):$(LATEST) zsh

.PHONY: test
test:
	docker run --rm $(IMAGE_NAME):$(LATEST) sh -c ' \
		echo "Checking installed tools..."; \
		terraform --version && \
		vault --version && \
		aws --version && \
		kubectl version --client && \
		helm version && \
		kind version && \
		traefik version && \
		task --version && \
		docker version \
	'

.PHONY: push
push:
	docker tag $(IMAGE_NAME):$(LATEST) $(IMAGE_NAME):$(VERSION)
	docker push $(IMAGE_NAME):$(VERSION)
	docker push $(IMAGE_NAME):$(LATEST)

.PHONY: clean
clean:
	docker image prune -f

.PHONY: deploy-k8s
deploy-k8s:
	kubectl run toolbox --image=$(IMAGE_NAME):$(LATEST) -- sleep infinity

.PHONY: exec-k8s
exec-k8s:
	kubectl exec -it toolbox -- zsh

Explanation

build: Builds the Docker image locally with the latest tag.

run: Launches the container in an interactive shell.

test: Runs each tool’s --version to confirm they’re installed.

push: Tags and pushes the image to Docker Hub.

deploy-k8s: Runs the container in a Kubernetes pod.

exec-k8s: Opens a shell into that running pod.

zshrc

For a better shell experience, add your Zsh config:

export PATH="/usr/local/bin:$PATH"
export EDITOR=vim

# Enable autocompletion
autoload -Uz compinit
compinit

# Enable fzf for fuzzy search
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

# Some aliases
alias ls="eza --icons=always"
alias la="eza -al --icons"

# Theme or prompt settings (e.g. powerlevel10k)
ZSH_THEME="powerlevel10k/powerlevel10k"
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh

More aliases for kubectl, terraform, etc. as desired

GitHub Actions (Optional)

If you want automatic builds when you push changes, create .github/workflows/ci.yml:

name: devops-toolbox

on:
  push:
    paths:
      - .github/workflows/ci.yml
      - Dockerfile
      - build/binaries.sh

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write

    steps:
    - name: Check out
      uses: actions/checkout@v2

    - name: Log in to Docker Hub
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    - name: Build and Publish
      run: |
        registry="your-dockerhub-username"
        sha=$(git rev-parse --short HEAD)
        primary_tag="${registry}/devops-toolbox:${sha}"

        docker build -t $primary_tag -f Dockerfile .
        docker push $primary_tag

        current_branch=$(git rev-parse --abbrev-ref HEAD)
        if [ "$current_branch" = "main" ]; then
          retention_tag="${registry}/devops-toolbox:rc-${sha}"
          docker tag $primary_tag $retention_tag
          docker push $retention_tag
        else
          retention_tag="${registry}/devops-toolbox:alpha-${sha}"
          docker tag $primary_tag $retention_tag
          docker push $retention_tag
        fi

When you push changes to GitHub, this workflow will build your Docker image and push it to your Docker Hub account.

Building and Testing Locally

Build:

make build

docker build -t your-dockerhub-username/devops-toolbox:latest .

Test:

make test

This checks each installed tool’s version.

Run:

make run

You should see a Zsh prompt with Terraform, Vault, AWS CLI, Helm, kubectl, etc. available.

Pushing to Docker Hub

Assuming you’re logged in to Docker Hub (docker login), run:

make push

This tags the image with v1.0.0 and latest, then pushes both to your Docker Hub repository.

Deploying in Kubernetes

If you want to spin up this container in a Kubernetes cluster:

make deploy-k8s

It creates a pod named toolbox that sleeps indefinitely. To open a shell inside that pod:

make exec-k8s

You’ll then see the same environment as the local container—handy for on-cluster troubleshooting or dev tasks.

Conclusion

By following these steps, you’ve built a DevOps Toolbox container from scratch. You learned how to:

  • Use a multi-stage Dockerfile to install and strip binaries.

  • Write a script (binaries.sh) for easy updates to Terraform, Vault, Helm, etc.

  • Set up a Zsh environment with helpful aliases.

  • Automate builds with Makefile and optionally GitHub Actions.

  • Push your image to Docker Hub and deploy it on Kubernetes.

This container simplifies DevOps tasks, letting you quickly spin up a consistent environment anywhere Docker or Kubernetes runs.

Related Posts