Debian-based production containers
Table of Contents
Some of my services need libraries or tools that are only well-packaged on Debian. I want Debian slim containers with the same defaults as my Alpine ones: correct timezone, no unnecessary packages pulled in, and easy user setup.
1. Disabling automatic recommends
Debian's apt installs recommended packages by default, which bloats
images. I disable this globally so every apt install stays lean.
@function
def debian_no_auto_install(self, ctr: dagger.Container) -> dagger.Container:
"""Disable apt recommends and suggests."""
return ctr.with_exec([
"sh", "-c",
'echo \'APT::Install-Recommends "0";\' > /etc/apt/apt.conf.d/01norecommend'
" && "
'echo \'APT::Install-Suggests "0";\' >> /etc/apt/apt.conf.d/01norecommend',
])
2. Setting the timezone on Debian
Debian ships timezone data by default, so it's simpler than Alpine: just symlink the right zoneinfo file.
@function
def debian_set_tz(self, ctr: dagger.Container) -> dagger.Container:
"""Set timezone on a Debian container (uses Lib.timezone)."""
return (
ctr
.with_exec(["rm", "/etc/localtime"])
.with_exec(["ln", "-sf", f"/usr/share/zoneinfo/{self.timezone}", "/etc/localtime"])
)
3. Cleaning up apt caches
After installing packages, I clean up to keep the image small.
@function
def debian_apt_cleanup(self, ctr: dagger.Container) -> dagger.Container:
"""Clean apt caches."""
return ctr.with_exec([
"sh", "-c",
"apt-get --quiet clean && rm -rf /var/lib/apt/lists/*",
])
4. A base Debian container
This is the Debian equivalent of my Alpine target: slim image with timezone set, no recommends, and optional extra packages.
@function
def debian(self, distro_packages: list[str] = ()) -> dagger.Container:
"""Debian slim with timezone set, no auto-install, and optional extra packages."""
ctr = dag.container().from_(self.pinned(self._debian_image))
ctr = self.debian_no_auto_install(ctr)
ctr = self.debian_set_tz(ctr)
if distro_packages:
packages_str = shlex.join(distro_packages)
ctr = ctr.with_exec([
"sh", "-c",
"{ apt-get --quiet update"
f" && apt-get --quiet install --yes {packages_str}"
" ; } > /tmp/log 2>&1 || { cat /tmp/log; exit 1; }",
])
ctr = self.debian_apt_cleanup(ctr)
return ctr
dagger call debian with-exec --args="sh","-c","readlink -f /etc/localtime" stdout
dagger call debian with-exec --args="cat","/etc/apt/apt.conf.d/01norecommend" stdout
dagger call debian --distro-packages=curl with-exec --args="which","curl" stdout
5. Debian with a default user
Same pattern as Alpine: a non-root user ready to go.
@function
def debian_user(self, distro_packages: list[str] = ()) -> dagger.Container:
"""Debian with a default user."""
ctr = self.debian(distro_packages=distro_packages)
return self.use_user(ctr)
dagger call debian-user with-exec --args="id","sam" stdout
6. Python with a user and virtualenv on Debian
For Python services that need Debian-specific libraries (e.g., for native extensions), I want the same venv setup as Alpine but on Debian.
@function
def debian_python_user_venv(
self,
distro_packages: list[str] = (),
groups: list[str] = (),
pip_packages: list[str] = (),
work_dir: str = "/app",
) -> dagger.Container:
"""Debian with python, user, and a virtualenv."""
ctr = self.debian(distro_packages=["python3-venv"] + list(distro_packages))
return self.python_user_venv(ctr, groups=groups, pip_packages=pip_packages, work_dir=work_dir)
dagger call debian-python-user-venv with-exec --args="sh","-c","test -d /app/venv && /app/venv/bin/python --version" stdout
7. Extracting the localtime file
Distroless containers can't install packages. I extract the timezone file from the existing debian() container so I can copy it into distroless images.
.file(path) extracts a single file from the container as a File
artifact. Since debian() already sets the timezone, we just pull
/etc/localtime from it — no need to create a separate container.
@function
def debian_localtime(self) -> dagger.File:
"""Extract the localtime file from a Debian container."""
return self.debian().file("/etc/localtime")
TMP="${TMP:-$(mktemp -d)}"
dagger call debian-localtime export --path="$TMP/out/localtime" > /dev/null
test -s "$TMP/out/localtime" && echo "non-empty"
8. Image reference for pin management
See alpine for the convention.
@property
def _debian_image(self) -> str:
return f"debian:{self.debian_tag}"