Hands on Dockerfile: Recipes of containers (Part 1)


docker containerization dockerfile

Docker has revolutionized the way we package, deploy, and manage software applications. Its containerization technology offers unprecedented portability, speed, and resource efficiency. If you’re in the fields of development, system administration, or DevOps, learning Docker is a valuable investment. In this article, we’ll take you on a hands-on journey with Docker.

Concepts you need to know

Before getting our hands dirty, it’s important to understand concepts about docker and containerization. Check out the following articles to learn about the concepts:

Prerequisites

In order to follow this tutorial, you need to meet the following requirements:

  • Familirility with basic linux CLI (Command Line Interface): You should be comfortable with navigating your terminal and running commands. Check out this article at https://bagor.tech/what-is-cli/ to learn about the CLI.

  • Have a system with Docker installed: If you haven’t already done so, you can install Docker on your system (Windows, macOS, or Linux). Follow the instructions at https://www.docker.com/get-started/.

  • Install git in your system: Install git from here https://git-scm.com/downloads/.

Docker CLI

Now that you have Docker installed, open your terminal (Mac/Linux) or command prompt (Windows) and execute the following command:

docker --help
Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Common Commands:
  run         Create and run a new container from an image
  exec        Execute a command in a running container
  ps          List containers
  build       Build an image from a Dockerfile
  pull        Download an image from a registry
  push        Upload an image to a registry
  images      List images
  login       Log in to a registry
  logout      Log out from a registry
  search      Search Docker Hub for images
  version     Show the Docker version information
  info        Display system-wide information

...

If you get the same response, it means that you’ve installed docker succesfully. While you are here, take some time to read docker commands documentation. In this tutorial, we will only use docker run, docker ps, docker build, docker exec, docker image and docker compose.

Note: response may vary depending on the Docker version you installed.

Your first docker container

For your first container, let’s start by running a simple “Hello, World” image from Docker Hub:

docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
478afc919002: Pull complete
Digest: sha256:a26bff933ddc26d5cdf7faa98b4ae1e3ec20c4985e6f87ac0973052224d24302
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
   (arm64v8)
3. The Docker daemon created a new container from that image which runs the
   executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
   to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

What happened:

  1. You asked Docker to run the entrypoint command of the “hello-world” image (docker run hello-world).
  2. Docker looked for the image on your computer and didn’t find it.
  3. Docker went online to Docker Hub and downloaded the “hello-world” image.
  4. Docker built a tiny virtual environment (a container) using the “hello-world” image.
  5. Docker ran a tiny program inside that container that printed the “Hello from Docker!” message.

Now, you can check whether you have downloaded the “hello-world” image on your computer by executing the following command:

docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
hello-world   latest    ee301c921b8a   11 months ago   9.14kB

Writing a container image (Dockerfile)

Prelude

  1. Clone this github repository

    git clone https://github.com/gstdl/rust-docker-demo-counter-api.git
    cd rust-docker-demo-counter-api
    
  2. Understanding the contents of the repository

    The code you just cloned contains a simple web server written in Rust. This web server’s purpose is to keep track of a counter value. It expects a configuration file (‘config.yaml’) to exist and reads a “username” from it (you can change the username if you want to). When executed, it will serve 3 endpoints:

    • / : Displays the current counter value.
    • /add: Increments the counter.
    • /reset: Resets the counter to 0.

    All 3 endpoint will also display a greeting message to the “username” provided in the configuration file.

    Can I test the app without installing Rust? Yes (click for more details)

    You can play around with the application by executing the command below.

    docker run -p 8000:8000 gstdl94/rust-docker-demo-counter-api
    

    This will pull and run a working container of the application from docker hub. Once you have the container up and running, you can visit http://localhost:8000/, http://localhost:8000/add, and http://localhost:8000/reset to see what the app does.

    Click cmd+c (macOS) or ctrl+c (Windows/Linux) to exit the app.

Writing the Dockerfile

  1. Write the Dockerfile

    To begin, let’s create a new file named Dockerfile. The content of the file will be as follows:

    # Base image
    FROM rust:1.77.2-alpine3.18
    # Set working directory
    WORKDIR /app
    # Copy application code and dependencies
    COPY . .
    # Install OS dependencies
    RUN apk add --no-cache musl-dev
    # Build the application
    RUN cargo install --path .
    # Set environment variable ROCKET_ADDRESS
    ENV ROCKET_ADDRESS 0.0.0.0
    # Run the application
    CMD [ "/usr/local/cargo/bin/rust-rocket-counter-api" ]
    
    Detailed explanation (click to expand)
    1. Selecting a base image
    FROM rust:1.77.2-alpine3.18
    

    Every Dockerfile starts with a base image and every instructions afterwards extends the base image. The format of a base image is typically <image>:<tag> where in this tutorial the image is rust and the tag is 1.77.2-alpine3.18 indicating that we are building on top of rust version 1.77.2 on alpine version 3.18. You can search for base images in Docker Hub

    1. Extending the base image

    After selecting the base image, we continue by extending it using Docker instructions. The instructions used to extend the rust base image for this application starts with defining the working directory.

    WORKDIR /app
    

    After that, we continue by copying files and folders from our current working directory to the container’s working directory.

    COPY . .
    

    Next, we install required the application os dependencies and build the application binary.

    RUN apk add --no-cache musl-dev
    RUN cargo install --path .
    

    In this step, we set environment variables required for the application.

    ENV ROCKET_ADDRESS 0.0.0.0
    

    Finally, we execute the final command that will be executed once we start the application.

    CMD [ "/usr/local/cargo/bin/rust-rocket-counter-api" ]
    
  2. Building the Image:

    Build an image named my-counter:

    docker build -t my-counter:latest .
    
  3. Running from your Image:

    Run this command to run the my-counter app:

    docker run -p 8000:8000 my-counter
    
    Using config file: config.yaml
    thread 'main' panicked at src/main.rs:41:56:
    Can't open config.yaml: Os { code: 2, kind: NotFound, message: "No such file or directory" }
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    

    Running my-counter app will result in the error message above. The error message is telling us that the application is expecting a config file named config.yaml and it is not found.

    We can diagnose the file existenct by executing the command below. This will list all the files in the container’s working directory by executing the ls command.

    docker run my-counter ls
    
    Cargo.toml
    Dockerfile
    LICENSE
    src
    target
    

    Notice that config.yaml is not listed in the working directory, but it is available in the application github repository. We can map the config file from our computer to the container by using the -v (or --volume) flag in docker run.

    docker run -v ./config.yaml:/app/config.yaml my-counter ls
    
    Volume mapping explanation (click to expand)
    • ./config.yaml is where the config file is located in our computer.
    • /app/config.yaml is where we expect the config file is located in the container.

    You can map the file using -v flag or remove config.yaml from .dockerignore file.

    Cargo.toml
    Dockerfile
    LICENSE
    config.yaml
    src
    target
    

    Finally, the full command to run the app is:

    docker run -p 8000:8000 -v ./config.yaml:/app/config.yaml  my-counter
    
    Using config file: config.yaml
    Rocket has launched from http://0.0.0.0:8000
    

    Visit http://localhost:8000 to play around with the app (this assumes your app listens on port 8000).

Final remarks

Congratulations, you have succefully created a Dockefile. If you followed along, not only have you written your first Dockerfile, but you’ve also did a little bit of debugging to create your Dockefile.

Next Steps

  • Explore Docker Hub: Find and use images for databases (MySQL, PostgreSQL), web servers (Nginx, Apache), and other commonly used software.
  • Understanding Docker Volumes: Persist data generated by your containers.
  • Trying Docker Compose: Manage multi-container applications with ease.
  • Dockerfile best practices: Figure out ways to reduce docker image size and build time.

Github repository related to this post

References