Docker - Dockerfiles

  • Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
  • Dockerfiles are amazing because they allow us to define the environment our applications are meant to use in code. We can even commit the Dockerfiles to Git alongside our source code.

DevOps

  • Dockerfiles are a key component of DevOps. They allow developers to define the environment their applications run in, and they allow operations to define the environment that applications run in. This is a key part of the DevOps movement, where developers and operations work together to deliver better software faster.

GitOps / Infrastructure as Code(IaC)

  • Dockerfiles are a key part of GitOps and Infrastructure as Code (IaC). They allow us to define the environment our applications run in as code. This means we can commit our Dockerfiles to Git alongside our source code. This is a key part of GitOps and IaC, where we define our infrastructure as code and commit it to Git.

Creating a Dockerfile

  • The Dockerfile is a simple text file that contains a list of commands that the Docker client calls while creating an image. It’s a simple way to automate the image creation process. The best part is that the commands you write in a Dockerfile are almost identical to their equivalent Linux commands.

Example Dockerfile:

# This is a comment
 
# Use a lightweight debian os
# as the base image
FROM debian:stable-slim
 
# execute the 'echo "hello world"'
# command when the container runs
CMD ["echo", "hello world"]

Building an Image

  • To build an image from a Dockerfile, use the docker build command. The docker build command builds an image from a Dockerfile and a context. A context is the set of files located in the specified PATH or URL. The build process can refer to any of the files in the context. For example, your build can use a COPY instruction to reference a file in the context.
docker build -t my-image .
  • The -t flag lets you tag your image so it’s easier to find later using the docker images command.
  • The . at the end of the docker build command is the build context (the files COPY can see). Docker looks for the Dockerfile inside the context by default; -f path/to/Dockerfile overrides that.
  • The docker build command will build the image and tag it with the name my-image.

Running a Container

  • To run a container from an image, use the docker run command. The docker run command creates a container from an image and runs it using the specified command.
docker run my-image
  • The docker run command will run the container and execute the command specified in the Dockerfile.
  • In this case, the command is echo "hello world", so the container will run and output hello world.

Dockerizing a Server

For Reference:

1. Create the Dockerfile

The first step to running your Go server in Docker is to create a Dockerfile in the root directory of your project.

  • Base Image: Start with a lightweight Debian Linux image for the container environment:

    FROM debian:stable-slim

2. Add the Go Server Executable

The next step is to copy the Go server executable into the container.

  • COPY Command: This copies the Go binary from your host machine into the Docker image:

    COPY goserver /bin/goserver
    • goserver: Replace this with the name of your actual Go binary if it’s different.

    • /bin/goserver: This is where the Go binary will be placed inside the Docker container.

    Note: You can use COPY instead of ADD because you don’t need the extra functionality that ADD offers (e.g., extracting archives).


3. Run the Go Server

To ensure the Go server starts when the container is run, add a CMD instruction at the end of your Dockerfile:

  • CMD Command: This starts the Go server process inside the container:

    CMD ["/bin/goserver"]

    This tells Docker to execute /bin/goserver when the container is started.


4. Build the Docker Image

After setting up your Dockerfile, you need to build the Docker image.

  • Build the image:

    docker build . -t goserver:latest
    • .: Specifies the current directory as the build context.
    • -t goserver:latest: Tags the image as goserver:latest.

5. Run the Docker Container

Run the container while forwarding the necessary ports to access the server from your host system:

  • Run the container:

    docker run -p 8080:8080 goserver
    • -p 8080:8080: Maps port 8080 on the host to port 8080 inside the container, allowing access to the Go server via localhost:8080.

6. Troubleshooting: Exec Format Error

If you encounter an exec format error, this typically means the binary was compiled for a different operating system or architecture (e.g., MacOS or Windows, but the container runs on Linux).

  • Solution: Recompile the Go server binary for Linux before building the Docker image:

    GOOS=linux GOARCH=amd64 go build
    • GOOS=linux: Ensures the binary is built for a Linux environment.
    • GOARCH=amd64: Specifies the architecture (64-bit).

After recompiling, rebuild the Docker image and run the container again.


Summary

  1. Create Dockerfile with a lightweight base image (Debian).
  2. Copy the Go binary into the container using the COPY command.
  3. Run the server on container startup using the CMD command.
  4. Build and run the Docker image, ensuring to map the correct ports.
  5. Troubleshoot exec format errors by compiling the Go binary for Linux.

Configuring and Dockerizing the Go Server with Environment Variables

1. Why Dockerize a Simple Service?

Dockerizing a simple service offers several benefits:

  • Cross-platform compatibility: Anyone with Docker can run your image, regardless of their OS.
  • Cloud deployment: The image can be deployed on cloud services that support Docker containers (e.g., AWS, Google Cloud, Kubernetes).
  • Encapsulation: If the service has dependencies (e.g., a Python interpreter), Docker can bundle these dependencies, eliminating the need for users to configure them.

2. Configuring the Server with Environment Variables

To make your Go server more configurable, it’s important to use environment variables. The server should bind to a port specified through an environment variable, which provides flexibility in different environments (development, testing, production).

  • Using os.Getenv() in Go: Modify your Go server to bind to the port specified by an environment variable (PORT):

    package main
     
    import (
        "fmt"
        "net/http"
        "os"
    )
     
    func main() {
        // Get the port from the environment variable
        port := os.Getenv("PORT")
        if port == "" {
            port = "8080" // Fallback to default port if not set
        }
     
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Hello from Go server!")
        })
     
        fmt.Printf("Server is running on port %s\n", port)
        http.ListenAndServe(":" + port, nil)
    }

3. Test the Server Locally

Before Dockerizing, you should test the environment variable configuration:

  1. Set the environment variable:

    export PORT=8080
  2. Verify the variable is set:

    echo $PORT
  3. Run the Go server: It should bind to the port specified by the PORT environment variable.


4. Update the Dockerfile to Support the PORT Variable

To make the Docker image more configurable, you can set a default PORT environment variable within the Dockerfile:

  1. Update the Dockerfile:

    FROM debian:stable-slim
     
    # Copy the Go server binary to the image
    COPY goserver /bin/goserver
     
    # Set the PORT environment variable
    ENV PORT=8080
     
    # Run the Go server
    CMD ["/bin/goserver"]
    • ENV PORT=8080: Sets the default port to 8080. ENV values are baked into the image and exist at runtime in the container, which is why you can override this when running it (-e PORT=9000). The build-time-only variable is ARG.
  2. Build the Docker image:

    docker build . -t goserver:latest

5. Run the Docker Container

When running the Docker container, you can specify a different port if needed using the -e flag:

  • Run the container:

    docker run -p 8080:8080 goserver

    To override the PORT environment variable:

    docker run -e PORT=9000 -p 9000:9000 goserver

6. Why Use Environment Variables?

  • Flexibility: Environment variables allow you to change configuration (e.g., ports, database URLs) without modifying your code.
  • Separation of concerns: You can manage local development and production configurations easily by swapping environment variables.

Summary

  1. Modify your Go server to read the port from the PORT environment variable.
  2. Test the server locally by setting the PORT variable in your shell.
  3. Update the Dockerfile to include the ENV PORT=8080 directive.
  4. Build and run the Docker container, using environment variables to configure ports flexibly.

Simple Example: Dockerizing a Python Script

Let’s go through a simple example where we’ll Dockerize a basic Python script that prints “Hello, World!” and counts the number of characters in a string.


1. Create the Project Directory

  1. Set up the project folder:

    • Create a folder called simple-python-app.
    • Inside this folder, create a Python file called main.py.
  2. Write the Python script (main.py):

def main():
    message = "Hello, World!"
    num_chars = len(message)
    
    print(f"Message: {message}")
    print(f"Number of characters: {num_chars}")
 
if __name__ == "__main__":
    main()

This simple Python program prints a message and counts how many characters are in it.


2. Create the Dockerfile

In the root of your simple-python-app folder, create a file called Dockerfile. This file tells Docker how to package your Python application.

Dockerfile:

# Use the official Python image from Docker Hub
FROM python:3.10-slim
 
# Set the working directory inside the container
WORKDIR /app
 
# Copy the current directory contents into the container at /app
COPY . /app
 
# Command to run the Python script when the container starts
CMD ["python", "main.py"]

3. Build the Docker Image

Now, you need to build your Docker image using the Dockerfile you just created.

  1. Open a terminal or command prompt.
  2. Navigate to the simple-python-app directory where your Dockerfile is located.
  3. Run the following command to build the Docker image:
docker build -t simple-python-app .

This will build the Docker image and tag it as simple-python-app.


4. Run the Docker Container

After building the image, you can run the container with this command:

docker run simple-python-app

You should see the following output:

Message: Hello, World!
Number of characters: 13

Summary

  • Project Directory: Created a folder containing a simple Python script (main.py) and a Dockerfile.
  • Dockerfile: Used to define the environment for running the Python app in a Docker container.
  • Build & Run: Built the Docker image and ran the container, which executed the Python script and printed the results.

Notes on Dockerizing Python for the Bookbot Example

Steps to Dockerize a Python Application

  1. Base Image:

    • Debian Slim is chosen as the base image because it’s lightweight and reliable for building custom environments.
    • This base image doesn’t come with Python installed, so we need to install it manually.
  2. Copy Application Code:

    • The COPY command is used to add the Python script (main.py) and its dependencies (e.g., text files in the books folder) into the image.
    • The COPY command ensures that the application code and necessary data are part of the image, making it portable.
  3. Updating the System:

    • apt-get update and apt-get upgrade -y update the package lists and upgrade installed packages. Keep update in the same RUN as install: as a separate layer it gets cached and the package lists go stale.
    • The slow RUN steps come before the COPY of our code. Every code change invalidates the cache from the COPY onwards, so copying first would rebuild Python every time.
  4. Install Build Tools:

    • Installing the necessary build tools like build-essential, zlib1g-dev, libssl-dev, etc., is required to compile Python from source.
  5. Download and Install Python:

    • Download Python Source Code: wget https://www.python.org/ftp/python/3.10.8/Python-3.10.8.tgz fetches the Python source code from the official Python repository.
    • Extract Python: tar -xf Python-3.10.*.tgz extracts the downloaded file.
    • Build and Install Python: This part compiles Python and installs it using make altinstall. altinstall is used instead of install to prevent overwriting system Python versions.
  6. Run the Python Script:

    • The CMD command at the end runs the Python script using the newly installed Python version. It specifies python3.10, which was built and installed in the previous steps.

Full Dockerfile Breakdown

# Build from a slim Debian/Linux image
FROM debian:stable-slim
 
# Update apt (get the latest package lists) and install build tooling + dependencies
# required for building Python, in ONE layer so the package lists are never stale
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev \
    libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget libbz2-dev && \
    rm -rf /var/lib/apt/lists/*
 
# Download Python interpreter code and unpack it
RUN wget https://www.python.org/ftp/python/3.10.8/Python-3.10.8.tgz
RUN tar -xf Python-3.10.*.tgz
 
# Build the Python interpreter and install it
RUN cd Python-3.10.8 && ./configure --enable-optimizations && make && make altinstall
 
# Copy our data dependencies and code LAST, so a code change doesn't
# invalidate the cached (slow) Python build above
COPY books/ books/
COPY main.py main.py
 
# Run the Python script
CMD ["python3.10", "main.py"]

Build and Run the Docker Image

  1. Build the Docker Image:

    docker build -t bookbot .
    • The -t bookbot flag tags the image with the name “bookbot.”
    • The . is the build context (current directory); the Dockerfile is looked up there by default (-f overrides it).
  2. Run the Docker Container:

    docker run bookbot
    • This command starts a container from the bookbot image and runs the Python script inside the container.

Key Points

  • Docker provides an isolated environment, allowing you to bundle your Python application along with its runtime (Python 3.10 in this case) and dependencies.
  • Using a lightweight base image like Debian slim helps keep the container size small, but you still need to manually install the Python runtime.
  • The CMD instruction ensures that the Python script runs automatically when the container starts.
  • The build tools stay in the final image. For a smaller image, use a multi-stage build or just start FROM python:3.10-slim.