In general, the easiest safe approach is to do everything in your Dockerfile as the root user until the very end, at which point you can declare an alternate USER that gets used when you run the container.

FROM ???
# Debian adduser(8); this does not have a specific known uid
RUN adduser --system --no-create-home nonroot

# ... do the various install and setup steps as root ...

# Specify metadata for when you run the container
USER nonroot
EXPOSE 12345
CMD ["my_application"]

For your more specific questions:

Is installing packages with apt-get as root ok?

It's required; apt-get won't run as non-root. If you have a base image that switches to a non-root user you need to switch back with USER root before you can run apt-get commands.

Best location to install these packages?

The normal system location. If you're using apt-get to install things, it will put them in /usr and that's fine; pip install will want to install things into the system Python site-packages directory; and so on. If you're installing things by hand, /usr/local is a good place for them, particularly since /usr/local/bin is usually in $PATH. The "user home directory" isn't a well-defined concept in Docker and I wouldn't try to use it.

When installing python packages with pip as root, I get the following warning...

You can in fact ignore it, with the justification you state. There are two common paths to using pip in Docker: the one you show where you pip install things directly into the "normal" Python, and a second path using a multi-stage build to create a fully-populated virtual environment that can then be COPYed into a runtime image without build tools. In both cases you'll still probably want to be root.

Anything else I am missing or should be aware of?

In your Dockerfile:

## get UID/GID of host user for remapping to access bindmounts on host
ARG UID
ARG GID

This is not a best practice, since it means you'll have to rebuild the image whenever someone with a different host uid wants to use it. Create the non-root user with an arbitrary uid, independent from any specific host user.

RUN usermod -aG sudo flaskuser

If your "non-root" user has unrestricted sudo access, they are effectively root. sudo has some significant issues in Docker and is never necessary, since every path to run a command also has a way to specify the user to run it as.

RUN chown flaskuser:users /tmp/requirements.txt

Your code and other source files should have the default root:root ownership. By default they will be world-readable but not writeable, and that's fine. You want to prevent your application from overwriting its own source code, intentionally or otherwise.

RUN chmod -R  777 /usr/local/lib/python3.11/site-packages/*

chmod 0777 is never a best practice. It gives a place for unprivileged code to write out their malware payloads and execute them. For a typical Docker setup you don't need chmod at all.

The bind mounted workspace is only for development, for a production image I would copy the necessary files/artifacts into the image/container.

If you use a bind mount to overwrite all of the application code with content from the host, then you're not actually running the code from the image, and some or all of the Dockerfile's work will just be lost. This means that, when you go to production without the bind mount, you're running an untested setup.

Since your development environment will almost always be different from your production environment in some way, I'd recommend using a non-Docker Python virtual environment for day-to-day development, have good (pytest) unit tests that can run outside the container, and do integration testing on the built container before deploying.

Permission issues can also come up if your application is trying to write out files to a host directory. The best approach here is to restructure your application to avoid it, storing the data somewhere else, like a relational database. In this answer I discuss permission setup for a bind-mounted data directory, though that sounds a little different from what you're asking about here.

Answer from David Maze on Stack Overflow
🌐
TestDriven.io
testdriven.io › blog › docker-best-practices
Docker Best Practices for Python Developers | TestDriven.io
February 12, 2024 - When in doubt, start with a *-slim ... application. You want to avoid having to continually update the Dockerfile to install necessary system-level dependencies when you add a new Python package....
🌐
Snyk
snyk.io › blog › best-practices-containerizing-python-docker
Best practices for containerizing Python applications with Docker | Snyk
November 11, 2021 - But it creates another problem: you'd be adding all the system-level dependencies from the image we used for compiling the dependencies to the final Docker base image — and we don't want that to happen (remember our best practice to achieve as small a Docker base image as possible). With that first option ruled out, let’s explore the second: using a virtualenv. If we do that, we would end up with the following Dockerfile. 1FROM python:3.10-slim as build 2RUN apt-get update 3RUN apt-get install -y --no-install-recommends \ 4 build-essential gcc 5 6WORKDIR /usr/app 7RUN python -m venv /usr/app/venv 8ENV PATH="/usr/app/venv/bin:$PATH" 9 10COPY requirements.txt .
Discussions

dockerfile - Docker non-root User Best Practices for Python Images? - Stack Overflow
I have been building some python Docker images recently. Best practice is obviously not to run containers as root user and remove sudo privileges from the non-privileged user. But I have been wonde... More on stackoverflow.com
🌐 stackoverflow.com
The Perfect Python Dockerfile - better performance and security

Those are nice and surprising insights, especially on the performance side.

I wonder however what's the point of using virtual envs in a docker machine ?

Virtual envs are necessary on your local machine to separe different projects with different requirements, but in the docker context your code should be fairly isolated.

More on reddit.com
🌐 r/Python
51
298
May 19, 2021
Best practices with Python in Docker
Regarding logging I would recommend to use Grafana Loki. You install Loki driver on docker host and then configure it and then all your containers will start sending logs to your Grafana Loki instance. There options is to write logs to file, but I personally prefer centralized logging solution. I think there is even syslog driver for docker, so in theory you can send logs to syslog endpoint. Regarding automatic restart, just configure your docker container with “restart=always” in this case when container will crash, docker will restart it More on reddit.com
🌐 r/docker
9
5
October 24, 2024
The Perfect Python Dockerfile - better performance and security
One of your perks for using a virtual environment is Easy to copy packages folder between multi-stage builds but if you run pip as a non-root user and install your packages with --user you can get the same benefit without a virtual environment. Also your final L29 has COPY . . but that's going to copy your files as the root user instead of your custom user. Using USER myuser alone isn't enough to make COPY commands be owned by that user. You need to explicitly --chown them with the COPY command. Also what happens if your application requires using a package that requires C dependencies? For example the PostgreSQL package requires this. You'll end up having to install a number of apt packages to handle that. You may also want to use python as the user instead of myuser because other official Docker images use the name of the programming runtime as the name of the user. If the Python image ever creates a user for you by default in the future it'll probably end up being named python. You may also want to consider setting your PYTHONPATH and moving all of that gunicorn configuruation into its own file. You also have a bunch of individual ENV instructions. Those are going to create a layer for each one but you can combine them all into 1 layer by adding them in 1 call. You may also want to set a USER env variable because certain command line tools will expect that env variable to exist and if it doesn't you could get unexpected side effects. There's also handling assets too, such as running collectstatic but only in production mode, and likely also copying assets from a multi-stage Webpack stage. If you're curious I have a full example of the above at https://github.com/nickjj/docker-django-example . I'll also be talking about that Docker / Docker Compose set up during a live demo in a few days at DockerCon. More on reddit.com
🌐 r/django
24
78
May 24, 2021
🌐
Reddit
reddit.com › r/django › the perfect python dockerfile - better performance and security
r/django on Reddit: The Perfect Python Dockerfile - better performance and security
May 24, 2021 -

Having a reliable Dockerfile as your base can save you hours of headaches and bigger problems down the road.

https://luis-sena.medium.com/creating-the-perfect-python-dockerfile-51bdec41f1c8

This article shares a Dockerfile base that has been battle-tested through many different projects.

This can also serve as a succinct tutorial of the different features/commands used to improve the final image.

Nothing is perfect I know! Please feel free to provide any feedback and we can iterate on the shared Dockerfile if needed.

Top answer
1 of 5
36
One of your perks for using a virtual environment is Easy to copy packages folder between multi-stage builds but if you run pip as a non-root user and install your packages with --user you can get the same benefit without a virtual environment. Also your final L29 has COPY . . but that's going to copy your files as the root user instead of your custom user. Using USER myuser alone isn't enough to make COPY commands be owned by that user. You need to explicitly --chown them with the COPY command. Also what happens if your application requires using a package that requires C dependencies? For example the PostgreSQL package requires this. You'll end up having to install a number of apt packages to handle that. You may also want to use python as the user instead of myuser because other official Docker images use the name of the programming runtime as the name of the user. If the Python image ever creates a user for you by default in the future it'll probably end up being named python. You may also want to consider setting your PYTHONPATH and moving all of that gunicorn configuruation into its own file. You also have a bunch of individual ENV instructions. Those are going to create a layer for each one but you can combine them all into 1 layer by adding them in 1 call. You may also want to set a USER env variable because certain command line tools will expect that env variable to exist and if it doesn't you could get unexpected side effects. There's also handling assets too, such as running collectstatic but only in production mode, and likely also copying assets from a multi-stage Webpack stage. If you're curious I have a full example of the above at https://github.com/nickjj/docker-django-example . I'll also be talking about that Docker / Docker Compose set up during a live demo in a few days at DockerCon.
2 of 5
6
Nice write-up! A few comments: You included 3.9-slim and 3.9-slim-buster in your benchmarks, but they're actually the same image. If you look at the tags list on Docker Hub, you'll find that both are aliases to the same image. I'm curious why you had so much variation between the two benchmarks as well... maybe there are some other confounding variables. You can further improve the build time by merging neighboring ENV or RUN steps in a single step. For example: ENV PYTHONUNBUFFERED=1 ENV VIRTUAL_ENV=:/home/myuser/venv" ENV PATH="/home/myuser/venv/bin:$PATH" can be turned into: ENV PYTHONUNBUFFERED=1 \ VIRTUAL_ENV="/home/myuser/venv" \ PATH="/home/myuser/venv/bin:$PATH" which will create a single image layer instead of 3.
🌐
Python⇒Speed
pythonspeed.com › docker
Articles: Production-ready Docker packaging for Python developers
July 8, 2022 - Faster or slower: the basics of Docker build caching Docker’s layer caching can speed up your image build—if you write your Dockerfile correctly. Where’s that log file? Debugging failed Docker builds Your Docker build just failed, and the reason is buried a log file—which is somewhere inside the build process. How do you read that log file? Debugging ImportError and ModuleNotFoundErrors in your Docker image There are many reasons your Python code might fail to import in Docker.
🌐
Docker
docs.docker.com › manuals › docker build › best practices
Best practices | Docker Docs
# syntax=docker/dockerfile:1 FROM ubuntu:24.04 RUN apt-get -y update && apt-get install -y --no-install-recommends python3
Top answer
1 of 2
33

In general, the easiest safe approach is to do everything in your Dockerfile as the root user until the very end, at which point you can declare an alternate USER that gets used when you run the container.

FROM ???
# Debian adduser(8); this does not have a specific known uid
RUN adduser --system --no-create-home nonroot

# ... do the various install and setup steps as root ...

# Specify metadata for when you run the container
USER nonroot
EXPOSE 12345
CMD ["my_application"]

For your more specific questions:

Is installing packages with apt-get as root ok?

It's required; apt-get won't run as non-root. If you have a base image that switches to a non-root user you need to switch back with USER root before you can run apt-get commands.

Best location to install these packages?

The normal system location. If you're using apt-get to install things, it will put them in /usr and that's fine; pip install will want to install things into the system Python site-packages directory; and so on. If you're installing things by hand, /usr/local is a good place for them, particularly since /usr/local/bin is usually in $PATH. The "user home directory" isn't a well-defined concept in Docker and I wouldn't try to use it.

When installing python packages with pip as root, I get the following warning...

You can in fact ignore it, with the justification you state. There are two common paths to using pip in Docker: the one you show where you pip install things directly into the "normal" Python, and a second path using a multi-stage build to create a fully-populated virtual environment that can then be COPYed into a runtime image without build tools. In both cases you'll still probably want to be root.

Anything else I am missing or should be aware of?

In your Dockerfile:

## get UID/GID of host user for remapping to access bindmounts on host
ARG UID
ARG GID

This is not a best practice, since it means you'll have to rebuild the image whenever someone with a different host uid wants to use it. Create the non-root user with an arbitrary uid, independent from any specific host user.

RUN usermod -aG sudo flaskuser

If your "non-root" user has unrestricted sudo access, they are effectively root. sudo has some significant issues in Docker and is never necessary, since every path to run a command also has a way to specify the user to run it as.

RUN chown flaskuser:users /tmp/requirements.txt

Your code and other source files should have the default root:root ownership. By default they will be world-readable but not writeable, and that's fine. You want to prevent your application from overwriting its own source code, intentionally or otherwise.

RUN chmod -R  777 /usr/local/lib/python3.11/site-packages/*

chmod 0777 is never a best practice. It gives a place for unprivileged code to write out their malware payloads and execute them. For a typical Docker setup you don't need chmod at all.

The bind mounted workspace is only for development, for a production image I would copy the necessary files/artifacts into the image/container.

If you use a bind mount to overwrite all of the application code with content from the host, then you're not actually running the code from the image, and some or all of the Dockerfile's work will just be lost. This means that, when you go to production without the bind mount, you're running an untested setup.

Since your development environment will almost always be different from your production environment in some way, I'd recommend using a non-Docker Python virtual environment for day-to-day development, have good (pytest) unit tests that can run outside the container, and do integration testing on the built container before deploying.

Permission issues can also come up if your application is trying to write out files to a host directory. The best approach here is to restructure your application to avoid it, storing the data somewhere else, like a relational database. In this answer I discuss permission setup for a bind-mounted data directory, though that sounds a little different from what you're asking about here.

2 of 2
-1

Thanks again for your extensive explanations David.

I had to digest all of that and after some more reading on the topic I finally grasped everything you said (so I hope).

The reason I first added the user with a UID/GID matching the host user was, that when I started, I ran my containers on my NAS, which only allows to SSH with root. So running the container with root while the project folder is owned by another user would result in permission issues when the Container-user was trying to access the bind mounted files. Back then I did not quite understand all of that so I carried a false thought along that the container user must always match the host user id.

So I have changed my Dockerfile to use an arbitrary user like you suggested, removed all the unnecessary chown/chmod and I can run this successfully on my local macbook and on a VPS I am currently testing out.

## ################################################################
## WEB Builder Stage
## ################################################################
FROM python:3.10-slim-buster AS builder

## ----------------------------------------------------------------
## Install Packages
## ----------------------------------------------------------------
RUN apt-get update \
    && apt-get install -y libmariadb3 libmariadb-dev \
    && apt-get install -y gcc \
    ## cleanup
    && apt-get clean \
    && apt-get autoclean \
    && apt-get autoremove --purge  -y \
    && rm -rf /var/lib/apt/lists/*

## ----------------------------------------------------------------
## Add venv
## ----------------------------------------------------------------
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

## ----------------------------------------------------------------
## Install python packages
## ----------------------------------------------------------------
COPY ./requirements.txt /tmp/requirements.txt
RUN python3 -m pip install --upgrade pip \
 && python3 -m pip install wheel \
 && python3 -m pip install  --disable-pip-version-check --no-cache-dir -r /tmp/requirements.txt




## ################################################################
## Final Stage
## ################################################################
FROM python:3.10-slim-buster

## ----------------------------------------------------------------
## add user so we can run things as non-root
## ----------------------------------------------------------------
RUN adduser flaskuser

## ----------------------------------------------------------------
## Copy from builder and set ENV for venv
## ----------------------------------------------------------------
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

## ----------------------------------------------------------------
## Set Python ENV
## ----------------------------------------------------------------
ENV PYTHONUNBUFFERED=1 \   PYTHONPATH="${PYTHONPATH}:/workspace/web/app:/opt/venv/bin:/opt/venv/lib/python3.10/site-packages"

## ----------------------------------------------------------------
## Copy app files into container
## ----------------------------------------------------------------
WORKDIR /workspace/web
COPY . .

## ----------------------------------------------------------------
## Switch to non-priviliged user and run app
## the entrypoint script runs either uwisg or flask dev server
## depending on FLASK_ENV
## ----------------------------------------------------------------
USER flaskuser
CMD ["/workspace/web/docker-entrypoint.sh"]

If I want to run the container on my NAS (from the NAS host CLI with root) using bind mounts, I can still do so by using a docker-compose.override.yml that will contain

 myservice:
   user: "{UID}:{GID}"

where "{UID}:{GID}" are matching my host user who owns the bind mounted folder.

But I am also gonna change this. I am developing and testing only locally now and might use my NAS as sort of first integration environment where I will just test the fully built containers/images pulled from a registry (so no need for bind mounts anymore.

I also started to use multistage builds, which, besides making the final images way smaller should hopefully decrease the attack surface by not including unnecessary build dependencies.

🌐
Sysdig
sysdig.com › learn-cloud-native › dockerfile-best-practices
Top 21 Dockerfile best practices for container security | Sysdig
Use the minimal required base container to follow Dockerfile best practices. Ideally, we would create containers from scratch, but only binaries that are 100% static will work. Distroless are a nice alternative.
Find elsewhere
🌐
Collabnix
collabnix.com › 10-essential-docker-best-practices-for-python-developers-in-2025
Top 10 Docker Best Practices for Python Developers
August 4, 2025 - This comprehensive guide covers 10 essential Docker best practices specifically tailored for Python developers.
🌐
GitHub
github.com › dnaprawa › dockerfile-best-practices
GitHub - dnaprawa/dockerfile-best-practices: Dockerfile Best Practices · GitHub
Each RUN instruction in your Dockerfile will end up creating an additional layer in your final image. The best practice is to limit the amount of layers to keep the image lightweight.
Starred by 250 users
Forked by 19 users
🌐
Medium
luis-sena.medium.com › creating-the-perfect-python-dockerfile-51bdec41f1c8
Creating the Perfect Python Dockerfile | by Luis Sena | Medium
September 20, 2021 - You can use python instead of python3 or python3.9 command(Yes, there are other ways) You can have a single Dockerfile to run tests and deploy.
🌐
Docker
docker.com › blog › containerized-python-development-part-1
Containerized Python Development - Part 1 | Docker
May 24, 2024 - This first part covers how to containerize a Python service/tool and the best practices for it.
🌐
Depot
depot.dev › docs › container-builds › how-to-guides › optimal-dockerfiles › python-uv-dockerfile
Optimal Dockerfile for Python with uv | Container Builds | Depot Documentation
USER appuser ENTRYPOINT ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Copy code · The runtime stage uses a clean slim image and creates a non-root user for security. We copy the entire application including the virtual environment from the build stage and set proper ownership. Cache mounts in this Dockerfile speed up builds by persisting the package manager cache.
🌐
KDnuggets
kdnuggets.com › how-to-write-efficient-dockerfiles-for-your-python-applications
How to Write Efficient Dockerfiles for Your Python Applications - KDnuggets
April 16, 2025 - This is a security best practice that should be standard in all production containers. # Create a non-privileged user RUN addgroup --system appgroup && \ adduser --system --ingroup appgroup appuser && \ chown -R appuser:appgroup /app # Switch to that user USER appuser CMD ["python3", "app.py"]
🌐
Reddit
reddit.com › r/python › the perfect python dockerfile - better performance and security
r/Python on Reddit: The Perfect Python Dockerfile - better performance and security
May 19, 2021 -

Having a reliable Dockerfile as your base can save you hours of headaches and bigger problems down the road.

This article shares a Dockerfile base that has been battle-tested through many different projects.

https://luis-sena.medium.com/creating-the-perfect-python-dockerfile-51bdec41f1c8

This can also serve as a succinct tutorial of the different features/commands used to improve the final image.

Nothing is perfect I know! Please feel free to provide any feedback and we can iterate on the shared Dockerfile if needed.

🌐
Medium
medium.com › vantageai › how-to-make-your-python-docker-images-secure-fast-small-b3a6870373a0
How to make your Python Docker images secure, fast & small | by Björn van Dijkman | VantageAI | Medium
February 20, 2023 - ENV PIP_DEFAULT_TIMEOUT=100 \ # Allow statements and log messages to immediately appear PYTHONUNBUFFERED=1 \ # disable a pip version check to reduce run-time & log-spam PIP_DISABLE_PIP_VERSION_CHECK=1 \ # cache is useless in docker image, so disable to reduce image size PIP_NO_CACHE_DIR=1 ARG POETRY_VERSION=1.3.2 · The ENV variables in the Dockerfile are set to optimize the behavior of pip and poetry during the installation of packages in the Docker container.
🌐
Docker
docs.docker.com › guides › python › containerize your app
Containerize a Python application
ENV PYTHONUNBUFFERED=1 #Add dependencies for adduser RUN apt update -y && apt install adduser -y WORKDIR /app # Create a non-privileged user that the app will run under. # See https://docs.docker.com/go/dockerfile-user-best-practices/ 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.
🌐
Python⇒Speed
pythonspeed.com › articles › dockerizing-python-is-hard
Broken by default: why you should avoid most Dockerfile examples
January 17, 2025 - As a starting point, I recommend the official Dockerfile best practices documentation, Hynek’s articles, and of course the various articles on this site about Docker packaging for Python.
🌐
Pythonpodcast
pythonpodcast.com › docker-python-production-episode-222
The Python Podcast.__init__: Docker Best Practices For Python In Production
Docker is a useful technology for packaging and deploying software to production environments, but it also introduces a different set of complexities that need to be understood. In this episode Itamar Turner-Trauring shares best practices for running Python workloads in production using Docker.
🌐
Medium
medium.com › @platform.engineers › optimizing-dockerfile-performance-best-practices-b2233c41215e
Optimizing Dockerfile Performance: Best Practices | by Platform Engineers | Medium
June 7, 2024 - Each instruction in the Dockerfile creates a new layer, and excessive layers can lead to larger image sizes and slower build times. To minimize layers, it is essential to combine instructions where possible. For example, instead of using multiple RUN instructions to install dependencies, combine them into a single instruction: # Bad practice RUN apt-get update RUN apt-get install -y python3 RUN pip3 install --no-cache-dir -r requirements.txt # Good practice RUN apt-get update && apt-get install -y python3 && pip3 install --no-cache-dir -r requirements.txt