Amazon Web Services offers reliable, scalable, and inexpensive cloud computing services. AWS is also one of the most popular choices for hosting a Spree application.

We recommend using AWS ECS Fargate to host your Spree application via Docker image.

Required AWS services

To fully run your Spree application on AWS, you will need the following services:

ServiceDescription
AWS ECS FargateAmazon Elastic Container Service (ECS) is a fully managed container orchestration service that allows you to run and scale containerized applications without managing the underlying infrastructure.
AWS RDSAmazon Relational Database Service makes it easy to set up, operate, and scale a relational database in the cloud. Spree works great with multiple databases: Amazon Aurora both MySQL and PostgreSQL variants, RDS PostgreSQL, RDS MySQL and RDS MariaDB
AWS ElastiCacheYou will need 2 instances of Valkey or Redis: one for the Active Job background queue and one for the Spree cache.
AWS S3Object storage service to store and read your uploaded files such as Product images, etc. More information.
AWS CloudFrontFast content delivery network CDN to speed up your asset images/stylesheets/javascript delivery. This will greatly enhance your application responsiveness.
AWS Route 53Domain name system (DNS) service to manage your domain names and DNS records.
AWS Certificate ManagerAWS Certificate Manager is a service that provides you with SSL/TLS certificates that you can use to secure your application. Spree in production works only with HTTPS.
AWS ECRAmazon Elastic Container Registry is a fully managed Docker container registry that makes it easy to store, manage, and deploy Docker container images.

Docker builds

To deploy your Spree application on AWS, you will need to build a Docker image and send it to AWS ECR so that AWS ECS Fargate can pull it and run it.

Spree Starter Dockerfile is a good starting point for your Docker build.

You can deploy it to AWS ECR straitgh from GitHub via GitHub Actions, eg:

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 GitHub Actions repository.

SecretDescription
AWS_ACCESS_KEY_IDAWS access key ID
AWS_SECRET_ACCESS_KEYAWS secret access key
AWS_ACCOUNT_IDAWS account ID
SUBNET_ID_1First subnet ID
SUBNET_ID_2Second subnet ID
SECURITY_GROUP_IDSecurity group ID

Environment variables

For a full list of Docker environment variables, please refer to the Environment variables page.

ECS tasks

You will need to create two ECS task definitions: one for the web service and one for the worker service.

Web service

{
  "family": "spree-web",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "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"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
        },
        {
          "name": "REDIS_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/redis-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

@ -0,0 +1,52 @@
{
  "family": "spree-worker",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "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": ["bundle", "exec", "sidekiq"],
      "essential": true,
      "environment": [
        {
          "name": "RAILS_ENV",
          "value": "production"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
        },
        {
          "name": "REDIS_URL",
          "valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/redis-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 sidekiq || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}