Skip to content

How to deploy a uv project to AWS Fargate

uv

AWS Fargate runs a container image you push to Amazon ECR, with no host to manage. Two things decide whether that image works: it has to match Fargate’s CPU architecture, and it has to run as a long-lived service rather than a one-shot handler. uv builds that image fast, and its lockfile keeps every rebuild reproducible.

This guide covers the build side: a Fargate-ready Dockerfile, the architecture match, the ECR push, and how to keep uv’s cache warm across an ECS build pipeline. For the deploy mechanics that are independent of uv (service definitions and networking), follow the Amazon ECS developer guide.

Build a Fargate-ready image

Fargate tasks are long-running, so the runtime image should be small, run as a non-root user, and carry no build tooling. A multi-stage build installs dependencies with uv in a builder stage and copies only the resulting virtual environment forward:

Dockerfile
# Build stage: resolve and install dependencies with uv
FROM python:3.13-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:0.11.24 /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --no-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --no-editable

# Runtime stage: copy only the virtual environment
FROM python:3.13-slim

RUN useradd --create-home appuser
WORKDIR /app
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv

ENV PATH="/app/.venv/bin:$PATH"
USER appuser

CMD ["python", "-m", "myapp"]

The --no-dev flag drops pytest, Ruff, and other dev tools that have no place in a production task. The --no-editable flag installs the project as a regular package inside .venv, so the runtime stage needs only that directory: no source tree, no uv binary. For why the two-step uv sync caches dependencies separately from source, see How to use uv in a Dockerfile.

UV_COMPILE_BYTECODE=1 writes .pyc files at build time. Fargate starts a fresh container for every task it scales up, so paying the bytecode-compile cost once at build time, rather than on each cold start, shortens scale-up latency for apps with heavy import graphs.

Match the image to Fargate’s CPU architecture

Fargate tasks run on x86_64 by default and on AWS Graviton (arm64) when configured for it. The image architecture and the task definition must agree, or the task fails to start.

Building on an Apple Silicon Mac is the common trap. A plain docker build there produces an arm64 image; deployed to a default x86_64 Fargate task it crashes with exec format error. Use docker buildx build with an explicit --platform to pin the target:

# x86_64 Fargate (the default)
docker buildx build --platform linux/amd64 -t myapp:latest .

# Graviton (arm64) Fargate
docker buildx build --platform linux/arm64 -t myapp:latest .

For an arm64 image, set cpuArchitecture to ARM64 in the task definition’s runtimePlatform block. Graviton tasks cost less per vCPU-hour, so arm64 is worth targeting when every dependency ships an arm64 wheel.

Push the image to Amazon ECR

Fargate pulls images from a registry; ECR is the native choice. Authenticate Docker to ECR, tag the image with the repository URI, and push:

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com

docker tag myapp:latest <account>.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push <account>.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

The task definition’s image field then points at that URI. Set runtimePlatform.cpuArchitecture to match the --platform the image was built for.

Warning

A Fargate task needs a network path to ECR to pull the image. A subnet flagged “public” (MapPublicIpOnLaunch=true) is not enough on its own: without a route to an internet gateway (or ECR VPC endpoints), the task fails to start with ResourceInitializationError: ... unable to pull ... connection issue between the task and Amazon ECR. Run the task in a subnet that has real internet egress, or add the endpoints.

Cache uv downloads across pipeline builds

The --mount=type=cache directory in the Dockerfile speeds up local rebuilds, but it lives on the build host’s local disk. AWS CodeBuild and most CI runners start each build on a fresh, ephemeral host, so that cache is empty every time and uv re-downloads every wheel.

Push the build cache to a registry instead. docker buildx writes the cache to an ECR repository on each build and reads it back on the next one, so uv’s downloads survive across hosts:

docker buildx build \
  --platform linux/amd64 \
  --cache-to type=registry,ref=<account>.dkr.ecr.us-east-1.amazonaws.com/myapp:buildcache,mode=max \
  --cache-from type=registry,ref=<account>.dkr.ecr.us-east-1.amazonaws.com/myapp:buildcache \
  -t <account>.dkr.ecr.us-east-1.amazonaws.com/myapp:latest \
  --push .

mode=max caches every layer, including the builder stage where uv installs dependencies; the default mode=min caches only the final stage and would miss the uv cache mount layer entirely. In a CodeBuild buildspec.yml, run this in the build phase after the ECR login in pre_build.

The registry cache backend needs the containerd image store, which docker buildx enables through its own builder. On CodeBuild, create a buildx builder with docker buildx create --use before the build step.

Learn more

Last updated on