Hynek Schlawack is a Python developer who writes about packaging, deployment, testing, and web server security.
Commenters recommend his guides on building containers with uv, hardening SSL ciphers, and Python dependency management as useful and long-lasting.
Building Docker images is a tricky design problem, as with many ops tasks. The problem is due to 3 issues:
1. A Docker image is the intersection of literally 50 years of technology, starting from Unix in the 1970s, none of which are perfect. Decisions from back then still have an impact, e.g. you need to get signal handling right if you want shutdown to work (https://hynek.me/articles/docker-signals/).
2. Getting it right is all about _details_. Lots and lots of details, all of which need to be correct. Signal handling, where logs go, security updates, on and on.
3. Every organization does things slightly differently.
So there are multiple approaches:
== Build an abstracted tool ==
A tool will support certain conventions and ways of doing things... but as soon as you want to diverge too much you'll start having issues, because it only supports its particular way of doing things.
Pros: Easy for users.
Cons: Only so long as they don't want something the tool can't handle.
The problem is that it's quite difficult to build a tool that works for _all_ organizations. If you've built a custom buildpack for your company, you've built a custom tool, which a pretty good solution, but still every organization has to build their own tool.
== Build a configuration language ==
In order to support all the edge cases, you build a configuration language... and pretty soon it's super complicated because there are so many different things people want to do. In fact, you end up with…
Well my team does that for one. We use Python packaging ecosystem, specify Python dependencies using standard tools like setup.py, requirements.txt and pip. All Python dependencies are baked into a fat Python package using PEX format[1]. Also tried Facebook's xar format[2], without success yet. What matters is to have a statically linked dependencies packaged in one executable file. Like a Windows exe file.
Then you proceed with bundling in higher level OS dependencies, because each app is not just a Python package but also a collection of shell scripts, configs, configuration of paths for caches, output directories, system variables, etc. For this we throw everything into one directory tree and run FPM [3] command on it which turns the directory tree into a RPM package. We use a fpm parameter to specify installation location of that tree to /opt or /usr elsewhere.
The way to bundle it properly is to actually use two rpm packages linked together by rpm dependency. One for the app and the other for the deployment configuration. The reason is you only have one executable, but many possible versions of deploying it. Ie. depending on environment (dev, staging, prod) or you just simply want to run the same app in parallel with different configuration.
eg. one rpm package for the app executable and statix files
my_app_exe-2.0-1.noarch.rpm
and many othe related config rpms
my_app_dev-1.2-1.noarch.rpm (depends on my_app_exe > 2.0)
my_app_prod-3.0-1.noarch.rpm (depends on…
I think that for the majority of use cases when it comes to packaging and shipping Python programs, we really want to ship OS packages. We don't want users (that includes sysadmins/operations teams in this case) to deal with having pip available, understanding what virtualenv is, and so on. We just want them to be able to use apt, yum, or whatever they experience as most common on their system. Yet at the same time, we don't want our application to take in regard ancient versions of OS packages to depend on.
I think in the end we want a self contained OS package, to offer a sane install method for any target OS. Why would users have to make a difference between e.g. a C program that they install through apt, or a Python program that all of a sudden requires a completely different way of installing?
Here are some interesting thoughts on it:
https://hynek.me/articles/python-app-deployment-with-native-packages/
And here's a tool that I wrote to be able to wrap up a virtualenv into an OS package, while specifying a precise list of OS dependencies. It's not perfect by far, and admittedly a bit crude, but it already does the job well for some real life "production" stuff. It's called vdist (virtualenv distribution).
https://github.com/objectified/vdist
Documentation is here:
https://vdist.readthedocs.org/en/latest/
Ciphers to avoid DSS, MD5/RC4, SHA-1.
Ciphers to prefer ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256
A pretty good source/guide:
https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
You'll need apache 2.4+[I think], or nginx. And possibly fresh certs to use DHE/EC.
A quick rundown of a fairly secure setup:
Cipher Priority list:
ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:!ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:!RSA+3DES:!aNULL:!MD5:!DSS:!SHA:AEAD
==========================================================
Generate the cert and private key:
openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:4096 -keyout serverkey.pem -out servercert.pem
==========================================================
Generate the DH parameters:
openssl dhparam -out dh2048.pem -outform PEM -2 2048
==========================================================
How to List Elliptic Curves:
openssl ecparam -list_curves
===========================================================
Note: Generating DH parameters is gonna take a while. If you are implementing this on a slowish machine like a Raspberry Pi, you might want to use a faster machine to do the DH step, then copy file the key over.
I think we should look back to the rant of Python Core Committer Hynek Schlawack. (https://hynek.me/articles/python-app-deployment-with-native-packages/)
In short build your system and it's dependencies once and once only then pass them around through test into live.
We have three competing needs: a reliable deployment process that can move a binary-like blob across multiple test environments
A need for exactly reproducible environments but without dd/ghosting
A desire to keep things simple
Isolation is good - whether through the mostly isolated approach of venv, the almost total isolation of jails/LXC or the vagrant approach. But they focus almost entirely on binary builds - how does one pass around a python environment without rebuilding it and it's dependencies each time ala pip?
Well by taking the running built python environments and passing them into a package manager like apt and calling that a binary. That might mean tar balling a venv or tar balling /use/local/python but in the end it matters that we pass around the same basic bits.
I am working this out in pyholodeck.mikadosoftware.com and in my head - when I have a good answer I will shout
Breaking changes don't break everyone. For many projects, only a small fraction of users are broken any given time. Firefox is on version 139 (similarly Chrome and other web browsers); how many times have you had to reinstall your plugins and extensions?
For that matter, have you seen any Python unit tests written before the Pytest 8 release that were broken by it? I think even ones that I wrote in the 6.x era would still run.
For that matter, the Python 3.x bytecode changes with every minor revision and things get removed from the standard library following a deprecation schedule, etc., and there's a tendency in the ecosystem to drop support for EOL Python versions, just to not have to think about it - but tons of (non-async) new code would likely work as far back as 3.6. It's not hard to avoid the := operator or the match statement (f-strings are definitely more endemic than that).
On the flip side, you can never really be sure what will break someone. Semver is an ideal, not reality (https://hynek.me/articles/semver-will-not-save-you).
And lots of projects are on calver anyway.