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:
Service | Description |
---|
AWS ECS Fargate | Amazon 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 RDS | Amazon 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 ElastiCache | You will need 2 instances of Valkey or Redis: one for the Active Job background queue and one for the Spree cache. |
AWS S3 | Object storage service to store and read your uploaded files such as Product images, etc. More information. |
AWS CloudFront | Fast content delivery network CDN to speed up your asset images/stylesheets/javascript delivery. This will greatly enhance your application responsiveness. |
AWS Route 53 | Domain name system (DNS) service to manage your domain names and DNS records. |
AWS Certificate Manager | AWS 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 ECR | Amazon 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.
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
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
}
}
]
}