@spree/docs 0.1.129 → 0.1.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/developer/contributing/developing-spree.md +2 -2
- package/dist/developer/core-concepts/reports.md +1 -1
- package/dist/developer/create-spree-app/quickstart.md +34 -14
- package/dist/developer/dashboard/deployment.md +8 -12
- package/dist/developer/deployment/aws.md +77 -434
- package/dist/developer/deployment/aws_ecs.md +460 -0
- package/dist/developer/deployment/background_jobs.md +106 -0
- package/dist/developer/deployment/caching.md +15 -7
- package/dist/developer/deployment/database.md +4 -4
- package/dist/developer/deployment/docker.md +50 -58
- package/dist/developer/deployment/emails.md +27 -19
- package/dist/developer/deployment/environment_variables.md +33 -21
- package/dist/developer/deployment/quickstart.md +79 -0
- package/dist/developer/deployment/render.md +28 -42
- package/package.json +1 -1
- package/dist/developer/deployment/heroku.md +0 -51
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: AWS — ECS Fargate
|
|
3
|
+
sidebarTitle: ECS Fargate
|
|
4
|
+
description: Production-grade Spree on AWS — ECS Fargate, RDS, CI/CD, and auto-scaling.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
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](aws.md) gets you to production with a fraction of the moving parts — on the same [Docker image](docker.md), so you can graduate to ECS later without rework.
|
|
8
|
+
|
|
9
|
+
## Required AWS Services
|
|
10
|
+
|
|
11
|
+
The required set is smaller than an AWS architecture diagram suggests — four services run the store:
|
|
12
|
+
|
|
13
|
+
| Service | Purpose |
|
|
14
|
+
|---------|---------|
|
|
15
|
+
| [ECS Fargate](https://aws.amazon.com/fargate/) | Runs the web (and optional worker) containers — no servers to manage |
|
|
16
|
+
| [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](database.md) |
|
|
17
|
+
| [ECR](https://aws.amazon.com/ecr/) | Registry for your Docker image |
|
|
18
|
+
| [S3](https://aws.amazon.com/s3/) | Uploaded files (product images) — Fargate containers have no persistent disk. See [Asset Storage](assets.md#aws-s3) |
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
Everything else is optional:
|
|
23
|
+
|
|
24
|
+
| Service | When |
|
|
25
|
+
|---------|------|
|
|
26
|
+
| [CloudFront](https://aws.amazon.com/cloudfront/) | [CDN](cdn.md) in front of assets and images |
|
|
27
|
+
| [ElastiCache](https://aws.amazon.com/elasticache/) | Valkey/Redis for [high-traffic caching](caching.md) — the default cache lives in the database |
|
|
28
|
+
| [Route 53](https://aws.amazon.com/route53/) | DNS — convenient on AWS, but any DNS provider works |
|
|
29
|
+
|
|
30
|
+
## Docker Image
|
|
31
|
+
|
|
32
|
+
Build [your project's image](docker.md) with `spree build --production`, or use the stock `ghcr.io/spree/spree` image for deployments without customizations.
|
|
33
|
+
|
|
34
|
+
To build and deploy a custom image to AWS ECR via GitHub Actions:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
name: Deploy to AWS Fargate
|
|
38
|
+
|
|
39
|
+
on:
|
|
40
|
+
push:
|
|
41
|
+
branches: [ main ]
|
|
42
|
+
pull_request:
|
|
43
|
+
branches: [ main ]
|
|
44
|
+
|
|
45
|
+
env:
|
|
46
|
+
AWS_REGION: us-east-1
|
|
47
|
+
ECR_REPOSITORY: spree-starter
|
|
48
|
+
ECS_SERVICE_WEB: spree-web
|
|
49
|
+
ECS_SERVICE_WORKER: spree-worker
|
|
50
|
+
ECS_CLUSTER: spree-cluster
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
build:
|
|
54
|
+
name: Build and Push to ECR
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
|
|
57
|
+
outputs:
|
|
58
|
+
image: ${{ steps.build-image.outputs.image }}
|
|
59
|
+
image-tag: ${{ steps.build-image.outputs.image-tag }}
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout code
|
|
63
|
+
uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- name: Configure AWS credentials
|
|
66
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
67
|
+
with:
|
|
68
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
69
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
70
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
71
|
+
|
|
72
|
+
- name: Login to Amazon ECR
|
|
73
|
+
id: login-ecr
|
|
74
|
+
uses: aws-actions/amazon-ecr-login@v2
|
|
75
|
+
|
|
76
|
+
- name: Build, tag, and push image to Amazon ECR
|
|
77
|
+
id: build-image
|
|
78
|
+
env:
|
|
79
|
+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
80
|
+
IMAGE_TAG: ${{ github.sha }}
|
|
81
|
+
run: |
|
|
82
|
+
# Build a docker container and push it to ECR
|
|
83
|
+
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
|
|
84
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
85
|
+
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
|
|
86
|
+
echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
|
87
|
+
|
|
88
|
+
deploy-web:
|
|
89
|
+
name: Deploy Web Service
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
needs: build
|
|
92
|
+
if: github.ref == 'refs/heads/main'
|
|
93
|
+
|
|
94
|
+
steps:
|
|
95
|
+
- name: Checkout code
|
|
96
|
+
uses: actions/checkout@v4
|
|
97
|
+
|
|
98
|
+
- name: Configure AWS credentials
|
|
99
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
100
|
+
with:
|
|
101
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
102
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
103
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
104
|
+
|
|
105
|
+
- name: Fill in the new image ID in the Amazon ECS task definition
|
|
106
|
+
id: task-def-web
|
|
107
|
+
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
|
108
|
+
env:
|
|
109
|
+
ECR_REGISTRY: ${{ needs.build.outputs.image }}
|
|
110
|
+
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
|
|
111
|
+
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
|
112
|
+
with:
|
|
113
|
+
task-definition: .aws/web-task-definition.json
|
|
114
|
+
container-name: web
|
|
115
|
+
image: ${{ needs.build.outputs.image }}
|
|
116
|
+
|
|
117
|
+
- name: Deploy Amazon ECS task definition for web
|
|
118
|
+
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
|
119
|
+
with:
|
|
120
|
+
task-definition: ${{ steps.task-def-web.outputs.task-definition }}
|
|
121
|
+
service: ${{ env.ECS_SERVICE_WEB }}
|
|
122
|
+
cluster: ${{ env.ECS_CLUSTER }}
|
|
123
|
+
wait-for-service-stability: true
|
|
124
|
+
|
|
125
|
+
deploy-worker:
|
|
126
|
+
name: Deploy Worker Service
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
needs: build
|
|
129
|
+
if: github.ref == 'refs/heads/main'
|
|
130
|
+
|
|
131
|
+
steps:
|
|
132
|
+
- name: Checkout code
|
|
133
|
+
uses: actions/checkout@v4
|
|
134
|
+
|
|
135
|
+
- name: Configure AWS credentials
|
|
136
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
137
|
+
with:
|
|
138
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
139
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
140
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
141
|
+
|
|
142
|
+
- name: Fill in the new image ID in the Amazon ECS task definition
|
|
143
|
+
id: task-def-worker
|
|
144
|
+
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
|
145
|
+
env:
|
|
146
|
+
ECR_REGISTRY: ${{ needs.build.outputs.image }}
|
|
147
|
+
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
|
|
148
|
+
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
|
149
|
+
with:
|
|
150
|
+
task-definition: .aws/worker-task-definition.json
|
|
151
|
+
container-name: worker
|
|
152
|
+
image: ${{ needs.build.outputs.image }}
|
|
153
|
+
|
|
154
|
+
- name: Deploy Amazon ECS task definition for worker
|
|
155
|
+
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
|
156
|
+
with:
|
|
157
|
+
task-definition: ${{ steps.task-def-worker.outputs.task-definition }}
|
|
158
|
+
service: ${{ env.ECS_SERVICE_WORKER }}
|
|
159
|
+
cluster: ${{ env.ECS_CLUSTER }}
|
|
160
|
+
wait-for-service-stability: true
|
|
161
|
+
|
|
162
|
+
migrate:
|
|
163
|
+
name: Run Database Migrations
|
|
164
|
+
runs-on: ubuntu-latest
|
|
165
|
+
needs: [build, deploy-web]
|
|
166
|
+
if: github.ref == 'refs/heads/main'
|
|
167
|
+
|
|
168
|
+
steps:
|
|
169
|
+
- name: Checkout code
|
|
170
|
+
uses: actions/checkout@v4
|
|
171
|
+
|
|
172
|
+
- name: Configure AWS credentials
|
|
173
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
174
|
+
with:
|
|
175
|
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
176
|
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
177
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
178
|
+
|
|
179
|
+
- name: Run database migrations
|
|
180
|
+
run: |
|
|
181
|
+
aws ecs run-task \
|
|
182
|
+
--cluster ${{ env.ECS_CLUSTER }} \
|
|
183
|
+
--task-definition spree-web \
|
|
184
|
+
--overrides '{
|
|
185
|
+
"containerOverrides": [{
|
|
186
|
+
"name": "web",
|
|
187
|
+
"command": ["bundle", "exec", "rails", "db:migrate"]
|
|
188
|
+
}]
|
|
189
|
+
}' \
|
|
190
|
+
--launch-type FARGATE \
|
|
191
|
+
--network-configuration '{
|
|
192
|
+
"awsvpcConfiguration": {
|
|
193
|
+
"subnets": ["'${{ secrets.SUBNET_ID_1 }}'", "'${{ secrets.SUBNET_ID_2 }}'"],
|
|
194
|
+
"securityGroups": ["'${{ secrets.SECURITY_GROUP_ID }}'"],
|
|
195
|
+
"assignPublicIp": "ENABLED"
|
|
196
|
+
}
|
|
197
|
+
}'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
| Secret | Description |
|
|
203
|
+
|--------|-------------|
|
|
204
|
+
| `AWS_ACCESS_KEY_ID` | AWS access key ID |
|
|
205
|
+
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key |
|
|
206
|
+
| `AWS_ACCOUNT_ID` | AWS account ID |
|
|
207
|
+
| `SUBNET_ID_1` | First subnet ID |
|
|
208
|
+
| `SUBNET_ID_2` | Second subnet ID |
|
|
209
|
+
| `SECURITY_GROUP_ID` | Security group ID |
|
|
210
|
+
|
|
211
|
+
## Environment Variables
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
For a full list of available variables, see [Environment Variables](environment_variables.md).
|
|
216
|
+
|
|
217
|
+
### Secrets Manager
|
|
218
|
+
|
|
219
|
+
Create the following secrets in AWS Secrets Manager:
|
|
220
|
+
|
|
221
|
+
| Secret Name | Variable | Description |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `spree/database-url` | `DATABASE_URL` | PostgreSQL connection URL |
|
|
224
|
+
| `spree/secret-key-base` | `SECRET_KEY_BASE` | Generate with `bin/rails secret` |
|
|
225
|
+
|
|
226
|
+
Optional secrets for email delivery, file storage, and error tracking:
|
|
227
|
+
|
|
228
|
+
| Secret Name | Variable | Description |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `spree/smtp-password` | `SMTP_PASSWORD` | SMTP auth password |
|
|
231
|
+
| `spree/sentry-dsn` | `SENTRY_DSN` | Sentry DSN for error tracking |
|
|
232
|
+
|
|
233
|
+
> **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.
|
|
234
|
+
|
|
235
|
+
## ECS Task Definitions
|
|
236
|
+
|
|
237
|
+
One task definition runs the whole app — the web server also runs background jobs in-process ([combined mode](quickstart.md#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.
|
|
238
|
+
|
|
239
|
+
### Web Service
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"family": "spree-web",
|
|
244
|
+
"networkMode": "awsvpc",
|
|
245
|
+
"requiresCompatibilities": ["FARGATE"],
|
|
246
|
+
"cpu": "1024",
|
|
247
|
+
"memory": "4096",
|
|
248
|
+
"executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
|
|
249
|
+
"taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskRole",
|
|
250
|
+
"containerDefinitions": [
|
|
251
|
+
{
|
|
252
|
+
"name": "web",
|
|
253
|
+
"image": "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}",
|
|
254
|
+
"portMappings": [
|
|
255
|
+
{
|
|
256
|
+
"containerPort": 3000,
|
|
257
|
+
"protocol": "tcp"
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
"essential": true,
|
|
261
|
+
"environment": [
|
|
262
|
+
{
|
|
263
|
+
"name": "RAILS_ENV",
|
|
264
|
+
"value": "production"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "PORT",
|
|
268
|
+
"value": "3000"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"name": "RAILS_MAX_THREADS",
|
|
272
|
+
"value": "3"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "WEB_CONCURRENCY",
|
|
276
|
+
"value": "auto"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"name": "RAILS_LOG_LEVEL",
|
|
280
|
+
"value": "info"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"name": "AWS_BUCKET",
|
|
284
|
+
"value": "your-spree-bucket"
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
"secrets": [
|
|
288
|
+
{
|
|
289
|
+
"name": "DATABASE_URL",
|
|
290
|
+
"valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "SECRET_KEY_BASE",
|
|
294
|
+
"valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/secret-key-base"
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
"logConfiguration": {
|
|
298
|
+
"logDriver": "awslogs",
|
|
299
|
+
"options": {
|
|
300
|
+
"awslogs-group": "/ecs/spree-web",
|
|
301
|
+
"awslogs-region": "${AWS_REGION}",
|
|
302
|
+
"awslogs-stream-prefix": "ecs"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
"healthCheck": {
|
|
306
|
+
"command": ["CMD-SHELL", "curl -f http://localhost:3000/up || exit 1"],
|
|
307
|
+
"interval": 30,
|
|
308
|
+
"timeout": 5,
|
|
309
|
+
"retries": 3,
|
|
310
|
+
"startPeriod": 60
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Worker Service (optional scale-out)
|
|
318
|
+
|
|
319
|
+
Switches to [split mode](quickstart.md#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`:
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"family": "spree-worker",
|
|
324
|
+
"networkMode": "awsvpc",
|
|
325
|
+
"requiresCompatibilities": ["FARGATE"],
|
|
326
|
+
"cpu": "512",
|
|
327
|
+
"memory": "2048",
|
|
328
|
+
"executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
|
|
329
|
+
"taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskRole",
|
|
330
|
+
"containerDefinitions": [
|
|
331
|
+
{
|
|
332
|
+
"name": "worker",
|
|
333
|
+
"image": "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}",
|
|
334
|
+
"command": ["bin/jobs"],
|
|
335
|
+
"essential": true,
|
|
336
|
+
"environment": [
|
|
337
|
+
{
|
|
338
|
+
"name": "RAILS_ENV",
|
|
339
|
+
"value": "production"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "RAILS_LOG_LEVEL",
|
|
343
|
+
"value": "info"
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
"secrets": [
|
|
347
|
+
{
|
|
348
|
+
"name": "DATABASE_URL",
|
|
349
|
+
"valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/database-url"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"name": "SECRET_KEY_BASE",
|
|
353
|
+
"valueFrom": "arn:aws:secretsmanager:${AWS_REGION}:${AWS_ACCOUNT_ID}:secret:spree/secret-key-base"
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"logConfiguration": {
|
|
357
|
+
"logDriver": "awslogs",
|
|
358
|
+
"options": {
|
|
359
|
+
"awslogs-group": "/ecs/spree-worker",
|
|
360
|
+
"awslogs-region": "${AWS_REGION}",
|
|
361
|
+
"awslogs-stream-prefix": "ecs"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"healthCheck": {
|
|
365
|
+
"command": ["CMD-SHELL", "pgrep -f solid_queue || exit 1"],
|
|
366
|
+
"interval": 30,
|
|
367
|
+
"timeout": 5,
|
|
368
|
+
"retries": 3,
|
|
369
|
+
"startPeriod": 60
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Production Sizing
|
|
377
|
+
|
|
378
|
+
The task definitions above are sized for production. Here is a summary and scaling guidance:
|
|
379
|
+
|
|
380
|
+
| Service | Fargate CPU | Fargate Memory | Instances |
|
|
381
|
+
|---|---|---|---|
|
|
382
|
+
| **Web** | 1 vCPU | 4 GB | 2+ (use ECS Service Auto Scaling) |
|
|
383
|
+
| **Worker** (optional split) | 0.5 vCPU | 2 GB | 1+ |
|
|
384
|
+
| **RDS (PostgreSQL)** | — | `db.r6g.large` (2 vCPU, 16 GB) | 1 |
|
|
385
|
+
|
|
386
|
+
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](caching.md).
|
|
387
|
+
|
|
388
|
+
### Auto Scaling
|
|
389
|
+
|
|
390
|
+
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:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# Register a scalable target (min 2, max 6 tasks)
|
|
394
|
+
aws application-autoscaling register-scalable-target \
|
|
395
|
+
--service-namespace ecs \
|
|
396
|
+
--resource-id service/spree-cluster/spree-web \
|
|
397
|
+
--scalable-dimension ecs:service:DesiredCount \
|
|
398
|
+
--min-capacity 2 \
|
|
399
|
+
--max-capacity 6
|
|
400
|
+
|
|
401
|
+
# Scale based on average CPU utilization (target 70%)
|
|
402
|
+
aws application-autoscaling put-scaling-policy \
|
|
403
|
+
--service-namespace ecs \
|
|
404
|
+
--resource-id service/spree-cluster/spree-web \
|
|
405
|
+
--scalable-dimension ecs:service:DesiredCount \
|
|
406
|
+
--policy-name spree-web-cpu-scaling \
|
|
407
|
+
--policy-type TargetTrackingScaling \
|
|
408
|
+
--target-tracking-scaling-policy-configuration '{
|
|
409
|
+
"TargetValue": 70.0,
|
|
410
|
+
"PredefinedMetricSpecification": {
|
|
411
|
+
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
|
|
412
|
+
},
|
|
413
|
+
"ScaleInCooldown": 300,
|
|
414
|
+
"ScaleOutCooldown": 60
|
|
415
|
+
}'
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## After Deployment
|
|
419
|
+
|
|
420
|
+
### Admin
|
|
421
|
+
|
|
422
|
+
Access your admin panel at:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
https://<your-domain>/admin
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Default credentials are created during `db:seed`. Change them immediately after first login.
|
|
429
|
+
|
|
430
|
+
### Database Migrations
|
|
431
|
+
|
|
432
|
+
The GitHub Actions workflow above runs migrations automatically on deploy. To run migrations manually:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
aws ecs run-task \
|
|
436
|
+
--cluster spree-cluster \
|
|
437
|
+
--task-definition spree-web \
|
|
438
|
+
--overrides '{
|
|
439
|
+
"containerOverrides": [{
|
|
440
|
+
"name": "web",
|
|
441
|
+
"command": ["bundle", "exec", "rails", "db:migrate"]
|
|
442
|
+
}]
|
|
443
|
+
}' \
|
|
444
|
+
--launch-type FARGATE \
|
|
445
|
+
--network-configuration '{
|
|
446
|
+
"awsvpcConfiguration": {
|
|
447
|
+
"subnets": ["subnet-xxx", "subnet-yyy"],
|
|
448
|
+
"securityGroups": ["sg-xxx"],
|
|
449
|
+
"assignPublicIp": "DISABLED"
|
|
450
|
+
}
|
|
451
|
+
}'
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Next Steps
|
|
455
|
+
|
|
456
|
+
- [Asset Storage](assets.md) — configure S3 for product images and uploads
|
|
457
|
+
- [Configure CDN](cdn.md) — set up CloudFront for asset delivery
|
|
458
|
+
- [Configure caching](caching.md) — works out of the box; add ElastiCache for high-traffic installs
|
|
459
|
+
- [Set environment variables](environment_variables.md) — SMTP, Sentry, SSL, etc.
|
|
460
|
+
- [Deploy the storefront](../storefront/nextjs/deployment.md) — the Next.js storefront ships separately, typically to Vercel
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Background Jobs
|
|
3
|
+
description: Spree's background jobs live in your database by default (Solid Queue) — nothing extra to run, with a Sidekiq/Redis swap for very high volume.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Everything that doesn't happen during a request runs as a background job: sending emails, processing images, delivering webhooks, importing and exporting catalogs, releasing expired stock reservations. Spree uses Active Job (Rails' standard job interface), so the queue backend is swappable — but the default needs nothing beyond your database.
|
|
7
|
+
|
|
8
|
+
## Solid Queue — the default
|
|
9
|
+
|
|
10
|
+
Production uses [Solid Queue](https://github.com/rails/solid_queue) — the built-in background job processor: jobs are stored in your database alongside everything else. There is nothing extra to provision, and by default the job supervisor runs **inside the web container** — one container is the whole application ([combined mode](quickstart.md#web-and-worker)).
|
|
11
|
+
|
|
12
|
+
When job volume grows, switch to split mode: run a dedicated worker from the same image with `bin/jobs`, and set `SOLID_QUEUE_IN_PUMA=false` on the web service. Multiple web instances and multiple workers are safe by default — they coordinate through the database.
|
|
13
|
+
|
|
14
|
+
| Variable | Default | Description |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `SOLID_QUEUE_IN_PUMA` | `true` | Run jobs inside the web container; set `false` when a dedicated worker runs |
|
|
17
|
+
| `JOB_THREADS` | `3` | Worker thread count — raise it on a dedicated worker |
|
|
18
|
+
| `JOB_CONCURRENCY` | `1` | Worker process count — the scaling knob for dedicated workers |
|
|
19
|
+
|
|
20
|
+
## Job Dashboard
|
|
21
|
+
|
|
22
|
+
Every deployment ships [Mission Control](https://github.com/rails/mission_control-jobs) — a web UI for background jobs — at `/jobs`: inspect, retry, and discard jobs from the browser. It's guarded by HTTP Basic auth: set `MISSION_CONTROL_USER` and `MISSION_CONTROL_PASSWORD` in production (without them the dashboard stays locked).
|
|
23
|
+
|
|
24
|
+
## Recurring Jobs
|
|
25
|
+
|
|
26
|
+
Scheduled work — releasing expired stock reservations, pruning price history, queue housekeeping — is defined in `config/recurring.yml`, Solid Queue's built-in cron. Add your own:
|
|
27
|
+
|
|
28
|
+
```yaml config/recurring.yml
|
|
29
|
+
production:
|
|
30
|
+
nightly_report:
|
|
31
|
+
class: MyReportJob
|
|
32
|
+
schedule: at 5am every day
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Queues
|
|
36
|
+
|
|
37
|
+
`config/queue.yml` lists queues in polling order: checkout-critical and operator-facing work first, bulk catalog processing second, housekeeping last. A trailing `"*"` catches any queue added later. Large CSV imports are additionally capped so they can't occupy the whole worker pool — see [`SPREE_IMPORT_JOB_CONCURRENCY`](environment_variables.md).
|
|
38
|
+
|
|
39
|
+
## Swapping to Sidekiq
|
|
40
|
+
|
|
41
|
+
For very high job volume, Spree works with [Sidekiq](https://sidekiq.org/) on Redis or Valkey — like any Active Job backend. This is a bigger step than the [cache swap](caching.md); here is the full setup.
|
|
42
|
+
|
|
43
|
+
Add the gems and switch the adapter:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Gemfile
|
|
47
|
+
gem "sidekiq"
|
|
48
|
+
gem "sidekiq-cron" # recurring jobs (replaces config/recurring.yml)
|
|
49
|
+
gem "sentry-sidekiq" # only if you use Sentry
|
|
50
|
+
|
|
51
|
+
# config/application.rb
|
|
52
|
+
config.active_job.queue_adapter = :sidekiq
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Configure the queues with weights — Sidekiq's equivalent of Solid Queue's polling order (checkout-critical work first, bulk catalog processing second, housekeeping last):
|
|
56
|
+
|
|
57
|
+
```yaml config/sidekiq.yml
|
|
58
|
+
:concurrency: <%= Integer(ENV.fetch('SIDEKIQ_CONCURRENCY', '10')) %>
|
|
59
|
+
:queues:
|
|
60
|
+
- [default, 5]
|
|
61
|
+
- [spree_imports, 5]
|
|
62
|
+
- [spree_payment_webhooks, 5]
|
|
63
|
+
- [mailers, 5]
|
|
64
|
+
- [spree_events, 3]
|
|
65
|
+
- [spree_exports, 3]
|
|
66
|
+
- [spree_images, 3]
|
|
67
|
+
- [spree_products, 3]
|
|
68
|
+
- [spree_reports, 3]
|
|
69
|
+
- [spree_variants, 3]
|
|
70
|
+
- [spree_taxons, 3]
|
|
71
|
+
- [spree_stock_location_stock_items, 3]
|
|
72
|
+
- [spree_coupon_codes, 3]
|
|
73
|
+
- [spree_addresses, 3]
|
|
74
|
+
- [spree_gift_cards, 3]
|
|
75
|
+
- [spree_webhooks, 3]
|
|
76
|
+
- [spree_api_keys, 3]
|
|
77
|
+
- [spree_search, 3]
|
|
78
|
+
- [active_storage_transform, 3]
|
|
79
|
+
- [active_storage_analysis, 1]
|
|
80
|
+
- [active_storage_purge, 1]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Move the `config/recurring.yml` schedules to sidekiq-cron (it auto-loads `config/schedule.yml`):
|
|
84
|
+
|
|
85
|
+
```yaml config/schedule.yml
|
|
86
|
+
expire_stock_reservations:
|
|
87
|
+
cron: "* * * * *"
|
|
88
|
+
class: Spree::StockReservations::ExpireJob
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `/jobs` dashboard is Solid Queue-specific — mount [Sidekiq's Web UI](https://github.com/sidekiq/sidekiq/wiki/Monitoring) instead:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# config/routes.rb
|
|
95
|
+
require "sidekiq/web"
|
|
96
|
+
|
|
97
|
+
Sidekiq::Web.use Rack::Auth::Basic do |user, password|
|
|
98
|
+
ActiveSupport::SecurityUtils.secure_compare(user, ENV["SIDEKIQ_USER"].to_s) &
|
|
99
|
+
ActiveSupport::SecurityUtils.secure_compare(password, ENV["SIDEKIQ_PASSWORD"].to_s)
|
|
100
|
+
end
|
|
101
|
+
mount Sidekiq::Web => "/sidekiq"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Finally: point Sidekiq at your Redis/Valkey via `REDIS_URL`, run `bundle exec sidekiq` as a dedicated worker service (set `RAILS_MAX_THREADS` to at least `SIDEKIQ_CONCURRENCY` there so the database pool covers every worker thread), and remove the `SOLID_QUEUE_IN_PUMA` setting — with Sidekiq, web and worker are always separate processes.
|
|
105
|
+
|
|
106
|
+
Reach for this when a dedicated Solid Queue worker with raised `JOB_THREADS`/`JOB_CONCURRENCY` no longer keeps up — for most stores, that point never arrives.
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Caching
|
|
3
|
-
description:
|
|
3
|
+
description: Spree's cache lives in Postgres by default (Solid Cache) — nothing extra to run, with a one-line Redis/Valkey swap for high-traffic installs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Caching improves performance by storing the results of expensive
|
|
6
|
+
Caching improves performance by storing the results of expensive computations. Spree's headless API caches sparingly — small memoized values (geo data, settings, taxonomy lookups) and the API's rate-limit counters — so the default store needs no dedicated service.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Solid Cache — the default
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Production uses [Solid Cache](https://github.com/rails/solid_cache) — the built-in cache store: the cache lives in your PostgreSQL database alongside everything else. There is nothing to configure, provision, or monitor — it works out of the box in every deployment.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
## Swapping to Redis or Valkey
|
|
13
|
+
|
|
14
|
+
For high-traffic installs that want an in-memory cache, the swap is one line in `config/environments/production.rb` plus the gem:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Gemfile
|
|
18
|
+
gem "redis"
|
|
19
|
+
|
|
20
|
+
# config/environments/production.rb
|
|
21
|
+
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
|
|
14
22
|
```
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
Works identically with Redis or Valkey (the Linux Foundation fork most hosting platforms now provision).
|
|
17
25
|
|
|
18
26
|
## Testing Locally
|
|
19
27
|
|
|
@@ -30,7 +30,7 @@ DATABASE_URL=sqlite3:db/production.sqlite3
|
|
|
30
30
|
|
|
31
31
|
### Using database.yml
|
|
32
32
|
|
|
33
|
-
You can also configure the database in `config/database.yml
|
|
33
|
+
You can also configure the database in `config/database.yml`. The connection pool is sized to cover web request threads plus [background job](background_jobs.md) threads, since jobs run inside the web container by default:
|
|
34
34
|
|
|
35
35
|
**PostgreSQL:**
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ You can also configure the database in `config/database.yml`:
|
|
|
38
38
|
default: &default
|
|
39
39
|
adapter: postgresql
|
|
40
40
|
encoding: unicode
|
|
41
|
-
pool: <%= ENV.fetch("RAILS_MAX_THREADS")
|
|
41
|
+
pool: <%= Integer(ENV.fetch("RAILS_MAX_THREADS", 3)) + Integer(ENV.fetch("JOB_THREADS", 3)) + 3 %>
|
|
42
42
|
|
|
43
43
|
development:
|
|
44
44
|
<<: *default
|
|
@@ -56,7 +56,7 @@ You can also configure the database in `config/database.yml`:
|
|
|
56
56
|
default: &default
|
|
57
57
|
adapter: mysql2
|
|
58
58
|
encoding: utf8mb4
|
|
59
|
-
pool: <%= ENV.fetch("RAILS_MAX_THREADS")
|
|
59
|
+
pool: <%= Integer(ENV.fetch("RAILS_MAX_THREADS", 3)) + Integer(ENV.fetch("JOB_THREADS", 3)) + 3 %>
|
|
60
60
|
|
|
61
61
|
development:
|
|
62
62
|
<<: *default
|
|
@@ -73,7 +73,7 @@ You can also configure the database in `config/database.yml`:
|
|
|
73
73
|
```yaml
|
|
74
74
|
default: &default
|
|
75
75
|
adapter: sqlite3
|
|
76
|
-
pool: <%= ENV.fetch("RAILS_MAX_THREADS")
|
|
76
|
+
pool: <%= Integer(ENV.fetch("RAILS_MAX_THREADS", 3)) + Integer(ENV.fetch("JOB_THREADS", 3)) + 3 %>
|
|
77
77
|
timeout: 5000
|
|
78
78
|
|
|
79
79
|
development:
|