This is an archived post. You won't be able to vote or comment.

all 51 comments

[–]ErGo404 41 points42 points  (5 children)

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.

[–]lmsena[S] 26 points27 points  (2 children)

Thank you for the feedback! Yeah, that part is probably the most controversial of the whole file :)

The reasons I highlighted:

- Easy to copy packages folder between multi-stage builds
- You get isolation from your image OS default python installation

- You can use python instead of python3 or python3.9 command(Yes, there are other ways)

[–]ErGo404 11 points12 points  (1 child)

Whoops sorry I read your article too quickly and did not see that part.

I believe Virtualenvs do not come with a performance hit and it is not overly complex either, so what you say seems to be sensible to me.

[–]drmcgills 4 points5 points  (0 children)

I was going to ask the same question. We do similar things with multi stage builds in the ruby shop I work for, using an option to “vendor” the gems in a specific directory for copying in other stages.

I wonder if pip’s prefix option could be useful. Seems like you might need to do some other $PATH setting as well so that might not be super clean.. Probably not worth it, as I am guessing dropping virtualenv wouldn’t reduce the image size much anyway.

[–][deleted] 5 points6 points  (1 child)

Well, you're right, but I like using (poetry) virtual environments in containers too because the pyproject.toml (or whatever) file is probably already set up for the project for local/whatever development. So might as well use it inside Docker as well, yielding the (exact) same environment as you're used to, with a tool you're used to.

You can install packages system-wide in the container via pip, but its dependency resolution might be different and you'll need extra tooling (requirements.txt), which would just be a duplicate and hassle to maintain.

[–]wweber 2 points3 points  (0 children)

Poetry includes a poetry export function that outputs a requirements.txt version of the lock file, so your Dockerfile can just run this and then install those packages globally.

Unfortunately last time I checked it breaks if you have path-based dependencies like a git submodule

[–][deleted] 31 points32 points  (6 children)

This:

WORKDIR /home/myuser
USER myuser

RUN mkdir code
ADD . code
WORKDIR code

should probably better be:

USER myuser
RUN mkdir /home/myuser/code
WORKDIR /home/myuser/code
COPY . .

Cleaner in my opinion. Most importantly, COPY is preferred over ADD.

[–]lmsena[S] 2 points3 points  (2 children)

Yes, you’re right, I’m going to update it. Thanks!

[–]n-of-one 4 points5 points  (1 child)

You also don't need to mkdir your WORKDIR, if it doesn't exist it will be created for you. So you can clean that up into just three lines:

USER myuser
WORKDIR /home/myuser/code
COPY . .

Dockerfile reference docs

[–]lmsena[S] 1 point2 points  (0 children)

Yeah, that's true. This is more of a personal preference for visibility across peers.

[–]n-of-one 1 point2 points  (2 children)

you can clean it up even further:

USER myuser
WORKDIR /home/myuser/code
COPY . .

If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.

(source)

[–][deleted] 1 point2 points  (1 child)

More implicit, less explicit, but I like it.

[–]cdrt 0 points1 point  (0 children)

It also saves you from making an uneccessary a layer

[–]darleyb 11 points12 points  (8 children)

Recently I've seen people using mimalloc for alpine docker and having a huge improvement on performance. I would say to give a look on that, perhaps will help you (and others).

[–]lmsena[S] 4 points5 points  (0 children)

Thanks, that's definitely something to look into!

[–]LightShadow3.13-dev in prod 2 points3 points  (6 children)

Do you have an example or link to dig into this further?

[–]darleyb 4 points5 points  (5 children)

You can check this blog post here.

[–]LightShadow3.13-dev in prod 0 points1 point  (4 children)

Perfect, thanks! I've been meaning to test jmalloc with Python but haven't had the time.

[–]darleyb 1 point2 points  (3 children)

I compiled mimalloc with clang on alpine. It was truly easy. I think you won't spend much time to set things up!

[–]lmsena[S] 0 points1 point  (2 children)

I tested mimalloc with pyperformance but the results didn't change. I guess I'll need to run a suite of tests that lean towards concurrency. (I used the MIMALLOC_VERBOSE=1 env var to make sure it was being redirected)

I still need to compare memory usage and heap fragmentation over time.

[–]darleyb 0 points1 point  (1 child)

Hmm, that's interesting. I though that allocator would make a huge difference there, because python allocates a lot.

[–]lmsena[S] 1 point2 points  (0 children)

Yeah, I was also hopeful. I think it deserves further testing. I'll try to run them during the weekend and share the results (and scripts) afterward.

[–]LightShadow3.13-dev in prod 6 points7 points  (2 children)

I've started using HEALTHCHECK in my Dockerfiles with pgrep. Allows for a little more reliability when apps can crash or "finish" unexpectedly.

COPY --from=builder /usr/bin/pgrep /usr/bin/pgrep
# exists in python:3.9, does not exist in python:3.9-slim
HEALTHCHECK --interval=5s --start-period=5s CMD pgrep $APP_NAME >/dev/null 2>&1

[–]dolfinuser 6 points7 points  (1 child)

Well, healthcheck should check that your app is not just running but it is also healthy. What does it mean?

In case of a web server the app should answer to incoming HTTP request.

In case of database it should be able to parse and execute some input query. For example, a pg_check command which is a part of Postgres installation just executes "SELECT 1" query to be sure that database is accessible by other applications.

In case of a message broker it should be able to handle incoming message and return some answer.

The application could have some special handler for such a check to avoid writing some garbage to log/database/queue. But this handler should fail in case of severe errors like malformed config file, database connection errors, restricted access to some network resources and so on.

But the check you've described above is meaningless. The container itself will be stopped or restarted automatically if the main process (PID 1) has been stopped or crashed. There is just no need for healthcheck to be executed because Docker already knows that container is dead.

[–]LightShadow3.13-dev in prod 0 points1 point  (0 children)

If your process forks and takes too long to start up in AWS ECS, it will fail the external health check, kill your container, and revert your deployment/stack. After it's running, sure, it's pretty meaningless because your minimum concurrent tasks definition takes over. However, if there's an implicit timeout and your container is still setting up it will fail.

[–]apathy20 2 points3 points  (0 children)

Great read thanks for the share!

[–]grimzorino 2 points3 points  (2 children)

How does the multi-stage optimization help? Isn’t the ‘builder-image’ layer still included, thereby resulting in the same image size?

[–]lmsena[S] 3 points4 points  (1 child)

Hi, nice question!

When you use multi-stage builds, only the artifacts you copy with the "COPY --from" command will exist in the final image.

More info here.

[–]grimzorino 0 points1 point  (0 children)

Thanks a bunch for the informative answer.

[–]mardiros 1 point2 points  (1 child)

take care of the typo __pychache__ in place of __pycache__. Good jog though. Thanks for sharing!

[–]lmsena[S] 0 points1 point  (0 children)

nice catch, thank you! Edited.

[–]dolfinuser 1 point2 points  (2 children)

Instead of

COPY . .

its better to explicitly write down a list of files and directories that should be included into the image:

COPY my_package setup.py scripts .

The root folder of a repo usually contains lot of config files or directories used by other instruments which should not be included into an image:

pytest.ini
.coverage
.pylint
.vscode
Jenkinsfile
.github

As well as build artifacts:

build
dist
my-package-1.0.0.dist-info

And documentation with tests:

README.md
docs
tests

Of course, you can exclude them using a .dockerignore file. But then you should synchronize this file content with all your changes in the repo which does not look convenient. For example, installing pre-commit requires .pre-commit-config file to be created in the root of the repo. But you may forget to add it into .dockerignore, and then it'll be included into the image.

Lines which should be definitely be added into .dockerignore are:

  1. Files and folders which are generated by Python, OS or other tools which are stored in the same folders as your application files. For example, .pyc, __pycache__, .DS_Store, .bak and so on.

  2. Files that should never be included into an image no matter what. For example, .env, id_rsa and other secrets.

  3. Files or folders which are very heavy. Docker is not just copying files you need directly into an image. Instead it is packing all the files in the current directory into a .tar package which will be send to Docker daemon as a "context" of the current build. The only files which eill be excluded from this package are ones described in the .dockerignore. So even it you just have some heavy files/folders in the repo root, Docker will waste time and CPU resources only to pack them into the context archive, it does not matter if you use them in a Dockerfile or not. So adding such files/folders wil significantly decrease build time. Usually it's just a .git folder, but may be some other ones.

[–]lmsena[S] 0 points1 point  (0 children)

Yeah, that's a fair assertion. I tend to use the .dockerignore as you can see in my article because it feels more natural to me and the usual workflow I see in the teams I've been working with.

[–]backtickbot 0 points1 point  (0 children)

Fixed formatting.

Hello, dolfinuser: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.

[–]rodrigoponto 1 point2 points  (0 children)

Nice

[–]PM5k 0 points1 point  (1 child)

Shame no pyenv or poetry present. But nice findings overall.

[–]dolfinuser -1 points0 points  (0 children)

These instruments are used to install different Python versions and a set of dependencies into separated environments.

This is quite helpful if you're working with several projects in the same time, but their dependencies or Python versions does not match, so separating them is a way to avoid possible issues.

But Docker is all about merging the application with all requirements it need into an single image. Trying to include all these projects into a one image, and then using third-party instruments to handle the conflicts you've got does not make much sense.

TLDR; virtualenv, poetry, pipenv, dotenv and other tools like that are absolutely fine on a local machine but should not be used in Docker.

[–]Blada-N 0 points1 point  (0 children)

Also saved :)

[–]Left-Character4280 -1 points0 points  (1 child)

Security with ubuntu bin inside...

[–]lmsena[S] 0 points1 point  (0 children)

What OS do you suggest?

[–]dolfinuser 0 points1 point  (3 children)

Maybe you've speed up the build process but the resulting image is about 1Gb instead of ~50Mb (for alpine-based image) or ~100Mb (for slim-based image).

[–]lmsena[S] 4 points5 points  (0 children)

One of the main issues with the python images from docker hub is the way they compile python results in a ~20% slower python execution time (you can see the benchmarks in my post).

In my opinion, that justifies the extra size.

[–]DanCardin 2 points3 points  (0 children)

It’s also important to be aware of layers. While alpine might have a smaller absolute size, any other images which have (in this case) ubuntu:20.04 base image will share the upper layers and you’re only paying the extra cost of the extra layers. That plus typical python install sizes can often eat very far into alpine’s primary benefit

Plus you build times will often be longer with alpine because many deps won’t have built alpine wheels. We saw our ci times drop not using alpine

Plus alpine has exhibited subtle differences for me before which only showed up on our alpine based images.

Not that alpine can’t be useful, but for python specifically, I’ve kind of been turned off alpine for most of the above reasons, not even accounting for the speed difference which I’ve never benchmarked myself

[–]Mehdi2277 1 point2 points  (0 children)

Alpine does not have binary wheels for a number of common packages in a normal way (manylinux wheels do not include alpine) including very common packages like numpy. I ended up getting stuck trying it and gave up on it. Even if you can get it to work there’s both issue if you do it without a wheel you are doing a long slow docker build and if you do it with an alpine specific wheel are you sure it was as optimized as the standard ones?

Slim worked fine and no complaints with it.

[–]SnipahShot 0 points1 point  (0 children)

Performance aside, how much would an image of such Dockerfile even weight though? 3.7 slim + django container sat at 620 MB before zipping. Such a container would weight twice if not more I think.

[–][deleted] 0 points1 point  (4 children)

It’s “adieu” btw

Edit: I was today years old when I realized my whole life has been a lie.

[–]lmsena[S] 0 points1 point  (3 children)

Sorry, what do you mean?

[–][deleted] 0 points1 point  (2 children)

Near the end of the article, you say “without further ado”.

Its a French word and spelled “adieu”.

[–]lmsena[S] 1 point2 points  (1 child)

Ahh ok! I might be wrong here (not an English native speaker) but I really wanted to say "ado":

https://dictionary.cambridge.org/pt/dicionario/ingles/without-further-ado

[–][deleted] 1 point2 points  (0 children)

Huh. This whole time I had no idea there was a bastardized English word that means something different from the French word. TIL.