One of the biggest challenges in software development is ensuring your application behaves the same way everywhere. This includes on your laptop, your teammate’s system, and in production. For example, you install Flask version 3.0, while your teammate is using 2.2. Even small differences like this can lead to confusing bugs. The app works perfectly on your machine. It can crash or behave differently on theirs.

This mismatch involves different dependencies, Python versions, or missing packages. These issues are why applications often fail when moving from development to production.

Docker solves this problem by packaging the code together with its runtime and dependencies into a container. The app behaves consistently on your laptop. It behaves the same way in a colleague’s setup. The consistency remains even on a cloud server.

In this guide, we’ll build a simple Hello World Flask app. We will then containerise it using Docker. Finally, we will run it step by step.

Create the Project Directory

Before writing any code, it’s best to start with a clean project folder. This ensures all files for your application are kept together. The files include the Python scripts, Dockerfile, and any helper files.

mkdir flask-docker-demo
cd flask-docker-demo

Now you’re inside the project directory where you’ll create your app. Keeping everything in one place also makes it easy for Docker to copy the required files into the container later.

Write the Flask Application

We’ll split the application into two files:

  • main.py → defines the application logic.
  • server.py → runs the app.

The reason for this split is separation of concerns. main.py only defines the app, which makes it safe to import into tests or production servers like Gunicorn. Meanwhile, server.py is a lightweight runner that makes it easy to launch the app locally with python server.py.
In production, you typically don’t use server.py at all — you run the app directly with a command like:

gunicorn main:app

This approach keeps the app clean, reusable, and ready for both development and production.

main.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, World! This Flask app is running inside Docker."

@app.route('/hello/<name>')
def personalized(name):
    return f"Hello, {name}! Welcome to Docker + Flask."

server.py

import main

if __name__ == "__main__":
    main.app.run(host="0.0.0.0", port=6000)

When you run a Flask app normally on your laptop, it defaults to listening on 127.0.0.1:5000. That works fine locally. However, it becomes a problem inside a Docker container. The app would only respond to requests from inside the container itself. It would not respond to your browser on the host machine.

To make it accessible from outside the container we use the following in the server.py:

main.app.run(host="0.0.0.0", port=6000)

The host="0.0.0.0" tells Flask to listen on all available network interfaces, including the container’s external interface. If we didn’t set this, Flask would default to 127.0.0.1 (localhost) and only accept requests from inside the container. In that case, your browser on the host machine would not be able to connect.

We also specify port=6000. Flask’s default is port 5000, but that port is often used by other services during development. Choosing 6000 helps avoid conflicts and keeps things predictable.

Later, when we run the container with Docker’s -p 6000:6000 option, the port we exposed in server.py will be mapped from the container to the host. That will allow you to open your browser and reach the app at:

http://localhost:6000

Create a Dockerfile

Up until now, we’ve only written Python code. But for Docker to run our app, it needs clear instructions on how to set up the environment. Docker needs to know which Python version to use. It also needs to know which files to include. Docker must identify what dependencies to install. It must also determine what command to run when the container starts.

That’s exactly what a Dockerfile provides. Think of it as a recipe that Docker follows to build an image. Without it, Docker wouldn’t know how to package our app into a container.

We’ll define our Docker file as below:

FROM python:3.11-slim
WORKDIR /app
COPY main.py server.py /app/
RUN pip install flask
CMD ["python", "server.py"]

This file does four things:

  • FROM python:3.11-slim → start with a lightweight Python image.
  • WORKDIR /app → create (if needed) and switch to a folder /app inside the container. From now on, all commands run there.
  • COPY main.py server.py /app/ → copy our code into /app.
  • RUN pip install flask → install Flask inside the container.
  • CMD ["python", "server.py"] → run server.py when the container starts.

Container File System

When we write in the Dockerfile:

WORKDIR /app

we’re not referring to a folder that already exists. Instead, this instruction tells Docker:

  • “Inside the container, create (if it doesn’t exist) and switch to a directory called /app.”
  • From this point onward, all commands in the Dockerfile (like COPY, RUN, CMD) will run relative to /app.

So when we later say:

COPY main.py server.py /app/

those files get placed into the /app directory inside the container.

And when we run:

CMD ["python", "server.py"]

the container will execute that command from inside /app, so it finds server.py immediately. In short: /app is just a convention. We could call it /code or /project. However, /app is commonly used to signal “this is where the application lives inside the container.”

Build the Docker Image

Now that the Dockerfile is ready, the next step is to build an image from it.

An image is a snapshot of your app and its environment. It includes Python, Flask, your code, and the startup command. This is what Docker will later use to launch containers.

Run the following inside the project directory (~/flask-docker-app), where your Dockerfile, main.py, and server.py all live:

sudo docker build -t my-flask-app .

Here’s what happens: the -t my-flask-app option tags the image with the name my-flask-app. This allows you to refer to it easily instead of using a long auto-generated ID. The final dot (.) tells Docker to use the current directory as the build context, meaning it will look here for the Dockerfile and the files it needs to include. After this step, Docker will have packaged everything into an image called my-flask-app, ready to be run as a container. Below is a capture from a real run on an Ubuntu machine:

sudo docker build -t my-flask-app .

[+] Building ... FINISHED
 => [1/4] FROM python:3.11-slim
 => [2/4] WORKDIR /app
 => [3/4] COPY main.py server.py /app/
 => [4/4] RUN pip install flask
 => exporting to image
 => naming to docker.io/library/my-flask-app

Here’s what’s happening:

  • Docker starts by pulling the Python 3.11 slim image as the base. Docker pulls the base image (python:3.11-slim) from Docker Hub if it isn’t already available locally.
  • It then creates the /app directory inside the container and switches into it (WORKDIR /app).
  • Next, it copies your code (main.py and server.py) into that directory.
  • Then it installs Flask inside the image (RUN pip install flask).
  • Finally, it exports everything into a new image and tags it with the name my-flask-app.

By the end of this step, you have a packaged image called my-flask-app. This image contains Python, Flask, and your app code. It is ready to be run as a container.

Run the Container

Once the image is built, it’s like having a ready-made package of your app. A container is the running instance of that image. We can now start our container with:

sudo docker run -p 6000:6000 my-flask-app

This tells Docker: “Take the my-flask-app image and launch it. Map the app’s port 6000 inside the container to port 6000 on my machine.” With that, you can open your browser at http://localhost:6000 and see the app in action.

Test in the Browser

Now open the following to see out flask based hello world running as follows:

http://localhost:6000

And you’ll see:

Hello from Flask inside Docker!

Troubleshooting

Browser blocks port 6000 (ERR_UNSAFE_PORT)

Your browser (e.g., Chrome) blocks 6000 as unsafe, so reaching http://localhost:6000 fails with ERR_UNSAFE_PORT. Keep Flask listening on 6000 inside the container, but expose a safe host port like 5001:

sudo docker run -p 5001:6000 my-flask-app
# then open:
http://localhost:5001

Make the app itself use a safe port

If you’d rather align everything to a safe port end-to-end, switch Flask to 5001 inside the container:

# server.py
from main import app
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

Rebuild and run with matching ports

Bake that change into the image and run with 5001 on both sides:

sudo docker build -t my-flask-app .
sudo docker run -p 5001:5001 my-flask-app
# open:
http://localhost:5001

Verify and manage containers

See what’s running:

sudo docker ps
[sudo] password for vbhadra: 
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                         NAMES
1b786200ddc2   my-flask-app   "python server.py"       34 minutes ago   Up 34 minutes   0.0.0.0:5001->5001/tcp, [::]:5001->5001/tcp   stupefied_nightingale

Output shows our containers:

  • my-flask-app (command: python server.py) mapped 0.0.0.0:5001->5001/tcp — up for 34 mins.

Inspect logs

You can view the Flask container’s logs with the following command:

sudo docker logs 1b786200ddc2

This showed the app binding to 0.0.0.0:5001, successful GET / requests (200), and a GET /favicon.ico (404), which is normal without a favicon.

If you ever want to stream logs live, use:

sudo docker logs -f 1b786200ddc2

Cleanup

When you’re finished, it’s good practice to clean up: stop your containers, then remove them.

# stop by ID or name
sudo docker stop <container_id_or_name> <another_id_or_name>

# remove the stopped containers
sudo docker rm <container_id_or_name> <another_id_or_name>

Example:

vbhadra@vbhadra-DQ77MK:~/flask-docker-demo$ sudo docker stop  1b786200ddc2
vbhadra@vbhadra-DQ77MK:~/flask-docker-demo$ sudo docker rm 1b786200ddc2
vbhadra@vbhadra-DQ77MK:~/flask-docker-demo$ sudo docker ps 
CONTAINER ID   IMAGE     COMMAND                  CREATED       STATUS       PORTS     NAMES

That’s it. We took a tiny Flask app. We packaged it with a Dockerfile and ran it as a container. Then, we checked it in the browser and fixed the port/binding issues. Finally, we cleaned up. You now have a portable, repeatable setup that runs the same anywhere Docker runs.

From here, the natural next step is the cloud. We need to publish the image to a registry. Next, we should run it on a managed container platform. Finally, put it behind a secure, scalable entry point. It should have basic observability, environment-based configuration, and automated delivery. That’s how this “hello world” will eventually grow into a small, production-ready service without locking into any single provider. But that is for another day!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.