Development Environment

Homelab

thumbnail

This note describes my development environment featuring VS Code and a permanent docker container.

Rather than deal with handling different environments depending on the computer I was using to develop on, I decided to migrate my development environment to my home lab. This also gave me some good practice setting up and running a service that doesn’t need to have high-availability like some of the services I plan to run in the future.

Creating the Dockerfile

To begin, I need to define what my development environment looks like. Docker describes containers using a Dockerfile, which is a text file that contains statements describing how to build a container. There are many Dockerfiles available on the internet to use as inspiration.

My development environment uses Debian Unstable with a few tools. The complete Dockerfile can be seen below, but I’ll describe the major points first. I begin with a FROM statement to grab the latest Debian Unstable Docker image, add myself as a maintainer, and then install packages that I want to use. I also want to use Node, but I manage that via NVM instead of apt. Because I’m using NVM, I need to add the Node binary to my path.

FROM debian:unstable-slim

LABEL maintainer="Nicholas Nooney <nicholasnooney@gmail.com>"

RUN apt update && apt install -y -q --no-install-recommends \
  apt-transport-https \
  build-essential \
  ca-certificates \
  curl \
  git \
  golang \
  gpg \
  hugo \
  libssl-dev \
  ssh \
  wget

# Install NVM to manage node
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 16.3.0

WORKDIR ${NVM_DIR}

RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh \
  | bash \
  && . ${NVM_DIR}/nvm.sh \
  && nvm install ${NODE_VERSION} \
  && nvm alias default ${NODE_VERSION} \
  && nvm use default

ENV PATH ${NVM_DIR}/versions/node/v${NODE_VERSION}/bin:${PATH}

With this Dockerfile, I can build an image of my development environment. I can upload the image to Docker Hub and then use Docker to instantiate a container that runs on my home lab. As with the other services on my home lab, the development environment is described with a docker-compose.yml file.

version: "3.8"

networks:
  traefik-public:
    external: true

volumes:
  developer:
    driver: local-persist
    driver_opts:
      mountpoint: /mnt/lab/developer

services:
  devbox:
    image: "nicholasnooney/devbox"
    hostname: "devbox"
    networks:
      - traefik-public
    volumes:
      - developer:/developer
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.devbox.rule=Host(`${FQDN}`)"
      - "traefik.http.routers.devbox.entrypoints=websecure"
      - "traefik.http.routers.devbox.tls=true"
      - "traefik.http.services.devbox.loadbalancer.server.port=80"
    tty: true
    stdin_open: true

With a single command I can connect to my development environment running on my home lab!

docker-compose config | docker -H ssh://docker.nooney.casa stack deploy -c - devbox

Automation

I’d prefer not to have to worry about updates for my development environment. I know that this may result in the occasional breakage, but I think it’s an acceptable trade off. In order to ensure my image is up to date, I need to rebuild the image and then update the container on my home lab.

Rebuilding the Image

I host the Dockerfile that defines my environment in a git repository on GitHub. Using a GitHub action, I rebuild the image when the following events occur:

  • I push an update on the main branch to GitHub.
  • I manually trigger the rebuild.
  • Every 24 hours.

They trigger the following workflow:

name: CI

on:
  push:
    branches: [ main ]
  schedule:
    - cron: 0 0 * * *
  workflow_dispatch:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v1
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      -
        name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          pull: true
          push: true
          tags: nicholasnooney/devbox:latest
      -
        name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}

With this GitHub action, the image is updated and pushed to Docker Hub daily.

Updating the Container

Currently, I manually update the development container running on my home lab. In the future, I’ll explore options to make automatic updates possible.