Day 2 of #40daysofkubernetes challenge
First containerized app up and running

As part of the 40 days Kubernetes challenge Day 2, I have containerized a FastAPI based application with both a Dockerfile created by me manually and one created by docker init. In this blog I am going to share my learnings from it.
Manual Dockerfile creation
# Getting the ready-made Alpine image which already has Python 3.12 in it
FROM python:3.12-alpine
# Creating a working directory for our application.
# After this our control will get into this app folder
WORKDIR /app
# Copying all the things from the current local directory to
# the container current directory that is app directory
COPY . .
# Installing all the dependencies in the container
RUN pip install -r requirements.txt
# Starting the application inside the container.
# This will run only with command docker run
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Expose the port that the application listens on.
EXPOSE 8000
This is the Dockerfile I created myself for running my application.
FROM - This keyword is used to fetch the existing images from Docker Hub, and here we are fetching an Alpine image which has Python built into it.
WORKDIR - This creates a working directory for the application, under which everything else will be placed. Without this, the commands would run from the image's default directory, which is usually /. So COPY . . would dump our application into the root filesystem next to /bin and /etc, and pip install -r requirements.txt would look for the file at /requirements.txt. It still works, but it is messy.
COPY - This command is used to get all the data from the current local directory (first .) and put it into the container's current directory (second .), in this case /app.
RUN - This command runs any command inside the container while the image is being built. Here we are using it to install all the dependencies listed in requirements.txt.
CMD - This is the command that starts our application when the container runs.
EXPOSE - This documents that our application listens on port 8000. It does not actually open the port. The -p flag in docker run is what publishes it to the outside world.
The docker init command
As mentioned previously, I wrote my Dockerfile myself, but there was also a line in the task saying "explore docker init command". I did not know what it was, so I tried it without reading any documentation and without knowing what it would do.
It prompted a few questions such as the programming language and the version required, and after successfully answering all of them a Dockerfile was created for me which was quite different from what I made.
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12.8
FROM python:${PYTHON_VERSION}-slim as base
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Create a non-privileged user that the app will run under.
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
# Download dependencies as a separate step to take advantage of Docker's caching.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt
# Switch to the non-privileged user to run the application.
USER appuser
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD uvicorn 'main:app' --host=0.0.0.0 --port=8000
ARG PYTHON_VERSION - A variable for the version instead of hardcoding it. Changing the Python version is now a one line edit.
slim instead of alpine - Alpine uses musl instead of glibc. Python packages ship prebuilt wheels made for glibc, so on Alpine pip often has to compile from source. Slim avoids that.
PYTHONDONTWRITEBYTECODE - Stops Python from writing .pyc cache files. They are useless in a container that runs once.
PYTHONUNBUFFERED - Python buffers its output by default, so if the app crashes the logs can be lost before they are flushed. This forces the output out immediately, which is why docker logs actually shows the error.
adduser and USER - By default a container runs as root. If someone exploits the app, they are root inside the container. This creates a user with no password, no home directory and no login shell, and runs the app as that user instead.
--mount=type=cache - pip keeps its downloads in a cache directory. Normally each build starts empty and re-downloads everything. This keeps the cache between builds.
--mount=type=bind - This makes requirements.txt readable during the build without copying it into the image. So pip can read it, but no extra layer is created and the file is not left behind.
After seeing the difference between my Dockerfile and the one from docker init, I understood that my Dockerfile was missing a few things, and in the next blog I will go through a production grade Dockerfile written by me.
Commands executed to get the container up and running
docker build -t task2 .
Builds the image from the Dockerfile. -t gives it a name, otherwise the image would only have an ID. The . at the end is the build context - the folder that gets sent to the daemon, and the folder COPY copies from. This is also where Docker looks for the Dockerfile.
docker run -dp 8000:8000 task2
Creates a container from the image and starts it. -d runs it in the background, -p 8000:8000 maps port 8000 on my machine to port 8000 inside the container.
docker ps
Lists the running containers. If the container crashed it will not show here.
docker ps -a
Lists all containers including the stopped ones, with the exit code. This is how I found my container had exited instead of running.
docker logs <container id>
Shows whatever the application printed. This is where the actual error is when a container exits.
docker exec -it <container id> sh
Opens a shell inside a running container to look around. -i keeps the input open and -t gives a terminal, without both the shell exits immediately.
docker stop <container id>
Stops a running container.
docker tag task2:latest saran2003g/40daysofkubernetes:day2
Adds a second name to the same image. It does not create a new image - both names point to the same image ID. This is needed because the name decides which account and repository the image goes to when pushed.
docker push saran2003g/40daysofkubernetes:day2
Pushes the image to Docker Hub. The repository gets created automatically on the first push.





