💡 Full working example available on GitHub:
sign-documents-in-docker-fonts-java

El Servicio de Firma de Contratos que Funcionó durante Nueve Meses

El aprovisionamiento de fuentes en el contenedor es el paso que decide si un servicio de firma Java funciona en producción o solo en las pruebas que casualmente escribiste. Importa porque el fallo está programado: una imagen JRE te brinda suficiente cobertura de fuentes para que se vea correcta, y luego retiene el resto hasta que llega un documento específico.

Consideremos su forma. Un flujo de trabajo de documentos firma contratos, desplegado en eclipse-temurin:17-jre, y funciona. Nueve meses después, la empresa firma a su primer cliente en Japón, el nombre aparece en el texto de la firma y el trabajo falla con Specified font file was not found. No cambió nada en el servicio. La imagen nunca tuvo cobertura CJK; ningún documento la había solicitado.

La causa técnica es corta. eclipse-temurin:17-jre incluye 8 archivos de fuentes DejaVu para AWT, que cubren Latin, Greek y Cyrillic. GroupDocs.Signature no sustituye una familia faltante, por lo que una solicitud de una fuente compatible con japonés falla en lugar de degradarse, y dejar la fuente sin establecer no ayuda porque la biblioteca entonces solicita Times New Roman, que también está ausente.

Por Qué Esto es Peor que una Imagen sin Fuentes

Las imágenes base de .NET y Python no incluyen fuentes. Ese es un fallo mejor: la primera firma falla, en la primera ejecución de prueba, y alguien lo corrige antes de que el servicio se envíe.

Una imagen JVM falla parcialmente, lo que es la versión costosa. El error vive en código que ya está en producción, se desencadena por datos del cliente y no por nada en el despliegue, y la persona de guardia ve un error de fuente de un servicio que nadie ha tocado en meses. El costo del incidente no es la corrección — la corrección es una capa del Dockerfile — sino la hora antes de que alguien crea que las fuentes están involucradas.

Esa asimetría es el argumento para tratar la cobertura de fuentes como algo que se afirma al iniciar en lugar de algo que se descubre.

También cambia quién paga. Una imagen sin fuentes le cuesta a un desarrollador veinte minutos durante la configuración. Una imagen parcialmente cubierta le cuesta a un ingeniero de guardia una hora en un momento inoportuno, más lo que valía el contrato retrasado, más la revisión que sigue a un incidente que nadie puede atribuir a un cambio. La diferencia técnica entre ambas son cuatro paquetes en un Dockerfile.

Qué Cuesta Realmente el Aprovisionamiento

Cuatro paquetes Debian en la etapa de ejecución:

RUN apt-get update && apt-get install -y --no-install-recommends \
        fontconfig \
        fonts-dejavu-core \
        fonts-liberation \
        fonts-noto-cjk \
    && fc-cache -f \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

El tamaño de la imagen es la objeción habitual, y vale la pena ser específico: el paquete CJK es el grande, los otros tres son pequeños, y ninguno es opcional si tus documentos pueden contener nombres no latinos. Instala lo que realmente necesita tu conjunto de documentos y verifica con una lectura de vuelta en lugar de recortar por instinto.

fontconfig es el resolvedor más fc-list para depuración. fonts-dejavu-core duplica lo que el JRE ya incluye, lo cual es deliberado: mantiene la imagen honesta si la imagen base cambia. fonts-liberation importa porque los documentos creados en Windows hacen referencia a Arial y Times New Roman por nombre y esperan una renderización métrica compatible. fonts-noto-cjk es el que el incidente anterior necesitó.

Resolver una Familia en Lugar de Nombrar una

El aprovisionamiento solo no es suficiente, porque el código aún tiene que nombrar una familia que exista. La forma portátil es preguntar a la biblioteca: intentar una firma desechable por cada candidato, conservar la primera que no lance excepción.

for (String candidate : candidates) {
    if (tryFamily(sourcePath, candidate) == null) {
        return candidate;
    }
}
return null;

La prueba en sí es una llamada de firma ordinaria al directorio temporal, con el fallo convertido en un valor en lugar de una excepción:

SignatureFont font = new SignatureFont();
font.setFamilyName(familyName);
font.setSize(10);
options.setFont(font);
signature.sign(scratch.getAbsolutePath(), options);
return null;

La detección por nombre de archivo es el atajo que parece equivalente y no lo es. fonts-noto-cjk de Debian instala NotoSansCJK-Regular.ttc, cuyo nombre de familia es Noto Sans CJK JP, por lo que coincidir nombres de archivo tanto pierde fuentes como reporta familias que no se resolverán.

Degradar Honestamente

Con la resolución en su lugar, las dos clases de fallo se separan limpiamente. No hay familia Latin significa que la imagen no puede firmar en absoluto, lo que debería detener el contenedor. No hay familia CJK significa que se omite una firma y la ejecución continúa con una advertencia:

List<SignOptions> options = new ArrayList<>();
options.add(buildTextOptions(LATIN_TEXT, latinFamily, 50));

if (cjkFamily != null) {
    options.add(buildTextOptions(CJK_TEXT, cjkFamily, 120));
}

SignResult result = signature.sign(outputPath, options);

La distinción importa operativamente. Un contenedor que sale al iniciar con “no usable font family” es un problema de despliegue, detectado por quien lo desplegó. Una firma que falta silenciosamente en un documento entregado es un problema de cumplimiento, detectado por el destinatario. Encauzar el caso fatal a una salida distinta de cero mantiene los fallos en la primera categoría.

Luego lee el resultado, porque una firma CJK escrita sin cobertura CJK puede renderizarse como cajas vacías sin generar ningún error:

TextSearchOptions options = new TextSearchOptions();
options.setAllPages(true);
List<TextSignature> found = signature.search(TextSignature.class, options);

¿Dónde Deja Esto a un Equipo que ya Desplegó?

Añade la capa de fuentes, añade la resolución al iniciar y registra ambos resultados en la primera línea del servicio, donde el siguiente ingeniero realmente los verá. El cambio es una edición del Dockerfile más o menos treinta líneas, y convierte un incidente desencadenado por el cliente en un contenedor que o bien arranca con cobertura conocida o se niega a arrancar. Los documentos ya firmados no se ven afectados; solo los nuevos ganan la ruta CJK.

Verificando una Imagen que ya Ejecutas

Antes de cambiar nada, vale la pena saber qué contiene tu imagen actual. Dos comandos lo responden desde fuera:

docker run --rm your-image sh -c "ls -R /usr/share/fonts | head"
docker run --rm your-image sh -c "fc-list : family | sort -u | head -20"

El primero lista los archivos de fuentes, el segundo lista los nombres de familia que devolvería un resolvedor, y la brecha entre ambos es la razón por la que falla la coincidencia por nombre de archivo. Si fc-list falta, esa es su propia respuesta: fontconfig no está instalado, y cualquier búsqueda de familia se ejecuta a ciegas.

Dentro del servicio, la comprobación equivalente pertenece al registro de inicio junto a las familias resueltas. Una línea que diga fonts on disk: 8, latin: DejaVu Sans, cjk: (none) le dice a la siguiente persona exactamente qué puede y qué no puede firmar este contenedor, lo cual es más útil que cualquier excepción que de otro modo leerían a las tres de la mañana.

El Detalle de la JVM que Nadie Espera

Una cosa más que muerde específicamente en Java, y no tiene que ver con fuentes. El artefacto Maven de GroupDocs es un jar gordo firmado. Reempaquetarlo en un jar sombreado produce NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, y el remedio habitual de eliminar META-INF/*.SF|RSA|DSA es insuficiente: MANIFEST.MF lleva alrededor de 19 MB de digestiones por entrada y también debe truncarse a su sección principal. El ejemplo evita el problema ejecutándose contra un classpath plano con un directorio dependency/ en lugar de sombrear nada.

Lo menciono porque ambos —la cobertura parcial de fuentes y el jar firmado— comparten una forma: la ruta JVM falla de una manera que parece tu código y no lo es. Ambos también son baratos de defender una vez nombrados: fija la disposición del classpath que sabes que funciona, y afirma la cobertura de fuentes al iniciar en lugar de confiar en la imagen base. Ninguno cuesta un rediseño, y ambos eliminan una clase de incidentes que de otro modo es indistinguible de un bug de aplicación.

Conclusión

Un servicio de firma Java en un contenedor está a una capa del Dockerfile y una verificación al iniciar de distancia de ser predecible. Instala fontconfig, DejaVu, Liberation y Noto CJK; resuelve la familia mediante sondeo en lugar de suposición; omite lo que no pueda incrustarse; verifica leyendo de vuelta. El repositorio de ejemplo envía ambas imágenes, de modo que la diferencia entre cobertura y sin cobertura requiere dos compilaciones para verse en lugar de un incidente para aprender.

Recursos Adicionales