How to deploy a uv project to AWS Fargate
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:
# 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:latestThe 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
- How to use uv in a Dockerfile covers the multi-stage pattern,
.dockerignore, secret mounts, and base-image choices in depth. - How to deploy a uv project to AWS Lambda is the serverless sibling: handler images, zip archives, and cross-platform wheel builds.
- uv: A Complete Guide covers what uv does, how fast it is, and the core workflows.
- Using uv in Docker is Astral’s reference for cache mounts, bytecode compilation, and multi-stage layout.
- Cache builds to improve performance documents CodeBuild’s caching options, including registry-backed Docker layer cache.