# How to deploy a uv project to AWS Fargate


AWS Fargate runs a container image you push to [Amazon ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html), 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](https://pydevtools.com/handbook/reference/uv.md) builds that image fast, and its [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) 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](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/getting-started-fargate.html).

## 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 {filename="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](https://pydevtools.com/handbook/reference/pytest.md), [Ruff](https://pydevtools.com/handbook/reference/ruff.md), 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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md).

`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](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-arm64.html) (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:

```bash
# 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:

```bash
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](https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html)), 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](https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html) 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:

```bash
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](https://docs.docker.com/engine/storage/containerd/), 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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md) covers the multi-stage pattern, `.dockerignore`, secret mounts, and base-image choices in depth.
- [How to deploy a uv project to AWS Lambda](https://pydevtools.com/handbook/how-to/how-to-deploy-a-uv-project-to-aws-lambda.md) is the serverless sibling: handler images, zip archives, and cross-platform wheel builds.
- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, and the core workflows.
- [Using uv in Docker](https://docs.astral.sh/uv/guides/integration/docker/) is Astral's reference for cache mounts, bytecode compilation, and multi-stage layout.
- [Cache builds to improve performance](https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html) documents CodeBuild's caching options, including registry-backed Docker layer cache.
