Alex's Slip-box

These are my org-mode notes in sort of Zettelkasten style

Containerized project environments

:ID: 30402D2F-51E2-4612-BDA1-8CAA741F349F

Instead of messing about with version managers and local environment setup when writing code, these days I prefer to just use docker and skip that hassle. The trade off is the little bit of overhead setting up a Dockerfile and docker-compose.yml. But this is mostly boiler plate with some minor customization for what is needed for my chosen language and project needs (ie, adding system dependencies like pandoc or something), so it’s not that big of a deal.

# The super quick and dirty

Just run this terminal command followed by the image to build a container from and the entrypoint. Maps the current host directory to /app inside the container. Example:

docker run -it --rm -v "$PWD":/app -w "/app" ruby:latest bash

# Exclude a directory

Working on a node project, probably don’t want the local node_modules directory

docker run -it --rm -v "$PWD":/app -v /app/node_modules node:latest bash

# The quick and dirty

Just need a bare environment for messing around? Copy this to a Dockerfile, pick an image, build and run. (see build and run command comments below)

FROM ruby:3.0.0

WORKDIR /app

COPY . .

CMD ["/bin/bash"]

# build command with a tag
# docker build -t lasagna .

# run command with a volume
# docker run -it --rm -v "$PWD":/app lasagna

# A Ruby Gem

The example here uses Ruby Gem project, but can easily be adapted for any language. The additional thing is to install dependencies as part of the container build.

# Just a Dockerfile

We could just use a Dockerfile alone (ie, not use Docker Compose at all). That would go something like this:

# Setup

Create an empty project directory and add a Dockerfile.

# The Dockerfile

Add this to the Dockerfile.

FROM ruby:3.2

# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1

WORKDIR /app

COPY Gemfile Gemfile.lock *.gemspec ./
RUN bundle install

COPY . .

CMD ["/bin/bash"]

# Scaffold the project

I need to generate all the boilerplate files, install the dependencies and generate the Gemfile.lock. This can be done by building a container on the fly from the same image specified in the Dockerfile (eg, ruby:3.0.0), mapping a volume from local project directory to the working directory (eg, /app) and passing the commands to docker run

docker run --rm -v "$PWD":/app -w /app ruby:3.2 bundle gem my_proj && mv my_proj/* ./ && rm -rf my_proj
  • This runs gem my_project to build the project files.
  • It moves the project files up to the working directory (You might need to use sudo on the mv and rm commands)
  • On Linux all the files generated with the gem command inside the container will be owned by root on the host machine. Change this by running sudo chown "$USER":"$USER" ./ -R in the project directory to take ownership.
  • Fill out the .gemspec

    Before the dependencies can be installed, a valid .gemspec is required. The bare minimum I need is:

    • authors
    • email
    • summary
    • description
    • version (change this to a string and remove the require_relative)
      • I mean it’s kinda silly to declare the version in a ruby file, isn’t it?

    The remaining meta data can be deleted or commented out.

  • Install dependencies and generate the Gemfile.lock

    With the .gemspec filled out, I can install the dependencies. This is the same as before, but just do a bundle install.

    docker run --rm -v "$PWD":/app -w /app ruby:3.0.0 bundle install
    

# Build the container and Write code

  • Build the container from the image and tag it. docker build -t my-proj .
  • Run the container: docker run -it --rm -v "$PWD":/app my-proj
    • The entry point drops me into a bash prompt inside the container.
  • Write code.

# Add Docker Compose (optional)

Docker Compose is totally optional, but there’s some advantages:

  • The compose file could be a global file that specifies different environments.
  • Easier to create volumes and using PWD means the volume is always bound to the the working dir from which you run docker compose.

# Add docker-compose.yml

This builds off the Dockerfile and the setup above.

version: "3.6"

services:
  ruby:
    # this is the same as the CMD in Dockerfile (this overrides it, actually)
    command: /bin/bash
    build: .
    volumes:
      - ${PWD}:/app:cached # filesyncing volume so don't have to rebuild.
    ports:
      - "12345:12345" # Expose a port (ie, serivce-ports) to the host if needed
    environment:
      # Add environment variables
      LANG: C.UTF-8
    working_dir: /app

To run it:

docker-compose run --rm --service-ports ruby
  • The --service-ports is to expose the ports on a run command (as opposed to docker-compose up which would be used when doing something like running a server and the ports would be exposed normally)

# Global docker-compose.yml

…or if using a global docker-compose.yml

docker-compose -f ~/path/to/global/docker-compose.yml run --rm ruby

# Other Examples

# VueJS project

The Dockerfile

FROM node:16.2-alpine3.11

WORKDIR /app

COPY package.json package-lock.json
RUN npm install

COPY . .

CMD ["/bin/sh"]

Open a shell prompt and setup the project:

docker run --rm -it -v "$PWD":/app -w /app node:16.2-alpine3.11 sh
  • npm install -g @vue/cli
  • vue create my-project
  • mv my-project/* my-project/.gitignore ./
  • rmdir my-project

The docker-compose.yml

version: "3.6"

services:
  app:
    command: npm run serve
    build: .
    volumes:
      - ${PWD}:/app:cached
    ports:
      - "8080:8080"
    environment:
      LANG: C.UTF-8
    working_dir: /app

Run it with docker-compose up

# Simple Apache Web Server

If you want to serve the current directory on localhost:8080

docker run --rm -p 8080:80 --name="myapache" -v "$PWD":/usr/local/apache2/htdocs/ httpd

# Root user

If you need to get root access to a container (eg, to install dependencies):

docker exec -u 0 -it my_container bash

Search Results