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.

Three properties that shape the image Three characteristics of the converter and what each one requires of a container. It is a graphical application that initialises a toolkit whatever the invocation, it links against a desktop library stack, and it carries specific redistribution terms. The first two produce unhelpful failures; the third is not a technical question at all. Property Requires Failure if absent Graphical toolkit a virtual framebuffer unrelated-looking error Desktop library stack glibc base + libraries library load failure Licensed reading the terms not a technical failure The first two pass an "is the binary there?" check and fail on first use.

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:

Why the health check converts a fixture Two candidate health checks. Testing that the binary exists passes in an image with no display and no shared libraries, which are exactly the two ways this setup fails. Converting a small committed fixture exercises the display, the libraries and the licence together, and fails at container start rather than on the first real delivery. Binary exists — passes with no display — passes with missing libraries — passes without a licence — proves nothing Convert a fixture — exercises the framebuffer — exercises every library — exercises the licence — fails at start, not at work A broken image should never accept a job.
# 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 --interval=60s --timeout=120s --start-period=30s --retries=2 \
  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 -a picks 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.

Supervising one conversion Four stages per file. Fresh input and output directories are created because the converter takes directories rather than files and a shared output directory races. The converter runs under a virtual display and a timeout. The output is opened and its entities counted — the verification — and only then copied to the destination. Fresh directories per conversion 1 the converter takes directories Run under xvfb and a timeout 2 a hang otherwise never ends Open the output count entities 3 the exit code proves nothing Publish only if verified 4 audit comes next, separately

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.