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.