How to Build Production-Ready Docker Images for AI Applications
- pranavsankar
- 3 days ago
- 13 min read

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:
Record the ECR image digest.
Confirm that it matches the promoted local content.
Run the configured ECR vulnerability scan or continuous scan.
Make the AWS deployment reference the digest, not a mutable tag.
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.



Comments