I have heard many things about containerization and have run into many situations where I thought “wow it would be so much easier if I could ‘copy and paste’ everything remotely related to this project onto this other computer and have it run exactly the same.” Yeah, I think it’s time to learn about Docker.

3 Fundamental Elements

Docker functions in a very comparable way to virtual machines.

The Docker File (DNA)

At a glance: Code that tells Docker how to build an image
How it manifests: It’s a file in your project called Dockerfile with a series of commands, notably FROM to specify the base image, and RUN to call terminal commands during the build process such as installing dependencies, and CMD which specifies a default command to run when a container is started from the image.

Create the image file using docker build -t myapp:latest ./. The -t option specifies a tag formatted as name:tag (tag is latest by default). ./ is the path to the dockerfile.

The Image

An immutable snapshot of your software (i.e. read only), along with all of its dependencies down to the operating system level. Can be used to spin up multiple containers.

The Container

Your actual software running in the real world (i.e. an instance of the image).

Bring an image to life by using docker run myapp.

Container

Some commands:

  • docker run
  • docker ps: view running containers, use -a to show all
  • docker stop <id>: send stop signal (SIGTERM)
  • docker kill <id>: send kill signal (SIGKILL) when unresponsive
  • docker rm <id>: remove the container

Run a container from an image using docker run.

docker run -d -p hostport:containerport namespace/name:tag
docker run -d -p 8965:80 docker/getting-started:latest
docker run --name postgresql -p 5432:5432 -e POSTGRES_PASSWORD=mypassword -d bitnami/postgresql:latest
  • -d: run in detached mode, container runs in background and terminal can still be used
  • -p: exposes a port in the container and maps it to a port on the host machine
  • -e: adds an environment variable with the specified value
  • --name: optionally assign a name to the container
  • namespace/name:tag the last argument is the path to the image on Docker hub which will be downloaded, a tag can optionally be added to specify the version (otherwise latest)

Volumes

Changes made in a container (like modifying files) will not affect the image, when a container is restarted it will go back to its original state. Persistent state can be utilized through the use of storage volumes—a file system that lives outside of the container.

Create one using docker volume create <name>. Make sure it worked docker volume ls. Get more info using docker volume inspect <name>. When using docker run use the -v ghost-vol:/var/lib/ghost option to mount the volume.

Exec

Usually when a container is deployed, you just let it do its thing. However, you can still interface with it similar to ssh by using docker exec <id> <command>.

In the event you want a live shell rather than one-off commands, we can use -it and run the shell at /bin/sh like so docker exec -it <id> /bin/sh.

Dockerfiles

“Dockerize” a project by adding a Dockerfile file to the root of the project which can be built into an image. First, we’ll want to specify a base image using the FROM keyword—this is required. Then we’ll use some combination of the following commands (not comprehensive):

  • COPY <src> <dst>: Copy files/folders from host machine into the docker image
  • RUN <cmd> (&& <cmd> ...): Executes commands at build time (chain using &&)
  • CMD: Executes command at runtime, i.e. when the container starts (there can only be one CMD, last one takes precedence)
  • EXPOSE <port>: Declares what ports the container will listen on at runtime (does not publish a port on the host machine, however)

The (usual) Dockerfile process

  1. Create Dockerfile
  2. Add FROM command to specify base image
    1. IF PRECOMPILED: Copy files with COPY into the image
    2. IF DEPENDENCIES NEEDED: Use RUN commands to set up the image as if it were a fresh machine to run your server, and COPY anything that’s needed
  3. Run server with CMD
  4. Build the image with docker build . -t myserver:latest
  5. Run the image with docker run -p <port>:<port> myserver

The (sort of) deployment process

  1. Write code → Commit to Git → Push to GitHub → PR into main → Approve PR → Merge PR
  2. Upon merge, an automated script (like GitHub actions) builds the code
  3. The automated script builds a new docker image
  4. The automated script pushes the new image to a registry
  5. The server that runs the containers (like a Kubernetes cluster) is informed about the new version and pulls it, shutting down the old container and spinning up a new one.

Tags

Not much to say here, generally speaking a tag will always be either a semantic version like 0.1.1 or latest. Usually we’ll always build with a semantic versioning tag but also push to latest which will overwrite the previous latest. Like so:

docker build -t company/product:5.4.6 -t company/product:latest .
docker push company/product --all-tags

What’s the difference between containerization and virtualization?

TODO