How do I package my Python app for production? A Docker Guide.
Want to take your Python Flask app from local development to production? This guide shows you the end-to-end chain from running it with Gunicorn to packaging with Docker, testing it locally, publishing it to Docker Hub, and deploying it to the web with Hostly.
So you crafted a web-app using Python and a web application framework like Flask and now you want to package your project for further distribution? In the following article you will learn how to containerize your Python Flask Application using Docker and how to quickly deploy your application on Hostly afterwards!
How to dockerize a Python Flask Application
A Python application that runs on your computer still needs a predictable runtime, its dependencies, and a process that can serve requests once it is deployed. A container image packages those pieces together so the application can run on another machine without recreating your local Python setup.
In this tutorial, we will create a small Flask API that reads part of its response from an environment variable. We will then package it with Gunicorn, test the container locally, publish the image to Docker Hub, and deploy it on Hostly.
Want to know more about Hostly and how it works?
The example is intentionally small. There will be no database, authentication, virtual environment, or complex project structure. The goal is to understand the basic route from a Python file to a deployable image.
What you will need
Before starting, make sure you have:
- Docker Desktop or the Docker Engine
- A Docker Hub account (if you want to publish your image)
- A text editor or IDE
- A terminal (the tutorial is written with a linux shell in mind)
You do not need to install Flask or Gunicorn directly on your computer. Docker will install them while building the image.
The tutorial uses Python 3.14, whose latest stable maintenance release at the time of writing is Python 3.14.6. For our image, we will use the official python:3.14-slim image so that future builds can receive newer Python 3.14 patch releases without unexpectedly moving to another minor version. But more on this later.
1. Create the project
Create a directory for the application and cd into it:
$ mkdir hello-flask-api && cd hello-flask-apiThe finished minimum project will contain the following four files in this directory:
hello-flask-api/
├── app.py
├── requirements.txt
├── .dockerignore
└── DockerfileWe will create each one in the following sections.
2. Create the Flask application
Create a file named app.py and copy the following example code into it:
import os
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/")
def hello():
target = os.getenv("GREETING_TARGET", "World")
return jsonify(
message=f"Hello, {target}!"
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000)This is a basic Flask app, exposing a single API endpoint at /. It reads the GREETING_TARGET environment variable and uses its value in the response.
When the variable is not set, the application uses World as its default value and answers requests with the following JSON:
{
"message": "Hello, World!"
}Later, we will configure GREETING_TARGET through Hostly without changing or rebuilding the image.
The if __name__ == "__main__": block allows the application to be started directly with the command python app.py during local development. It binds the API to the IP 0.0.0.0, which makes the server reachable from outside its own process environment, including when it is run in a container. This will be useful later, when we want to containerize the application.
The built-in Flask server is intended for development rather than production deployments. Our image will therefore use Gunicorn to run the application when the container starts. Flask’s deployment documentation recommends using a dedicated WSGI (Web Server Gateway Interface) server for production workloads and documents Gunicorn as one of the supported options.
3. Declare the dependencies
Create a file named requirements.txt and copy the following lines into it:
Flask==3.1.3
gunicorn==26.0.0These are dependencies that Python will use to run your application.
Flask provides the web application framework, while Gunicorn provides the production webserver that accepts HTTP requests and passes them to the Flask application.
The versions are pinned so that rebuilding the image installs the same application dependencies rather than silently selecting newer releases. At the time of writing, these are the current Flask and Gunicorn releases.
For a larger application, requirements.txt can be much more extensive, as it would also contain packages for database access, authentication, validation, logging, or any other functionality the application uses.
4. Add a .dockerignore file
When Docker builds an image, it sends the project directory to the Docker builder as the build context. To not bloat the container or share private files (like API keys), you should exclude those files. Docker does this in a similar way to Git and its .gitignore.
Create a file named .dockerignore and copy the following lines of text into it:
__pycache__/
*.py[cod]
*.log
.venv/
venv/
.env
.env.*
.git/
.gitignore
.pytest_cache/
.coverage
htmlcov/Even though this tutorial does not create a virtual environment, excluding common virtual-environment directories prevents one from being copied accidentally if the project is extended later on.
The .env entries are especially important. Environment files frequently contain passwords, API keys, or other configuration values that should not be embedded in an image. Those values should only be provided when the container is deployed.
5. Create the Dockerfile
Create a file named Dockerfile without any file extension and insert the following content. Don't worry, we will explain every step!
# Base image
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt ./
# Install requirements/dependencies into the image
RUN pip install --no-cache-dir -r requirements.txt \
&& useradd \
--create-home \
--user-group \
--uid 10001 \
--shell /usr/sbin/nologin \
appuser
# Use dedicated non-root user for web-app
COPY --chown=appuser:appuser app.py ./
USER appuser
# The port to expose to the outside world
EXPOSE 3000
# What to execute on startup
CMD [
"gunicorn",
"--bind",
"0.0.0.0:3000",
"--workers",
"2",
"--access-logfile",
"-",
"app:app"
]This is still a relatively small Dockerfile, but it includes a few practices that make it more appropriate for an actual deployment.
Choose the Python base image
FROM python:3.14-slimEvery container image starts from a base image. The official Python image already contains the Python interpreter and the basic tooling needed to install Python packages.
The slim variant is based on a reduced Debian installation. It is smaller than the full Python image while retaining the broad package compatibility of a Debian-based environment. The official Python image publishes multiple variants for different size and compatibility requirements.
Some Python packages include native extensions and may require additional operating-system libraries or a compiler during installation. Our Flask application uses pure Python dependencies, so it does not require a separate build stage or additional Debian packages.
Configure Python for containers
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1PYTHONDONTWRITEBYTECODE=1 prevents Python from creating .pyc bytecode files inside the container.
PYTHONUNBUFFERED=1 causes output written to standard output and standard error to be forwarded immediately. That is useful in a container because application logs are normally collected from those output streams.
Set the working directory
WORKDIR /appThe remaining Dockerfile instructions operate relative to /app. This is also the directory from which Gunicorn will start.
Copy the dependency file first
COPY requirements.txt ./The dependency file is copied before the application source so Docker can cache the dependency-installation layer.
When app.py changes but requirements.txt does not, Docker may reuse the previously installed dependencies instead of downloading them again. Structuring Dockerfiles around cacheable dependency layers is a standard way to make repeated builds faster. If you are new to Docker, this is not as important, but will become more important the bigger and more complex your images get.
Install the dependencies
RUN pip install --no-cache-dir -r requirements.txt \
&& useradd \
--create-home \
--user-group \
--uid 10001 \
--shell /usr/sbin/nologin \
appuserpip installs Flask, Gunicorn, and their dependencies from requirements.txt.
The --no-cache-dir option prevents downloaded package archives from remaining in the finished image after installation.
The same build step creates an unprivileged Linux user named appuser. The application does not need administrative permissions, so there is no reason to run it as root.
The numeric user-ID is set explicitly instead of relying on whichever default ID the base image assigns. This makes the container identity more predictable if the image is later used with mounted storage or stricter runtime security policies.
Copy the application
COPY --chown=appuser:appuser app.py ./The application source is copied into /app and assigned to the non-root user.
For a larger project, you might copy an entire package or source directory instead:
COPY --chown=appuser:appuser src ./srcFor this example, the application consists of only one file.
Switch to the non-root user
USER appuserEvery following instruction, including the command that starts the application, runs as appuser.
Running the application without root privileges reduces the attack surface if the application or one of its dependencies were compromised. Docker’s image-building guidance includes running the final application process as a non-root user as part of a production-oriented baseline.
Document the application port
EXPOSE 3000The Flask application and Gunicorn will listen on port 3000.
EXPOSE documents that port in the image metadata. It does not make the container publicly accessible by itself. When running the image locally, we still need to publish the port with docker run -p. When we later on run it on Hostly, we will enter 3000 as the container port to make it accessible from the web.
Start the application with Gunicorn
CMD [
"gunicorn",
"--bind",
"0.0.0.0:3000",
"--workers",
"2",
"--access-logfile",
"-",
"app:app"
]The final argument, app:app, tells Gunicorn what to load:
- The first
apprefers to theapp.pymodule. - The second
apprefers to the Flask application created byapp = Flask(__name__).
This follows Gunicorn’s {module}:{application} loading syntax, which we will not go into detail in this tutorial.
Gunicorn binds to 0.0.0.0:3000, allowing traffic to reach it at port 3000 and later on to also reach it from outside the container.
The example starts two worker processes. One worker can only execute one synchronous request at a time, so two workers provide a more useful starting point for a small web service. Whenever you run anything more sophisticated than this example, you should adjust the worker count according to the workload and available memory rather than keeping this as a static value.
--access-logfile - writes HTTP access logs to standard output, allowing them to appear alongside the other container logs, which is just a convenient way to have everything in one place for something quick and small like a tutorial app.
6. Build the image
The project should now look like this:
hello-flask-api/
├── app.py
├── requirements.txt
├── .dockerignore
└── DockerfileBuild the image from the project directory:
$ docker build -t hello-flask-api:1.0.0 .The -t option assigns the image a name and version tag:
hello-flask-api:1.0.0The final period after the version tag tells Docker to use the current directory as the build context (your project directory).
During the build, Docker will:
- Download the Python base image
- Copy
requirements.txt - Install Flask and Gunicorn into the image
- Create the non-root user
appuser - Copy
app.py - Save the result as a new image
After the build completes, confirm that the image exists:
$ docker image lsYou should see hello-flask-api with the tag 1.0.0.
7. Run the image locally
Now that we have our built a custom Docker-Image, we can try to run it locally and see what happens. To start a container from the image, run the follow command:
$ docker run \
--rm \
-p 3000:3000 \
hello-flask-api:1.0.0The port mapping has the following structure:
HOST_PORT:CONTAINER_PORTWith this the traffic sent to port 3000 on your computer is forwarded to port 3000 inside the container.
Open the following address in your browser:
http://localhost:3000Alternatively, you can just use curl:
$ curl http://localhost:3000Because no greeting variable was provided, the response uses the default value:
{
"message": "Hello, World!"
}If you now want to stop the container, you can simply do this with Ctrl+C.
Test the environment variable
Now run the image again, but this time we will be setting GREETING_TARGET:
$ docker run \
--rm \
-p 3000:3000 \
-e GREETING_TARGET=Container \
hello-flask-api:1.0.0Request the endpoint again, e.g. with curl:
$ curl http://localhost:3000The response should now be:
{
"message": "Hello, Container!"
}The same image produced both responses by only changing its runtime configuration.
This is one of the most useful properties of environment-based configuration: the application image does not need to be rebuilt for every deployment environment. Depending on your use-case those configurable feature flags can save you a lot of build time and enable a wide variety of complex configurations. But because of the possible complexity of the configuration options you will want to judge carefully when and how to use them, so you won't get into an undefined state for your application.
8. Publish the image to Docker Hub
The image currently exists only in your local Docker environment. To run it on Hostly, the image needs to be pulled in from a container registry.
The next steps will teach you the basics of publishing an image onto Docker Hub.
- Create a public Docker Hub repository named:
hello-flask-api- Authenticate from your terminal:
$ docker login- Tag the local image with your Docker Hub username:
$ docker tag \
hello-flask-api:1.0.0 \
YOUR_USERNAME/hello-flask-api:1.0.0Replace YOUR_USERNAME with your actual Docker Hub username.
- Push the image to Docker Hub.
$ docker push YOUR_USERNAME/hello-flask-api:1.0.0docker push uploads a tagged image to Docker Hub or another configured registry. Registry authentication is handled through docker login.
- Once the upload finishes, the complete image address will be:
docker.io/YOUR_USERNAME/hello-flask-api:1.0.0Using an explicit version tag makes it possible to tell exactly which application version a deployment is running - something that can help you in debugging, once your application is running. Whenever the application changes, you can publish a new tag such as 1.0.1.
Building for more than one CPU architecture
A normal docker build creates an image for the architecture used by your Docker environment. Depending on your computer, that will commonly be either AMD64 or ARM64.
To publish a single image with support for both architectures, use Docker Buildx:
$ docker buildx build \
--platform linux/amd64,linux/arm64 \
-t YOUR_USERNAME/hello-flask-api:1.0.0 \
--push .A multi-platform build publishes multiple architecture-specific image variants under one image name. The container platform can then select the compatible variant when pulling the image.
Pure Python packages are usually straightforward to build for multiple architectures. Projects with compiled dependencies may need additional build tools or architecture-specific packages. You will need to consult the appropriate framework documentation to find out how.
9. Deploy the image on Hostly
The published image now contains everything required to run the application: Python, Flask, Gunicorn, the installed dependencies, and the application source.
Create a new service in Hostly and enter:
- Your desired
hostly.ninjasubdomain - Image URL:
docker.io/YOUR_USERNAME/hello-flask-api:1.0.0 - Container port:
3000
Add the following environment variable to get a customized example running:
GREETING_TARGET=HostlyThe example does not require secrets, volumes, or configuration files, so those sections can remain empty and you can click right through them.
Finally click Deploy.
Hostly will pull the image, start the container, route traffic to port 3000, and provide SSL for the selected subdomain. The service health check only needs to establish a successful TCP connection to the configured port, so the Flask application does not need a separate health endpoint - something a bigger codebase would want to ship as well, to see if everything is running as expected.
Opening the assigned address should return:
{
"message": "Hello, Hostly!"
}This tutorial uses a public Docker Hub repository to keep the publishing and deployment steps simple, but Hostly is able to also pull from private repositories when the required registry credentials are configured appropriately.
What you have built
Let us recap what we have done and learned in this tutorial: You started with a single Python file and produced an image that can run consistently on your computer or on Hostly.
The application demonstrates the basic responsibilities of a containerized Flask service:
- Declare application dependencies in
requirements.txt - Use a production WSGI server instead of exposing Flask’s development server
- Bind the server to
0.0.0.0 - Keep runtime configuration outside the image
- Install dependencies before copying frequently changing source files
- Run the application as a non-root user
- Test the completed image locally
- Publish the image to a container registry
- Deploy it using its image URL, port, and environment variables
A larger Flask application may contain additions like blueprints, database models, migrations, templates, and many more dependencies, but the deployment remains mostly the same: package the application into an image, make it listen on a known port, and supply its environment-specific configuration when the container starts. Afterwards you can deploy and test, that everything is working as expected.
Have fun building your dockerized Python applications on Hostly!

