> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS — ECS Fargate

> Production-grade Spree on AWS — ECS Fargate, RDS, CI/CD, and auto-scaling.

This is the advanced AWS path: containers managed by ECS Fargate with auto-scaling, zero-downtime rolling deploys, CI/CD via GitHub Actions, and secrets in AWS Secrets Manager. If you're getting started or running a single store, the [EC2 + RDS guide](/docs/developer/deployment/aws) gets you to production with a fraction of the moving parts — on the same [Docker image](/docs/developer/deployment/docker), so you can graduate to ECS later without rework.

## Required AWS Services

The required set is smaller than an AWS architecture diagram suggests — four services run the store:

| Service                                        | Purpose                                                                                                                                                                               |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ECS Fargate](https://aws.amazon.com/fargate/) | Runs the web (and optional worker) containers — no servers to manage                                                                                                                  |
| [RDS](https://aws.amazon.com/rds/)             | The database — data, background jobs, and cache all live here. PostgreSQL, MySQL/MariaDB, and both Aurora flavors work — see [Database Configuration](/docs/developer/deployment/database) |
| [ECR](https://aws.amazon.com/ecr/)             | Registry for your Docker image                                                                                                                                                        |
| [S3](https://aws.amazon.com/s3/)               | Uploaded files (product images) — Fargate containers have no persistent disk. See [Asset Storage](/docs/developer/deployment/assets#aws-s3)                                                |

In front of the web service you'll put an **Application Load Balancer** with a free HTTPS certificate from [Certificate Manager](https://aws.amazon.com/certificate-manager/) — that's your public entry point.

Everything else is optional:

| Service                                            | When                                                                                                             |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [CloudFront](https://aws.amazon.com/cloudfront/)   | [CDN](/docs/developer/deployment/cdn) in front of assets and images                                                   |
| [ElastiCache](https://aws.amazon.com/elasticache/) | Valkey/Redis for [high-traffic caching](/docs/developer/deployment/caching) — the default cache lives in the database |
| [Route 53](https://aws.amazon.com/route53/)        | DNS — convenient on AWS, but any DNS provider works                                                              |

## Docker Image

Build [your project's image](/docs/developer/deployment/docker) with `spree build --production`, or use the stock `ghcr.io/spree/spree` image for deployments without customizations.

To build and deploy a custom image to AWS ECR via GitHub Actions:

```yaml theme={"theme":"night-owl"}
name: Deploy to AWS Fargate

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: spree-starter
  ECS_SERVICE_WEB: spree-web
  ECS_SERVICE_WORKER: spree-worker
  ECS_CLUSTER: spree-cluster
  
jobs:
  build:
    name: Build and Push to ECR
    runs-on: ubuntu-latest
    
    outputs:
      image: ${{ steps.build-image.outputs.image }}
      image-tag: ${{ steps.build-image.outputs.image-tag }}
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
    
    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2
    
    - name: Build, tag, and push image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
      run: |
        # Build a docker container and push it to ECR
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
        echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
  
  deploy-web:
    name: Deploy Web Service
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
    
    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def-web
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      env:
        ECR_REGISTRY: ${{ needs.build.outputs.image }}
        IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
        AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
      with:
        task-definition: .aws/web-task-definition.json
        container-name: web
        image: ${{ needs.build.outputs.image }}
    
    - name: Deploy Amazon ECS task definition for web
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def-web.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE_WEB }}
        cluster: ${{ env.ECS_CLUSTER }}
        wait-for-service-stability: true
  
  deploy-worker:
    name: Deploy Worker Service
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
    
    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def-worker
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      env:
        ECR_REGISTRY: ${{ needs.build.outputs.image }}
        IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
        AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
      with:
        task-definition: .aws/worker-task-definition.json
        container-name: worker
        image: ${{ needs.build.outputs.image }}
    
    - name: Deploy Amazon ECS task definition for worker
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def-worker.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE_WORKER }}
        cluster: ${{ env.ECS_CLUSTER }}
        wait-for-service-stability: true

  migrate:
    name: Run Database Migrations
    runs-on: ubuntu-latest
    needs: [build, deploy-web]
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
    
    - name: Run database migrations
      run: |
        aws ecs run-task \
          --cluster ${{ env.ECS_CLUSTER }} \
          --task-definition spree-web \
          --overrides '{
            "containerOverrides": [{
              "name": "web",
              "command": ["bundle", "exec", "rails", "db:migrate"]
            }]
          }' \
          --launch-type FARGATE \
          --network-configuration '{
            "awsvpcConfiguration": {
              "subnets": ["'${{ secrets.SUBNET_ID_1 }}'", "'${{ secrets.SUBNET_ID_2 }}'"],
              "securityGroups": ["'${{ secrets.SECURITY_GROUP_ID }}'"],
              "assignPublicIp": "ENABLED"
            }
          }'
```

This action requires secrets to be set in your GitHub repository. You can find the full list of secrets in the [AWS ECS Deploy Task Definition](https://github.com/aws-actions/amazon-ecs-deploy-task-definition) GitHub Actions repository.

| Secret                  | Description           |
| ----------------------- | --------------------- |
| `AWS_ACCESS_KEY_ID`     | AWS access key ID     |
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key |
| `AWS_ACCOUNT_ID`        | AWS account ID        |
| `SUBNET_ID_1`           | First subnet ID       |
| `SUBNET_ID_2`           | Second subnet ID      |
| `SECURITY_GROUP_ID`     | Security group ID     |

## Environment Variables

Store secrets in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) and reference them in your task definitions. Non-sensitive configuration goes in the `environment` array directly.

For a full list of available variables, see [Environment Variables](/docs/developer/deployment/environment_variables).

### Secrets Manager

Create the following secrets in AWS Secrets Manager:

| Secret Name             | Variable          | Description                      |
| ----------------------- | ----------------- | -------------------------------- |
| `spree/database-url`    | `DATABASE_URL`    | PostgreSQL connection URL        |
| `spree/secret-key-base` | `SECRET_KEY_BASE` | Generate with `bin/rails secret` |

Optional secrets for email delivery, file storage, and error tracking:

| Secret Name           | Variable        | Description                   |
| --------------------- | --------------- | ----------------------------- |
| `spree/smtp-password` | `SMTP_PASSWORD` | SMTP auth password            |
| `spree/sentry-dsn`    | `SENTRY_DSN`    | Sentry DSN for error tracking |

<Tip>
  S3 file storage credentials are not needed as environment variables when your ECS task role has the appropriate S3 permissions. Use IAM roles instead of access keys when possible.
</Tip>

## ECS Task Definitions

One task definition runs the whole app — the web server also runs background jobs in-process ([combined mode](/docs/developer/deployment/quickstart#web-and-worker)). Save it as `.aws/web-task-definition.json`. The worker task definition is an optional scale-out step — skip it (and the `deploy-worker` job in the workflow above) until job load deserves its own service.

### Web Service

```json theme={"theme":"night-owl"}
{
  "family": "spree-web",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "4096",
  "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}",
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "RAILS_ENV",
          "value": "production"
        },
        {
          "name": "PORT",
          "value": "3000"
        },
        {
          "name": "RAILS_MAX_THREADS",
          "value": "3"
        },
        {
          "name": "WEB_CONCURRENCY",
          "value": "auto"
        },
        {
          "name": "RAILS_LOG_LEVEL",
          "value": "info"
        },
        {
          "name": "AWS_BUCKET",
          "value": "your-spree-bucket"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
        },
        {
          "name": "SECRET_KEY_BASE",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/secret-key-base"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/spree-web",
          "awslogs-region": "${AWS_REGION}",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:3000/up || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}
```

### Worker Service (optional scale-out)

Switches to [split mode](/docs/developer/deployment/quickstart#web-and-worker): background jobs move into a dedicated service — same image, no rebuild. When you deploy it, set `SOLID_QUEUE_IN_PUMA=false` in the web task's `environment` so the web container stops running jobs. Save as `.aws/worker-task-definition.json`:

```json theme={"theme":"night-owl"}
{
  "family": "spree-worker",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "2048",
  "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "worker",
      "image": "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}",
      "command": ["bin/jobs"],
      "essential": true,
      "environment": [
        {
          "name": "RAILS_ENV",
          "value": "production"
        },
        {
          "name": "RAILS_LOG_LEVEL",
          "value": "info"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
        },
        {
          "name": "SECRET_KEY_BASE",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/secret-key-base"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/spree-worker",
          "awslogs-region": "${AWS_REGION}",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "pgrep -f solid_queue || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}
```

## Production Sizing

The task definitions above are sized for production. Here is a summary and scaling guidance:

| Service                     | Fargate CPU | Fargate Memory                 | Instances                         |
| --------------------------- | ----------- | ------------------------------ | --------------------------------- |
| **Web**                     | 1 vCPU      | 4 GB                           | 2+ (use ECS Service Auto Scaling) |
| **Worker** (optional split) | 0.5 vCPU    | 2 GB                           | 1+                                |
| **RDS (PostgreSQL)**        | —           | `db.r6g.large` (2 vCPU, 16 GB) | 1                                 |

The database also stores background jobs and cache — the sizing above accounts for it. Scaling the web service to multiple instances is safe with in-process jobs: instances coordinate through the database. Add ElastiCache (Valkey or Redis) only for [high-traffic caching](/docs/developer/deployment/caching).

### Auto Scaling

Use [ECS Service Auto Scaling](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-auto-scaling.html) to scale the web service based on CPU or memory utilization:

```bash theme={"theme":"night-owl"}
# Register a scalable target (min 2, max 6 tasks)
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/spree-cluster/spree-web \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 6

# Scale based on average CPU utilization (target 70%)
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/spree-cluster/spree-web \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name spree-web-cpu-scaling \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'
```

## After Deployment

### Admin

Access your admin panel at:

```
https://<your-domain>/admin
```

Default credentials are created during `db:seed`. Change them immediately after first login.

### Database Migrations

The GitHub Actions workflow above runs migrations automatically on deploy. To run migrations manually:

```bash theme={"theme":"night-owl"}
aws ecs run-task \
  --cluster spree-cluster \
  --task-definition spree-web \
  --overrides '{
    "containerOverrides": [{
      "name": "web",
      "command": ["bundle", "exec", "rails", "db:migrate"]
    }]
  }' \
  --launch-type FARGATE \
  --network-configuration '{
    "awsvpcConfiguration": {
      "subnets": ["subnet-xxx", "subnet-yyy"],
      "securityGroups": ["sg-xxx"],
      "assignPublicIp": "DISABLED"
    }
  }'
```

## Next Steps

* [Asset Storage](/docs/developer/deployment/assets) — configure S3 for product images and uploads
* [Configure CDN](/docs/developer/deployment/cdn) — set up CloudFront for asset delivery
* [Configure caching](/docs/developer/deployment/caching) — works out of the box; add ElastiCache for high-traffic installs
* [Set environment variables](/docs/developer/deployment/environment_variables) — SMTP, Sentry, SSL, etc.
* [Deploy the storefront](/docs/developer/storefront/nextjs/deployment) — the Next.js storefront ships separately, typically to Vercel
