Genace

AI generation API Docker: inject secrets when the worker starts

·

A container can make a generation worker reproducible, but it does not make API submissions restart-safe. Treat the container image as code and the runtime as the place for credentials and persistent job state. This tutorial packages a small image submission worker whose accepted response is written to a mounted directory.

1. Keep credentials outside the build context

Create a directory containing worker.mjs, Dockerfile, and .dockerignore. The worker uses native fetch and writes a minimal recovery manifest for a valid 202 or a recognized submission 502 with a known job ID. Observation is a separate command or service.

import { writeFile } from 'node:fs/promises';

let evidence;
try {
  const key = process.env.GENACE_API_KEY;
  if (!key) throw new Error('Missing GENACE_API_KEY');
  const response = await fetch('https://genace.ai/api/v1/images/generations', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${key}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'nano-banana',
      prompt:
        process.env.IMAGE_PROMPT || 'A minimal ink drawing of a pine tree',
      n: 1,
    }),
    signal: AbortSignal.timeout(45000),
  });
  const data = await response.json();
  if (
    response.status === 202 &&
    typeof data?.job_id === 'string' &&
    data.job_id.trim() &&
    ['queued', 'running', 'succeeded', 'failed', 'canceled'].includes(
      data.state
    )
  ) {
    evidence = { job_id: data.job_id, state: data.state };
  } else {
    const error = data?.error;
    const outcome =
      error?.code === 'UPSTREAM_SUBMISSION_FAILED'
        ? 'rejected'
        : error?.code === 'UPSTREAM_SUBMISSION_UNCERTAIN'
          ? 'uncertain'
          : null;
    if (
      response.status !== 502 ||
      !outcome ||
      typeof error.job_id !== 'string' ||
      !error.job_id.trim()
    )
      throw new Error('Unrecognized submission response');
    evidence = {
      job_id: error.job_id,
      error_code: error.code,
      submission_outcome: outcome,
    };
  }
  await writeFile(
    `/data/${encodeURIComponent(evidence.job_id)}.json`,
    JSON.stringify(evidence),
    { mode: 0o600 }
  );
  console.log(JSON.stringify(evidence));
  if (!response.ok) process.exitCode = 1;
} catch {
  console.error(
    JSON.stringify({
      message: 'Submission or persistence failed; reconcile before retry',
      ...(evidence ? { job_id: evidence.job_id } : {}),
    })
  );
  process.exitCode = 1;
}

Known UPSTREAM_SUBMISSION_FAILED and UPSTREAM_SUBMISSION_UNCERTAIN responses can both contain error.job_id. The manifest keeps that ID even though the container exits unsuccessfully; the local submission_outcome field is not a new Genace response field. Rejected does not prove compensation has settled. Uncertain acceptance must not trigger automatic refunds or cancellation attempts that bypass reconciliation. Raw response messages and unrelated fields are omitted.

Encoding the ID keeps URL separators out of the filename; it does not make the ID trustworthy or authorize access. A failed write emits only the known ID and a controlled diagnostic, without retrying the POST. That log is a recovery aid, not durable persistence. writeFile completion is not an atomic database commit or crash/power-loss guarantee, and file permissions still require a reviewed host/container policy. Test the actual bind mount before any paid submission.

FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node worker.mjs ./worker.mjs
USER node
CMD ["node", "worker.mjs"]
.env
.env.*
node_modules
result
.git

The explicit COPY includes only the worker file, while the ignore file prevents accidental inclusion if the Dockerfile grows later. Do not put the API key in an ARG, ENV, or build command. Build metadata and image layers are the wrong place for a runtime secret.

2. Run with a persistent output directory

mkdir -p result
docker build -t genace-worker .
docker run --rm \
  --env GENACE_API_KEY \
  --env IMAGE_PROMPT='A studio photo of an amber glass vase' \
  --mount type=bind,source="$PWD/result",target=/data \
  genace-worker

The host directory must be writable by the container’s non-root user. Configure its ownership or permissions for your operating system before running a paid request. Verify the mount first with a harmless filesystem operation, because an accepted generation followed by a write permission failure leaves the job running without the intended local manifest.

3. Plan for restart behavior

Do not attach an unconditional restart policy to a one-shot submission container. Every restart re-executes the POST. A timeout or crash after remote acceptance does not prove no job exists, and the generation route does not publish request idempotency support.

For a durable worker, store an application operation record before dispatch, save the remote job_id immediately after acceptance, and implement explicit reconciliation for uncertain outcomes. When a known ID exists, restart in observation mode and request GET /api/v1/jobs/{id}?wait=30. A status response uses id and may contain null outputs until completion.

Common mistakes and verification

Never assume that deleting a container cancels its remote jobs. Container lifetime, observation deadline, and generation state are different things. Also avoid relying on container logs as your only state store; log rotation and infrastructure replacement can remove the reference you need.

Verify the built image contains no environment file, the runtime fails clearly when the key is missing, and the mounted directory is writable. Inspect a synthetic manifest before a live run. After an intentional generation, confirm the accepted ID persists after container removal and can be used by a separate status client.

See server key handling, environment configuration, and the Node.js polling example for the pieces surrounding this container.

Continue with code and integration

Models covered on this page

Where these facts come from