Containerization

The code we write is rarely run solely on our own laptops. We usually write software that will ultimately run on other machines. Docker gives us a way to package our app, and all its dependencies, into a 'shippable' container that will run on another machine.

Running containers

docker run hello-world

Once docker is installed in your shell you will have the docker command that you can use to build and run containers.

Building containers

With a Dockerfile you can define your own container. You will usually start with a 'base-image' like the Alpine container we have looked at already. You can then declare a series of scripts or commands that will build out your own docker image. We are going to dockerise an app. Use the example below or one of your own projects.

Download this ruby project

cd into the project folder and create a plain text file called Dockerfile with no extension.

FROM ruby:2.6.0

RUN apt-get update && apt-get install -y net-tools
RUN mkdir /app

WORKDIR /app

COPY Gemfile* .
RUN bundle install

COPY ./lib/ .

EXPOSE 4567

CMD ["ruby", "server.rb"]

In this file we use a base image called ruby:2.6.0. We RUN a command that installs a Unix package called 'net-tools'. We then create a new folder called /app at the root of the container. We then 'move' ourselves into that /app folder. From here we COPY from the local file system the lib folder into the working directory inside the container which will become /app/lib. We then make expose the containers' PORT 4567 to the 'host' system. Finally the container has a command that it will run when the container is started, which for us is ruby server.rb this will start the ruby web program noughts and crosses.

Ready to build your container?

docker build -t noughts-and-crosses .

This command is invoking docker to build a container and -t tag it with the label noughts-and-crosses from the Dockerfile that can be located in the current folder . the period or dot means 'here' in the current operating system, your laptop, the 'host'.

Ready to run your container?

docker run noughts-and-crosses

Easy. Now if you visit http://localhost:4567 you'll...
... not see anything 🙁 yet in your shell you should see the ruby process is running. Noughts and crosses is running, however it is sealed within that container that you just built. We did expose the PORT 4567, what we need to do is map that port to the same port on our 'host' computer. Stop the container with control+c.

docker run -p 4567:4567 noughts-and-crosses

That should enable us to use that webapp at http://localhost:4567. Some other things to experiment with. At the moment when we start a container it takes over our shell. We can run containers in 'detached' mode with the -d flag, which means we can still use our shell.

docker run -d -p 4567:4567 noughts-and-crosses

If we wanted to see what containers are running we can list them all with the command

docker ps

Can you see the CONTAINER ID? You can use that ID to address it. We can also interact and enter the running containers like we did with the Alpine image. You will have a unique ID so replace 1c784864b06e with your container ID.

docker exec -it 1c784864b06e sh

Here we invoke docker to exec execute a command in an -it interactive session for container 1c784864b06e and the command we want to execute is sh which will start a shell, and we'll be 'in' the container. Can you find out which flavour of Unix this base-image is built on (hint uname -all)?

Run your container on a server

Compose containers

You can run a container! Awesome. Now how about running 2, or 3 or 4 containers together. As this gets a bit unwieldy when you're just typing in shell commands, there is another tool called docker-compose that lets you define all the config for a set of containers in a file, then you can docker-compose up and start all the containers together.

This is great for example when you want to run a database, an API server and maybe a frontend project. We should start with just a pair of containers working together. You should have docker-compose installed along with docker for desktop.

Create a docker-compose.yaml file and lets define 2 containers.

version: "3.9"
    
services:
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress

In the folder where you created the docker-compose.yaml run docker-compose up. Oh wow a locally running wordpress http://localhost:8000 with zero effort, thank you very much that will do nicely. But before you go setting up your wordpress, there is one problem. As soon as the containers stop, the database, and all your wordpress config will disappear. To solve this problem docker will let us create 'volumes' that will persist data between the life cycle of containers coming up and going down. That way we can write data into our SQL database stop the containers, start them again, and retrieve our data. To create a couple of volumes we are going to add the following config to the 'services' object in our docker-compose.yaml file (volumes: should be at the root of the yaml, same level as version and services, so no spaces).

volumes:
  db_data: {}
  wordpress_data: {}

Then update the db: service to mount this external volume.

services:
  db:
    # other properties like image, environment etc
    volumes:
      - db_data:/var/lib/mysql
  wordpress:
    # other properties like image, ports, depends_on etc
    volumes:
      - wordpress_data:/var/www/html

Now when we start data will be persisted between container restarts. You can see the volumes docker has created by running the command:

docker volume ls

You can remove one of those volumes with docker volume rm CONTAINER ID. Whats really interesting is we can use this same method to 'mount' areas of our local file system into a container. So for example you can create a local folder named themes and download a wordpress theme. You can then mount that local folder into the container and use that theme in your wordpress.

  wordpress:
    # other properties like image, ports, depends_on etc
    volumes:
      - wordpress_data:/var/www/html
      - ./themes:/var/www/html/wp-content/themes

Assignment

If you tag your docker images with your docker-username/container-label and then push them you'll see your images online.

docker push your_docker_username/some_image_label