Containerized project environments
Table of Contents
: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 themv
andrm
commands) - On Linux all the files generated with the
gem
command inside the container will be owned byroot
on the host machine. Change this by runningsudo 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
- 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 abundle 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 arun
command (as opposed todocker-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
- The global
docker-compose.yml
may have a different configuration that the example. - See https://evilmartians.com/chronicles/reusable-development-containers-with-docker-compose-and-dip
# 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