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 buildcommand. Thedocker buildcommand 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 aCOPYinstruction to reference a file in the context.
docker build -t my-image .- The
-tflag lets you tag your image so it’s easier to find later using thedocker imagescommand. - The
.at the end of thedocker buildcommand is the build context (the filesCOPYcan see). Docker looks for the Dockerfile inside the context by default;-f path/to/Dockerfileoverrides that. - The
docker buildcommand will build the image and tag it with the namemy-image.
Running a Container
- To run a container from an image, use the
docker runcommand. Thedocker runcommand creates a container from an image and runs it using the specified command.
docker run my-image- The
docker runcommand 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 outputhello 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
COPYinstead ofADDbecause you don’t need the extra functionality thatADDoffers (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/goserverwhen 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 asgoserver: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 vialocalhost: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 buildGOOS=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
- Create Dockerfile with a lightweight base image (Debian).
- Copy the Go binary into the container using the
COPYcommand. - Run the server on container startup using the
CMDcommand. - Build and run the Docker image, ensuring to map the correct ports.
- 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:
-
Set the environment variable:
export PORT=8080 -
Verify the variable is set:
echo $PORT -
Run the Go server: It should bind to the port specified by the
PORTenvironment 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:
-
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.ENVvalues 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 isARG.
-
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 goserverTo override the
PORTenvironment 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
- Modify your Go server to read the port from the
PORTenvironment variable. - Test the server locally by setting the
PORTvariable in your shell. - Update the Dockerfile to include the
ENV PORT=8080directive. - 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
-
Set up the project folder:
- Create a folder called
simple-python-app. - Inside this folder, create a Python file called
main.py.
- Create a folder called
-
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.
- Open a terminal or command prompt.
- Navigate to the
simple-python-appdirectory where yourDockerfileis located. - 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-appYou 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
-
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.
-
Copy Application Code:
- The
COPYcommand is used to add the Python script (main.py) and its dependencies (e.g., text files in thebooksfolder) into the image. - The
COPYcommand ensures that the application code and necessary data are part of the image, making it portable.
- The
-
Updating the System:
apt-get updateandapt-get upgrade -yupdate the package lists and upgrade installed packages. Keepupdatein the sameRUNasinstall: as a separate layer it gets cached and the package lists go stale.- The slow
RUNsteps come before theCOPYof our code. Every code change invalidates the cache from theCOPYonwards, so copying first would rebuild Python every time.
-
Install Build Tools:
- Installing the necessary build tools like
build-essential,zlib1g-dev,libssl-dev, etc., is required to compile Python from source.
- Installing the necessary build tools like
-
Download and Install Python:
- Download Python Source Code:
wget https://www.python.org/ftp/python/3.10.8/Python-3.10.8.tgzfetches the Python source code from the official Python repository. - Extract Python:
tar -xf Python-3.10.*.tgzextracts the downloaded file. - Build and Install Python: This part compiles Python and installs it using
make altinstall.altinstallis used instead ofinstallto prevent overwriting system Python versions.
- Download Python Source Code:
-
Run the Python Script:
- The
CMDcommand at the end runs the Python script using the newly installed Python version. It specifiespython3.10, which was built and installed in the previous steps.
- The
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
-
Build the Docker Image:
docker build -t bookbot .- The
-t bookbotflag tags the image with the name “bookbot.” - The
.is the build context (current directory); theDockerfileis looked up there by default (-foverrides it).
- The
-
Run the Docker Container:
docker run bookbot- This command starts a container from the
bookbotimage and runs the Python script inside the container.
- This command starts a container from the
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
CMDinstruction 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.