The What, Why and How of Containers
annwan.me
annwan.me
Reminds me of how Alan Kay described OOP as communicating objects, where each object is a kind of computer.
"I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages."
The OOP humanity is so heavily invested on has very little relation to that vision on the GP.
As a timeline I like to plot the ratio of users to isolated compute. We've moved along points like users per building, users per room, user per computer, computers per user, kernels per user, processes per user.
Containers enabled the latest shift.
One thing that is wild to me is how nix solves this problem, of things needing to be linked together. It doesn't solve it with containers, but by rewriting the location of the links in the executable to be in the nix store. You can run LDD and see it in action.
To me, all that points at containers being in some way a solution to Dynamic linking. And maybe an over the top solution.
Should we be doing more static linking? Not even depending on libc? What are the challenges with that?
The good part of containers is you isolating the thing you're running. I'm very against resource waste, but if I can spend 90MB on a container image instead of installing a complete software stack to run a task which is executed weekly and runs for 10 minutes, I'd prefer that. Plus, I can create a virtual network and storage stack around the container(s) if I need to.
Case in point: I use imap-backup to backup my e-mail accounts, but it's a ruby application and I need to install the whole stack of things, plus the gems. Instead I containerize it, and keep my system clean.
Nix is something different and doesn't solve "many foreign processes not seeing each-other on the same OS" problem. Heck, even Docker doesn't solve all problems, so we have "user level containers" which do not require root access and designed to be run in multitenant systems.
For sure, I was thinking of the packaging nature of containers, not the 'security' nature of containers. The pivotroot part. Though I guess being able to have namespaces does make packaging clearer in certain cases.
For a horrible analogy: With actual shipping containers, we don't have each shipping container be a stripped down model of a ship, so that the things in it aren't confused.
Docker just made the interface more practical, and built the ecosystem around it. lxcontainers, apptainer and podman has improved upon the idea in different ways to cater different use cases.
So for me containers were never simplified ships to begin with. This different perspectives happen probably because people look from different perspectives at initial contact, which is normal.
I don't think there is a good physical-world analogy for what containers are doing, though. Maybe takeout including utensils and napkins to "simulate a kitchen" in case you eat it on the go, so you don't have to rely on the "system" forks and napkins? Still kinda rubbish honestly.
>> but it's a ruby application and I need to install the whole stack of things, plus the gems. Instead I containerize it, and keep my system clean.
There was a bit of drama recently with the Bottles project and some Distro maintainers. With the Bottles devs saying "we only want the appimage as a distribution method".
I see containers as a means of distributing software. If that ruby app was a binary, then the container would become baggage.
Good for them having wishes, yet free software doesn't work that way in most cases. :)
> I see containers as a means of distributing software
That's OK. Docker is a tool, and tools can be held and used in many ways.
Historically I would agree with you.
But bottles, and the attitude of some upstream projects is one of "modern", "faster", "support the latest version only" ... Software devs who live in user space want a more "App Store" like delivery mechanism where they have control. The kernel is inclined to never break backwards compatibility. Distros are sort of stuck in the middle...
>> I see containers ... > That's OK. Docker is a tool,
One of these is a subset of the other... (depending on your perspective). Dockerdesktop running a linux image on a windows box is most certainly a tool... that same container being ported to lcx in production makes it a package manager. Who is a subset of whom is something we could debate but these things can be both.
They can have control. They can say that we have flatpaks and app images we publish and support, and they only support the latest version (or the same minor version, whatever), and can say that packages got from distros may not be the latest.
There's no need for a yelling match, IMHO.
Distros also can do whatever they want. Like rclone. You can get the packages from rclone.org or from your distros. There's no yelling match, but trade-offs.
> ...that same container being ported to lcx in production makes it a package manager.
I don't think so. I have containers which work like binaries (in the form of "./binary infile outfile" fashion) and exit after processing what I give them. For me that container is a utility program as a whole. Same for the imap-backup example.
When you think services which are always on, docker might be a package manager, but I pack my own containers for example, so it's more like compiling for me.
So, what docker or containers is depends on your perspective, or like a chameleon which changes its color according to the landscape it's in.
So, it's a tool in the end. Package managers are tools, too.
When I write something, I just release the source code and I let other do the non fun part for me. You just have to mention the dependencies and how to build and that's it.
The thing with Bottles is that it also relies on a very specific setup of all underlying software. So in that case it's more about complexity that leads to the need of containerization than keeping systems clean or containerizing for sandboxing.
I'd say that we don't have good solutions to that problem. But nobody even tried our best candidates, so I'm not completely sure. (For a while, it looked like Android would finally try some. But then Google turned it into a user-hostile anti-privacy OS.)
Yes, some file systems implement "deduplication".
[1]: https://fuchsia.dev/fuchsia-src/concepts/filesystems/blobfs
Plus you don't need to recompile the universe because of a patch.
In general, it's much easier to link a binary with its libraries than go in to the opposite direction (extracting common code from static linked binaries), because once you statically link a binary the library code will vary slightly due to differences in memory addresses, compiler optimizations, unused code that has been omitted, different input library versions, etc.
Even if you were, in theory, able to write a complex filesystem driver that is able to extract the common parts of statically linked libraries so they can be deduplicated, then to reconstruct the original binary in memory, you'd have to perform something similar to dynamic linking, except now in the kernel, which really isn't an improvement.
But aren't we, when we use OCI images as a packaging mechanism, using containers to essentially throw away that sharing and arrive at a complicated version of static linking, where everything dynamically linked is shipped with the program?
Same goes for arguments about ease of patching things. When the software's package is actually an image, you are patching each image individually that is running on the system.
So there's no free lunch and everything is a trade off. People thinking that containers are no more work than managing servers or services are in the wrong.
The problem is the way we usually package the apps with Docker-Dockerlike builders
For example, if you have a complex service that consists of multiple binaries all written in C++ using boost, then for each binary you can create a container that contains a layer of a base OS (shared), C++ libraries (shared), boost libraries (shared), application binary (unique).
All the services can now share their common libraries, both on disk and in memory, which reduces I/O and memory use. That's one of the main advantages of containers over virtual machines (VMs): each VM instance has a distinct region of memory that is not shared with others even if they happen to load bit-for-bit identical binaries into memory.
(I know, VM memory deduplication exists to ameliorate this problem, but here my previous comment applies: it's much easier to start from shared components and link them together than extract the shared data after the fact. And typically VMs have lots of nonsharable state that containers do share, like pretty much all writable kernel pages.)
Wow, how is this possible using layers? How does docker handle it if I subsequently modify one of the files of my layer in my container?
A running container is based on an immutable image and a single writable layer. That writable layer is unique to the container which contains all modifications made to the immutable image by processes running in the container.
Docker relies on the immutability of layers to share them between containers. This is not much different from how regular Linux processes all share the readonly contents of binaries and libraries that they load, while each process has its own private heap space that is not shared with other processes.
That means that deleting a file from a base layer, either when building an image or at runtime from the container, doesn't actually modify the contents of that layer. It only adds a tombstone marker to the writable layer, that indicates the file was deleted, and OverlayFS creates the illusion that the file no longer exists inside that container.
(The flipside is that deleting files from immutable layers doesn't actually free up space because the actual file contents don't go anywhere, but that's rarely a problem.)
Based on my understanding of cgroups, that seems unintuitive to me. Are you certain that's the case? I may try testing this out when I get a chance.
And when your distro doesn't update? When the distro is slow to change?
Flatpak, Appimage, Snaps (why?)... upstream devs are bypassing distro maintainers in a lot of cases.
In your particular example, if you are not happy with your distro, you are free to find another one. Some distros are extremely fast to change.
> upstream devs are bypassing distro maintainers in a lot of cases
IMO, for open source projects, upstream devs should not distribute their library. They should let package maintainers do it for their distro.
The strongest argument I can think of, is enabling system managers (distro maintainers, even end users) to update dependencies. This might be to apply a security patch, enable some kind of tracing for profiling an application, and so on.
I’m sure I don’t have the full picture since I’m far more ops than dev though.
CGroups is a deceptively powerful mechanism. You can isolate a process resource wise (X cores, Y amount of memory, Z amount of swap), network wise (a different virtual network adapter with its own IP, bandwidth limits, etc.) and FS wise (running in its own filesystem with devices it can see).
It can keep your system tidy by encapsulating elaborate stacks which makes system management painful, allows deterministic operation and image generation if you tag everything with version.
Downside is you can do bad things with it like terminating HTTPS with a gateway container and talk HTTP among your backend instead of configuring tools, or writing shoddy software, and getting away with it because it works, or gets automatically restarted when it crashes 6 times a day.
I don't run every service as a container, because some services suffocate when they are in a container, but for short running things which needs system-wide changes to function, or test-driving small services before fully committing, it's a good tool to have.
However, it's abused with no end, and their popular use leaves a bad taste in your mouth.
If you have a statically linked executable, setup the cgroup for it as you will. No containers needed. You can namespace is as well.
No `FROM X` `RUN Y` dockerfile stuff needed.
Yes, I run many programs inside cgroups, but not in containers.
> If you have a statically linked executable, setup the cgroup for it as you will. No containers needed. You can namespace is as well.
Yes.
> No `FROM X` `RUN Y` dockerfile stuff needed.
Yes. dockerfile only sets up the chroot in an overlayfs and fires up the "container" using mechanisms present in the kernel already.
As I said on another comment, quoting myself:
> Docker just made the interface more practical, and built the ecosystem around it.
Maybe, but it makes less sense at that point. If your doing Node/Ruby/Python/PHP then yes the container makes sense to drag your runtime to the server...
Do containers (docker) make sense for dev. Sure, to a point. Because our dev (win/Mac) might not look like our deploy (linux).... If we move to a standardized remote dev model then docker makes less and less sense.
>> It’s one I’ve asked before; have we just created a more elaborate statically-linked executable via containerization?
The bottles project only supports their app image, as they no longer want to be responsible for supporting the disttro maintained packaged version of their product.
Yes containers are becoming a way of dealing with linking, and dependency management. Its a blunt instrument for dealing with software packaging and distribution.
You might start off writing Go programs, but then need to run Postgres database for development. Or discover that need special library in some other language and easier to make its own service. Or need to run third-party service. With Docker, you run the image and don't care what's inside, and the isolation gives some assurance that won't escape.
Static vs dynamic linking is an implementation detail as far as I'm concerned. If all the dynamic libs needed were in a well-defined location it wouldn't matter that much.
As for where configuration and data live, that's always available in the docs, and Linux convention puts stuff in /etc, so I'm not sure how containers help. And dynamic libraries are in a well-defined location, with environment variables and other tools that allow you to specify where they live. It's not like dynamic linking is an unsolved problem.
There are just fewer things that can go wrong when you get to a sufficient number of services. And lastly moving to a new host is infinitely easier too, export the volume, import of new host and off you go. And stuff like Kubernetes will just handle this for you (and more).
And as for those linux conventions, they vary a lot from distro to distro, you can never be quite sure where that specific version of that specific distro puts its files. So having them just not be able to touch the host ever is a good thing.
As for your second paragraph, that's very idealistic. Config can live in /var, /usr, /home, /usr/local, literally anywhere. I find it much nicer when all data / configuration for a piece of software is all self contained.
I mean, there are some advantages, if they all share the same base layer, maybe they share those libs at least on disk via a shared layer. But practically, though you are maybe not back where you started, you are at a place that seems to share some similarities.
This is one of the biggest issues with containers IMO. This and the layering system, which I think is poorly designed both to configure and to actually do the tasks it's meant to(build and delivery caching).
The solutions to this problem in the space have basically been to provide scanners to crack open the containers and detect things with known vulnerabilities. But I have not seen (m)any solutions around these scanners to facilitate the lifecycle of landing fixes.
Even if you provided a tree of a-proved base containers for each deployment lang in your org, you can't just update the base and deploy the world, there's not even tooling to automate working over the "FROM hierarchy" of images where you could detect which need to use new bases.
Because of the difficulty in managing large container hierarchies, in some orgs the later drives a common methodology of making image tags mutable, i.e. `ruby:myorg-v2`, which makes the FROM more like a dynamic link reference that gets updated automatically on the next build. I view this workaround as a regression brought on by the _still_ incredibly poor and complex SDLC tooling around managing images.
I think it would be funny if we reverted back to s-expressions and started the cycle all over.
The interesting thing about how Nix approaches the problem is to replace the concept of FHS almost entirely (only a couple binaries are linked to /) by hijacking PATH, and the linker configs like you mentioned. The biggest difference being that the whole version-pinned dep tree is encoded in a nix package(and in the linker config of the binaries it produces) rather than just the package itself.
At some level you could say there is no "dynamic" runtime linking in nix, i.e the linker uses partially specified deps in a discovery phase, all of the link bindings happen at build time.
The FHS did attempt to solve the issue of multi-version dependencies with an interesting name and symlink setup, but they are usually still bound by fairly loose version constraints (like major version). Containers are a lot more like nix in this way, where deps are "resolved" at build time by the distro's package manager by virtue of controlling the process' filesystem.
This is one major issue with the reproducibility of container builds, the distro package managers are not deterministic, you could run a build back-to-back and get different deps depending on your timing(yes, even between test and build CI steps).
Containers also provide many forms of isolation (network, file system etc.), they provide a modern versioning and distribution scheme, composibility (use another container as a base image).
All of these things can, and perhaps should be, done at a the language level as well but containers also work across languages, across linking paradigms, and with existing binaries.
Otherwise, sure, nix can rewrite the RPATH in your ELF file to make it pull dynamic libs from the nix store, but what does it do when two processes both want to listen on ports 80 and 443?
Possibly, if the Internet ever actually goes pure IPv6, one LAN will have enough addresses to assign one to each process instead of each host.
There are, of course, other ways to handle it. People used vhosts predominantly defined in a dedicated web server that was really only a reverse proxy, but now you need nix and nginx. Then you discover you also want resource isolation. Is there a userspace alternative to cGroups? I don't see how there could even in principle be an alternative to PID/UID namespaces and UID/GID submapping. Some things have to happen in the kernel and that means containers of some sort. It doesn't have to be the exact OCI standard that eventually grew out of Docker and eventually Kubernetes, but some kind of container.
But, to play along with my static linking thought-exercise: if you take a process and put it in a network namespace then is it a container? I wouldn't say it is. Container runtimes might have a nice interface for namespacing, but namespacing something doesn't make it a container.
I guess my thought experiment is if things are statically linked binaries and you had a way to run them with the control group and namespace settings you wanted, would the packaging aspect of containers add anything?
The elites don't want you to know it, but namespaces are just there for the taking. You can grab as many as you want. You can set the memory limit on any process with cgroups, no docker desktop required. :)
Anyways, just a thought experiment about how the industry sometimes seems to be going in a circle, in the fashion of the lady who swallowed a fly.
The way they are used on GNU/Linux is indeed a solution to GNU/Linux's software distribution issues on a highly fragmented landscape.
Turns out that's exactly what containers are a packaging of! And I only found out about two years ago.
Although this article doesn't go into it, the benefits I've found of using containers rather than rolling isolation by hand is that a lot of semi-standardised monitoring, deployment, and workload management tooling expects things to come packaged as containers.
Well, no. When people say "containers", they always mean "Docker".
And Docker also comes with a daemon with full root permissions and ridiculous security policies. (Like, for example, forcefully turning off your machine's firewall, #yolo. WTF!)
P.S. I actually run systemd-nspawn in production, but I am probably the only person on earth to do so.
>By default, all external source IPs are allowed to connect to the Docker host. To allow only a specific IP or network to access the containers, insert a negated rule at the top of the DOCKER-USER filter chain.
Yikes. Should people read the docs? Yes. Should Docker not do this? Also yes.
Not really / not necessarily. https://github.com/opencontainers/runtime-spec
You're not alone, systemd-nspawn is very much underrated. I have used it a lot for machine containers, though I'm using podman+quadlet+systemd more right now. systemd-nspawn with mkosi for generating workload images is still a nice & powerful ecosystem.
Maybe you mean: "they always mean OCI images".
containerd without docker crio lxc windows containers podman
Mind sharing some good practical introduction article or set of articles for using VEs (virtual environments) with it? I'm tied to LXD at this moment which manages to provide both ease of operational and ease of configuration fine tunings be needed. I.e. I understand and tested for the projects I do taking care about on how to have network bridges, resources limiting, snapshot/rollback/create new image for VEs, storage profiles (say some I want to put on BTRFS some on ZFS some ...), simple `lxc ls` and `lxc shell <VE-name>` interfaces - may be systemd has all this kind of stuff as well. Or may be it shines in different area?
Apart from chroot, cgroups and namespaces, the containers are also build upon:
1) linux capabilities - that split the privileges of a root user into "capabilities" which allows limiting the actions a root user can do (see `man 7 capabilities`, `cat /proc/self/status | grep Cap` or `capsh --decode=a80425fb`)
2) seccomp - which is used to filter syscalls and their arguments that a process can execute. (fwiw Docker renders its seccomp policy based on the capabilities requested by the container)
3) AppArmor (or SELinux, though AppArmor is the default) - a LSM (Linux Security Module) used to limit access to certain paths on the system and syscalls
4) masked paths - container engines bind mounts certain sensitive paths so they can't be read or written to (like /proc/sysrq-trigger, /proc/irq, /proc/kcore etc.)
5) NoNewPrivs flag - while not enabled by default (e.g., in Docker) this prevents the user from gaining more privileges (e.g., suid binaries won't change the uid)
If anyone is interested in reading more about those topics and security of containers, you may want to read a blog post [0] where I dissected a privileged docker escape technique (note: with --privileged, you could just mount the disk device and read/write to it) and slides from a talk [1] I have given which details the Docker container building blocks and shows how we can investigate them etc.
[0] https://blog.trailofbits.com/2019/07/19/understanding-docker...
[1] https://docs.google.com/presentation/d/1tCqmGSOJJzi6ZK7TNhbz...
But once I went through that mental exercise I started reading code in containerd and cri-o. Wow, these are _not_ simple projects; containerd itself having a full GRPC-based service registry for driving dynamic logic via config.
One thing I was pretty disappointed about is how deeply ingrained OSI images are in the whole ecosystem. While you can replace almost all functional parts of runtime, but not really the concept of images. I think images are a poor solution to the problem they solve, and a big downside of this is a bunch of complexity in the runtimes trying to work around how images work (like remote snapshotters).
During the Viet Nam conflict, the Air Force needed to plan missions with multiple levels of classified data. This couldn't be done with the systems of that era. This resulted in research and development of multi-level security, the Bell-LaPadula model[2], and capability based security[1].
Conceptually, it's elegant, and requires almost no changes in user behavior while solving entire classes of problems with minimal code changes. It's a matter of changing the default from all access to no access, all the way down to the kernel.
[0] https://csrc.nist.rip/publications/history/ande72.pdf
[1] https://en.wikipedia.org/wiki/Capability-based_security
[2] https://en.wikipedia.org/wiki/Bell%E2%80%93LaPadula_model
https://www.sigarch.org/leave-your-os-at-home-the-rise-of-li...
Containers are basically monolithic kernels playing catching to the features designed into microkernel-based operating systems.
If I'm remembering correctly from when I ran through the instructions at home, it was written for the original cgroup sysfs interface rather than the more modern cgroup2 [0]. You can figure out which you're running with
> mount | grep cgroup > cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
which turns the examples is a nice "check your understanding"
[0]: https://docs.kernel.org/admin-guide/cgroup-v2.html#basic-ope...
https://frontendmasters.com/courses/complete-intro-container...
Note: if you look into the details of how setting up namespaces and cgroups works you'll run away in horror. The APIs are very iteratively evolved piecework, not really a coherent(ly designed) abstraction.
While you could schedule containers manually, or just run your application on VMs or hardware manually, something like kubernetes will let you define rules which it will dynamically evaluate against your infrastructure. You can instruct kubernetes to run your map reduce jobs on different nodes than your user-facing site... and you can give kubernetes an arbitrary number of nodes to work with, and it can scale your workloads for you automatically while also following your rules.
Using VMs with Kubernetes only makes sense when you need the strict isolation. If you are running own code, then containers are faster. Containers also perform better because they can share resources on host. In Kubernetes, containers can have minimum and maximum limits, which means they can dynamically use space not used by other containers. VMs need to be allocated memory when they start.
https://earthly.dev/blog/chroot/
Liz Rice has a good talk about the cgroups and namespaces.
It comes already with systemd (I know, cool, right?)
if you could have the automation / configuration of docker / podman for lxc that would have been nice.