top of page

How to Build Production-Ready Docker Images for AI Applications


An AI API that works in a local Python environment is not automatically ready to run as a production container. The image may contain build tools, credentials, stale packages, unnecessary model files, or an application process running as root. It may also lack a reliable health check, graceful shutdown behavior, or enough metadata to identify what was deployed.



This guide builds a hardened image for a synthetic document-summarization service named document-insight-api. Docker Desktop is used to build, inspect, run, and scan the image locally. The final section shows how the same verified image should move into Amazon Elastic Container Registry (Amazon ECR) and an AWS container runtime without rebuilding it.



The sample API returns a deterministic summary and does not download a model or call a paid inference service. That keeps the container workflow reproducible. A real application can replace the synthetic processor with Amazon Bedrock, a SageMaker endpoint, or a locally hosted model after applying the model-specific controls described later.




What You Will Build



The tutorial produces one versioned Linux container image with these properties:


  • A multi-stage Dockerfile separates dependency building from runtime.


  • Production dependencies are locked and verified.


  • Only application and runtime files enter the final image.


  • The service runs as a fixed non-root user.


  • No credentials or environment-specific secrets are stored in the image.


  • A health check reports whether the process can serve requests.


  • Runtime writes are restricted to an explicit temporary filesystem.


  • Linux capabilities are removed for the local verification run.


  • Docker Scout analyzes the image for known package vulnerabilities.


  • The image is tagged with a release version and promoted by digest.


The local workflow is:



Source and dependency lock
        ↓
Docker BuildKit checks
        ↓
Multi-stage image build
        ↓
Local image inspection
        ↓
Restricted container run
        ↓
Health and API verification
        ↓
Vulnerability review
        ↓
Immutable registry promotion




Why AI Container Images Need Additional Controls



AI containers often become large and operationally complex because they may include native libraries, tokenizers, GPU runtimes, model weights, vector-search clients, and framework caches. A straightforward COPY . . followed by pip install can accidentally ship notebooks, datasets, credentials, test output, package caches, and build compilers.



Production concerns extend beyond image size:


  • A base image or Python wheel may contain a newly disclosed vulnerability.


  • A model downloaded at startup may change without an application code change.


  • Large model initialization may cause an orchestrator to fail health checks too early.


  • The application may need read-only access to model artifacts but no access to AWS administration APIs.


  • GPU and CPU images may require different architectures and native libraries.


  • Prompt, model, and dependency versions must remain traceable to the deployed image digest.


The target is not the smallest image at any cost. It is a focused, reproducible, inspectable image with a documented runtime contract and an acceptable, reviewed risk profile.





Target Architecture



Docker Desktop provides the local image store, container runtime, and image inspection interface. Docker Scout supplies local package and vulnerability analysis. Amazon ECR becomes the enterprise registry, while the selected AWS runtime supplies networking, identity, scaling, logging, and orchestration.



The image is built once. Staging and production should reference the same image digest. Environment variables, runtime IAM roles, secrets, scaling, and endpoints change outside the image.





Prerequisites



Prepare the following:


  • Docker Desktop configured to run Linux containers.


  • A Docker Engine and BuildKit version that supports build checks and secret mounts.


  • At least 4 GB of free local memory for the lightweight sample; real local-model images may require substantially more.


  • Python familiarity sufficient to review a small FastAPI service.


  • A private source repository for aws-production-ai-container.


  • An approved dependency-locking process.


  • Optional for the AWS handoff: an AWS account, AWS CLI, a private ECR repository, and scoped push permissions.


Before capturing evidence, record the actual versions:


docker version
docker buildx version
docker scout version


Do not copy workstation usernames, registry credentials, Docker account details, private repository names, or unrelated local images into screenshots.





Step 1: Define the Container Runtime Contract



Decide what the image must do before writing the Dockerfile. For this tutorial, the contract is:



Requirement

Decision

Service

HTTP API for synthetic document summarization

Container port

8080

Liveness endpoint

GET /healthz

Readiness endpoint

GET /readyz

Runtime user

UID and GID 10001

Required write path

/tmp only

Configuration

Environment variables supplied at runtime

Secrets

Runtime secret provider; never stored in the image

Shutdown

Process receives SIGTERM directly

Image identity

Version tag plus immutable digest



Liveness and readiness answer different questions. Liveness means the process is responding. Readiness means the service has completed initialization and can accept traffic. Dockerfiles provide one HEALTHCHECK; an orchestrator such as Amazon ECS or Kubernetes should apply separate health and traffic-readiness behavior appropriate to the application.





Step 2: Create a Minimal, Observable AI API



Use this repository structure:



aws-production-ai-container/
├── app/
│   ├── __init__.py
│   └── main.py
├── tests/
│   └── test_api.py
├── Dockerfile
├── .dockerignore
├── requirements.lock
├── requirements-dev.lock
└── README.md


The sample API validates input, returns a deterministic response, and exposes health endpoints without logging document contents:



# app/main.py
import logging
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

logging.basicConfig(
    level=os.getenv("LOG_LEVEL", "INFO"),
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger("document-insight-api")
ready = False


@asynccontextmanager
async def lifespan(_app: FastAPI):
    global ready
    # Initialize an approved model client or load a versioned model here.
    ready = True
    logger.info("application_ready=true")
    yield
    ready = False


app = FastAPI(title="Document Insight API", lifespan=lifespan)


class SummaryRequest(BaseModel):
    text: str = Field(min_length=1, max_length=20_000)


@app.get("/healthz")
def health() -> dict[str, str]:
    return {"status": "alive"}


@app.get("/readyz")
def readiness() -> dict[str, str]:
    if not ready:
        raise HTTPException(status_code=503, detail="starting")
    return {"status": "ready"}


@app.post("/summarize")
def summarize(request: SummaryRequest) -> dict[str, str]:
    words = request.text.split()
    return {
        "model": os.getenv("MODEL_ID", "synthetic-summary-v1"),
        "summary": " ".join(words[:30]),
    }


Pin the production dependency graph in requirements.lock. Prefer a lock file with hashes and review transitive dependencies as well as direct dependencies. Keep test tools in requirements-dev.lock; they do not belong in the production runtime image.




Step 3: Exclude Files That Do Not Belong in the Build Context



Create .dockerignore before the first build. It reduces build context, avoids unnecessary cache invalidation, and lowers the risk of copying local-only files.



.git
.github
.idea
.vscode
.venv
venv
__pycache__
*.py[cod]
*.log
*.md
.env
.env.*
credentials*
secrets*
notebooks/
datasets/
models/
tests/
reports/
dist/
build/


Review exclusions against the application. For example, do not ignore models/ if the approved deployment strategy intentionally embeds a small, licensed, versioned model. Conversely, never allow a broad COPY . . to pull a local model cache or dataset into the image accidentally.



Use BuildKit secret mounts if a private package repository requires authentication during the build. Docker’s build secret documentation warns against passing credentials through build arguments or environment variables because those values can persist in image metadata or layers.





Step 4: Build a Multi-Stage, Non-Root Runtime Image



Use a multi-stage Dockerfile. The builder resolves and prepares packages; the final stage contains only the Python runtime, installed dependencies, and application code.



# syntax=docker/dockerfile:1

ARG PYTHON_IMAGE=python:3.13-slim

FROM ${PYTHON_IMAGE} AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /build
COPY requirements.lock ./

RUN python -m venv /opt/venv \
    && /opt/venv/bin/python -m pip install --upgrade pip \
    && /opt/venv/bin/python -m pip install --require-hashes -r requirements.lock


FROM ${PYTHON_IMAGE} AS runtime

ARG BUILD_REVISION="unknown"
LABEL org.opencontainers.image.title="document-insight-api" \
      org.opencontainers.image.description="Synthetic AI summarization API" \
      org.opencontainers.image.revision="${BUILD_REVISION}" \
      org.opencontainers.image.source="<VERIFIED_REPOSITORY_URL>"

ENV PATH="/opt/venv/bin:${PATH}" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    APP_ENV=production \
    PORT=8080

RUN groupadd --gid 10001 appgroup \
    && useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin appuser

WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY --chown=10001:10001 app ./app

USER 10001:10001
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2)"]

STOPSIGNAL SIGTERM
ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "1"]


The final image intentionally excludes the test suite, package cache, local notebooks, datasets, and build environment. The numeric USER allows deployment policies to verify that the application is not configured to run as root. The exec-form ENTRYPOINT lets the server process receive termination signals directly.



The Docker documentation recommends trusted and appropriately small base images, multi-stage builds, .dockerignore, version pinning, and non-root users when privileges are unnecessary. See Docker build best practices and the Dockerfile reference.



For a production release, resolve the base tag to an approved digest and record the update process. Tags are readable but mutable; digests provide an immutable base reference. Rebuild regularly so security fixes in the approved base and dependencies reach the application.





Step 5: Check and Build the Image Locally



Start Docker Desktop and wait for the engine to become ready. From the repository root, run Dockerfile checks before the build:



docker build --check .


Then build a versioned image and attach the source revision as OCI metadata:



docker build --pull --build-arg BUILD_REVISION=<GIT_COMMIT_SHA> --tag document-insight-api:1.0.0 .


Do not use latest as the only release identity. A semantic release tag helps people, while the resulting image digest identifies the exact content.



Inspect the important configuration:



docker image inspect document-insight-api:1.0.0 --format '{{json .Config.User}}'
docker image inspect document-insight-api:1.0.0 --format '{{json .Config.Healthcheck}}'
docker image inspect document-insight-api:1.0.0 --format '{{json .Config.Labels}}'


Expected evidence:


  • The build completes without Dockerfile check errors.


  • The final image is tagged document-insight-api:1.0.0.


  • The configured user is 10001:10001.


  • The image contains the health check and source-revision label.


  • Image history does not show secret values or an unintended COPY . . layer.





Step 6: Run the Container With Production-Like Restrictions



Run the container without root privileges, additional Linux capabilities, or a writable root filesystem:



docker run --detach --name document-insight-api --publish 127.0.0.1:8080:8080 --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m --cap-drop ALL --security-opt no-new-privileges --memory 512m --cpus 1.0 --env APP_ENV=local --env MODEL_ID=synthetic-summary-v1 document-insight-api:1.0.0


Binding to 127.0.0.1 keeps the tutorial service local to the workstation. The read-only root filesystem helps reveal accidental runtime writes. /tmp is the only temporary writable location supplied to the sample.



Test health, readiness, and one synthetic request:



Invoke-RestMethod http://127.0.0.1:8080/healthz
Invoke-RestMethod http://127.0.0.1:8080/readyz

$request = @{ text = 'Synthetic document text used to verify the container API.' } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8080/summarize -ContentType 'application/json' -Body $request


Inspect the runtime identity and health state:



docker inspect document-insight-api --format '{{json .State.Health}}'
docker exec document-insight-api id
docker logs document-insight-api


The container should report healthy, the runtime identity should be UID/GID 10001, and the API should return a synthetic summary without logging the submitted text.

Stop the container normally and confirm the process terminates within the expected grace period:



docker stop document-insight-api


TODO: VERIFY the health transition, API contract, non-root identity, read-only filesystem compatibility, resource constraints, log redaction, and graceful shutdown.





Step 7: Scan the Image and Set a Release Policy



Open Docker Desktop, go to Images, and select document-insight-api:1.0.0. Docker Scout can generate an SBOM and show packages, vulnerabilities, affected layers, and available remediation guidance in the image details view.



Run a CLI summary as reproducible evidence alongside the Desktop view:



docker scout quickview document-insight-api:1.0.0
docker scout cves document-insight-api:1.0.0

Define the release policy before reading the results. A starting policy might require:


  • No unreviewed critical vulnerabilities with a fix available.


  • High-severity findings either remediated or documented with exploitability, compensating controls, owner, and expiry.


  • Base-image and direct-dependency updates evaluated separately.


  • An SBOM retained with the release evidence.


  • A rescan before production because vulnerability intelligence changes after the image is built.


Vulnerability count alone is not a risk decision. Review whether the affected package is present in the runtime path, whether the vulnerable code is reachable, whether a fix exists, and whether the application has compensating controls. Do not hide findings simply to make a dashboard green.



Docker documents that its image details view exposes image hierarchy, layers, packages, vulnerabilities, and remediation recommendations. See the Docker Scout image details documentation.






Step 8: Promote the Verified Image to Amazon ECR



Local validation is a release gate, not the production registry. Create or select a private ECR repository named document-insight-api with encryption, lifecycle rules, an approved scanning configuration, and immutable release tags.



Authenticate Docker using a short-lived ECR authorization token, tag the already verified local image, and push it:



aws ecr get-login-password --region <AWS_REGION> | docker login --username AWS --password-stdin <ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com
docker tag document-insight-api:1.0.0 <ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/document-insight-api:1.0.0
docker push <ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/document-insight-api:1.0.0

The Amazon ECR push documentation notes that registry authentication tokens are time-limited. Do not store the resulting registry credential in the repository, Dockerfile, CI variables visible to untrusted jobs, or screenshots.



After the push:


  1. Record the ECR image digest.


  2. Confirm that it matches the promoted local content.


  3. Run the configured ECR vulnerability scan or continuous scan.


  4. Make the AWS deployment reference the digest, not a mutable tag.


  5. Retain the source revision, lock-file checksum, build record, SBOM, scan decision, and image digest together.


ECR supports tag immutability so an existing release tag cannot be silently overwritten. Review the current ECR tag immutability options when configuring the repository.



No AWS Console screenshot is required for this article. Use the registry digest and redacted CI or CLI output as publication evidence.





Verify the Complete Image Workflow



Record evidence for both expected success and meaningful failure paths:



Control

Test

Expected result

Dockerfile checks

Run BuildKit checks

No blocking Dockerfile findings

Reproducible build

Build twice from the same reviewed inputs in a controlled environment

Inputs and resulting identity are explainable; non-determinism is investigated

Non-root runtime

Run id inside the container

UID/GID is 10001, not root

Read-only root

Run with --read-only

API works using only declared writable mounts

Health failure

Temporarily test a deliberately invalid health target in a disposable tag

Container becomes unhealthy and evidence is visible

Secret exclusion

Inspect history, environment, files, and build context

No credentials or sensitive local files are present

Vulnerability gate

Run Docker Scout against the final tag

Findings meet the documented release policy

Graceful stop

Stop the running container

Application exits cleanly within the grace period

Registry integrity

Push once and compare digests

Deployment references the approved ECR digest

Immutable tag

Attempt to overwrite a disposable immutable ECR tag

Registry rejects the overwrite



The checks demonstrate the image’s local construction and runtime behavior. They do not prove application correctness under load, model quality, data compliance, network isolation, or suitability for a particular AWS runtime.





Production Considerations



Base Images and Dependency Governance



Use a trusted base, pin its digest for releases, and define who reviews updates. A digest prevents unexpected movement but also prevents automatic security fixes, so combine pinning with scheduled rebuilds and vulnerability monitoring.



Generate the dependency lock and hashes through an approved process. Do not hand-edit hashes. Separate build, development, and runtime dependencies, and remove compilers and package managers from the final stage when the application does not need them.



Secrets and AWS Identity



Do not bake model-provider tokens, database passwords, AWS credentials, or private certificates into an image. Use BuildKit secret mounts only for build-time access. At runtime, use the AWS workload identity mechanism for the selected platform—such as an ECS task role or EKS pod identity—and retrieve secrets through an approved secret service.



The container should receive only the AWS permissions required for its data, model, logging, and messaging operations. It should not inherit the deployment pipeline’s permissions.



Model Artifact Strategy



Choose one explicit model-delivery strategy:


  • External managed inference: keep model weights out of the image and call an approved endpoint such as Amazon Bedrock or SageMaker.


  • Versioned startup download: retrieve a specific model artifact version and verify its digest or signature before readiness becomes true.


  • Model embedded in the image: use only when licensing, size, patching, and distribution requirements justify it; expect larger images and slower distribution.


  • Mounted model volume: control version and access through the platform and keep application and model lifecycles separately traceable.


Never download an unversioned “latest” model during startup. Record model license, source, checksum, evaluation result, and compatibility with the application image.



CPU, GPU, and Multi-Architecture Builds



Build for the architecture used by the AWS runtime. Native Python wheels, CUDA libraries, and inference frameworks may differ across linux/amd64, linux/arm64, CPU, and GPU targets. Test each published platform rather than assuming a multi-architecture manifest makes the application portable.



Use a dedicated GPU base and runtime only when needed. GPU images require their own patch, license, driver-compatibility, vulnerability, size, and startup review.



Health, Startup, and Shutdown



Do not mark a large-model service ready before the model and required indexes are available. Set startup grace periods based on observed initialization time. Keep health endpoints fast and independent of expensive inference calls.



Handle SIGTERM, stop accepting new work, finish or checkpoint in-flight work within the orchestrator grace period, and release connections. Long inference requests require explicit timeout and retry semantics outside the container image.



Runtime Hardening



Carry the local restrictions into the AWS task or pod definition where supported:


  • Non-root user.


  • Read-only root filesystem.


  • No unnecessary Linux capabilities.


  • No privileged mode.


  • Explicit CPU, memory, temporary storage, and process limits.


  • Controlled outbound network access.


  • Runtime filesystem mounts declared intentionally.


  • Separate task/pod identity and deployment identity.


Test the exact production settings in staging. A secure Dockerfile cannot compensate for an over-privileged runtime configuration.



Observability and Sensitive Data



Use structured logs and correlation identifiers, but avoid logging prompts, source documents, embeddings, model responses, tokens, or credentials unless a reviewed data policy requires and protects them. Export request counts, latency, errors, timeouts, model identifiers, and resource pressure without leaking content.



Correlate source commit, image digest, model version, deployment version, and request ID so an incident can be traced to the exact running components.



Vulnerability Management and Supply Chain



Scan locally for fast feedback and scan again in ECR. Amazon ECR supports basic scanning and enhanced scanning through Amazon Inspector; current scan behavior and pricing should be reviewed before deployment. Use CI policy gates, SBOM retention, signed attestations where the organization supports them, and time-limited vulnerability exceptions with named owners.



Treat a scan as a point-in-time result. Continuously evaluate deployed digests as new vulnerability information becomes available.



Cost and Scaling



Docker Desktop verification uses workstation resources, while AWS costs depend on ECR storage and data transfer, ECR or Inspector scanning choices, runtime CPU/GPU/memory, logs, network paths, and model inference. Large images increase storage and deployment transfer time. Large embedded models also slow task replacement and incident recovery.

Use current official pricing pages for the selected AWS services and measure image pull, startup, memory, inference latency, and shutdown behavior under representative load before choosing scaling settings.



Clean Up the Local Tutorial



Preserve the four required screenshots, build identifiers, image digest, and scan evidence before cleanup.



Then:

docker stop document-insight-api
docker rm document-insight-api
docker image rm document-insight-api:1.0.0


Run the commands only against the named tutorial container and image. If the stopped container or image is already absent, Docker will report that condition.



Also remove disposable scan exports, temporary lock-generation files, and local test data that should not be retained. Do not delete shared BuildKit caches or unrelated Docker Desktop images as part of this tutorial.



If the image was pushed to ECR, remove the disposable tag or repository through the approved AWS cleanup process after retaining required evidence. Registry images, scan findings, logs, KMS keys, and deployed AWS tasks are separate resources and may continue to incur charges.





Reference Implementation



Publish the companion repository with this structure:



aws-production-ai-container/
├── README.md
├── app/
├── tests/
├── Dockerfile
├── .dockerignore
├── requirements.lock
├── requirements-dev.lock
├── compose.yaml
├── scripts/
│   ├── verify-image.ps1
│   └── smoke-test.ps1
└── infrastructure/
    └── ecr.yml

How Codersarts Can Help



Codersarts can containerize an existing AI application, reduce its runtime attack surface, separate model and application lifecycles, implement dependency and image scanning, create an Amazon ECR promotion workflow, and deploy the verified digest to ECS, EKS, App Runner, or SageMaker with suitable identity, networking, monitoring, and scaling controls.



Learn more about Codersarts AI development services or discuss how to move an AI prototype into a controlled AWS container workflow.





Conclusion



A production container image is more than a Dockerfile that starts successfully. It must have controlled inputs, a focused final stage, a non-root runtime, explicit health and shutdown behavior, no embedded secrets, a documented vulnerability decision, and an immutable identity.



Docker Desktop provides a practical local environment for proving those properties before the image reaches AWS. Once the image passes its local gates, promote that exact digest to Amazon ECR and let the AWS runtime supply environment-specific identity, secrets, networking, scaling, and operational controls.



References

 
 
 

Comments


bottom of page