Running the ODA File Converter in Docker
The converter is the only dependable route from arbitrary DWG into a Python pipeline, and it is a desktop application. Containerising it means installing the shared libraries it links against, giving it a virtual framebuffer because it initialises a graphical toolkit whatever the invocation, proving at container start that it actually converts, and supervising each run under a timeout. This page is part of DWG-to-Python Integration.
What Makes This Different From a CLI Tool
Three properties of the converter shape the image.
It is a graphical application. The command-line invocation drives the same binary a person would use interactively, and the binary initialises its toolkit during start-up. In a container with no display that initialisation fails, and the failure message is rarely about displays — it is a library load error or a silent non-zero exit. A virtual framebuffer resolves it.
It links against a desktop library stack. The package pulls in more than a headless base image carries, and a missing shared library produces the same class of unhelpful failure. This is why the health check has to convert rather than merely check that a file exists.
It is licensed. Free to use, with specific redistribution terms. An image containing it, pushed to a registry, is a distribution decision that belongs to whoever owns licensing rather than to whoever writes the Dockerfile.
Production-Ready Script
The container definition:
# A Debian base rather than Alpine: the converter links against glibc.
FROM debian:12-slim
ENV DEBIAN_FRONTEND=noninteractive \
DISPLAY=:99 \
ODA_BIN=/usr/bin/ODAFileConverter
RUN apt-get update && apt-get install -y --no-install-recommends \
xvfb libxcb-xinerama0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
libxcb-render-util0 libxkbcommon-x11-0 libglu1-mesa libfontconfig1 \
ca-certificates python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Install the converter package obtained under your own licence terms.
COPY vendor/ODAFileConverter.deb /tmp/
RUN dpkg -i /tmp/ODAFileConverter.deb || apt-get -fy install \
&& rm /tmp/ODAFileConverter.deb
COPY fixtures/minimal.dwg /opt/fixtures/minimal.dwg
COPY convert.py /opt/convert.py
# Fail the container at start if it cannot actually convert.
HEALTHCHECK \
CMD python3 /opt/convert.py --selftest || exit 1
ENTRYPOINT ["python3", "/opt/convert.py"]
And the supervisor it runs:
# Python 3.9+, ezdxf>=1.1.0 — /opt/convert.py
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
import ezdxf
ODA = os.environ.get("ODA_BIN", "/usr/bin/ODAFileConverter")
FIXTURE = Path("/opt/fixtures/minimal.dwg")
class ConversionError(RuntimeError):
pass
def _run(indir: Path, outdir: Path, *, version: str, timeout_s: int) -> None:
"""The seven positional arguments, under a virtual display and a timeout."""
cmd = [
"xvfb-run", "-a", "--server-args=-screen 0 1024x768x24",
ODA, str(indir), str(outdir), version, "DXF", "0", "1",
]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s)
except subprocess.TimeoutExpired as exc:
raise ConversionError(f"converter exceeded {timeout_s}s") from exc
if proc.returncode != 0:
tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-3:]
raise ConversionError(f"converter exited {proc.returncode}: {' | '.join(tail)}")
def convert(dwg: Path, out_dxf: Path, *, version: str = "ACAD2018",
timeout_s: int = 600) -> Path:
"""Convert one file and VERIFY the output — the exit code proves nothing."""
with tempfile.TemporaryDirectory() as tmp:
indir, outdir = Path(tmp) / "in", Path(tmp) / "out"
indir.mkdir(); outdir.mkdir()
shutil.copy2(dwg, indir / dwg.name)
_run(indir, outdir, version=version, timeout_s=timeout_s)
produced = list(outdir.glob("*.dxf"))
if not produced:
raise ConversionError(f"{dwg.name}: converter wrote no DXF")
doc = ezdxf.readfile(str(produced[0])) # opening it IS the verification
if sum(1 for _ in doc.modelspace()) == 0:
raise ConversionError(f"{dwg.name}: converted DXF has no modelspace entities")
out_dxf.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(produced[0], out_dxf)
return out_dxf
def selftest() -> int:
"""Convert a committed fixture. A binary that exists is not a binary that works."""
try:
with tempfile.TemporaryDirectory() as tmp:
convert(FIXTURE, Path(tmp) / "out.dxf", timeout_s=180)
except Exception as exc:
print(f"selftest FAILED: {exc}", file=sys.stderr)
return 1
print("selftest ok")
return 0
if __name__ == "__main__":
sys.exit(selftest() if "--selftest" in sys.argv else 0)
Key implementation notes:
xvfb-run -apicks a free display number, so several conversions can run on one host without colliding on:99.- The conversion is verified by opening the output and counting entities. A converter that exits zero having written an empty or missing file is a routine occurrence, not an edge case.
- The health check converts a fixture. It is the only check that exercises the display, the shared libraries and the licence together.
- Input and output directories are per-conversion temporaries. The converter takes directories, not files, and sharing one output directory across concurrent conversions is a race.
- The recovery-mode audit described in the DXF audit guide belongs immediately after this step, not inside it — conversion and structural repair are separate concerns.
Compatibility Matrix
| Component | Supported range | Notes |
|---|---|---|
| Base image | glibc-based (Debian, Ubuntu) | musl bases do not run the binary |
| Display | xvfb |
required even for command-line use |
| Converter | current releases | argument order stable; verify per version |
ezdxf |
>=1.1.0 |
output verification |
| Concurrency | one directory pair per conversion | shared output directories race |
Fallback Strategies
1. A library load error at start-up. A missing shared library. Run ldd on the binary inside the image and add what is reported missing; the list varies by converter release.
2. Converter exits zero, no output. The verification catches it. The usual causes are an unsupported source version and an output directory that does not exist — the converter creates neither.
3. Hangs on a specific file. The timeout catches it; quarantine that file and continue. A file that hangs the converter reliably is a support case, not a retry case.
4. Concurrency problems. Per-conversion temporary directories and xvfb-run -a handle most of it. Beyond a handful of parallel conversions per host, the converter itself becomes the bottleneck — scale across hosts rather than processes.
5. Licensing blocks the image. Mount the converter from a host path or a private volume instead of baking it into the image, so the image itself carries no redistributable component.
FAQ
Why does the converter need a display in headless mode?
Because it is a desktop application with a command-line invocation, not a command-line tool. It initialises its graphical toolkit during start-up regardless of how it was invoked, and without a display that initialisation fails — usually with an error that does not mention displays at all. A virtual framebuffer satisfies it and costs almost nothing.
Can I redistribute the converter in a container image?
That is a licensing question, not a technical one, and it has to be answered before the image is pushed anywhere. The converter is free to use and its redistribution terms are specific. Read them for the version you are packaging, and treat an image pushed to a shared registry as distribution.
What should the health check actually do?
Convert. Checking that the binary exists proves nothing — the failures in this setup are missing shared libraries and an unavailable display, both of which pass an existence check and fail on first use. Convert a tiny committed DWG fixture and assert the output opens, at container start, so a broken image never accepts work.
Related Pages
- DWG-to-Python Integration — parent workflow on the conversion hop and its alternatives
- Batch Converting DWG to DXF with the ODA File Converter — the seven positional arguments this container runs
- Auditing and Repairing DXF Files with ezdxf — the gate the converted output should pass next