Problem
You want to deploy a fresh Saleor copy? Or you Boss wants you to take over the process? Look no further.
Requirements
GCP Account
Cloudflare Account
Solution
Description
I will present you a working production config that I have used. It´s main focus is speed of set up, ease and being cheap.
On a high level the dashboard and storefront get compiled on the host machine into folders that are mounted from the host. Those folders are mounted into the nginx container to read from.
In return that are of course some flaws:
- By using docker volume mounts this set up sometimes has issues with caching in docker.
- You need to prune volumes and remove the backend image when you update emails.
Starting position
I expect you to have a working GCE Instance with Docker set up.
DNS
You require 2 DNS entries:
- Type: A, Hostname: domain, Value: IP Address of GCE Host
- Type: A, Hostname: dashboard.domain Value: IP Address of GCE Host
This allows you to address the GraphQL, the Storefront and the Dashboard.
Cloudflare SSL
Log into your account, select your account and your domain.
Once it finished loading, click on “SSL/TLS” and then go to “Origin Server”. Follow the Wizard, enter your information and download the certificates.
We will need it later.
A note:
You can safely rename the file extension of both files to .pem.
Folders/Files
On the server you need to have one folder that will contain everything from this set up, e.g. ~/$user/saleor. We will use /home/amelia as base path in this example.
In there we will put:
- saleor-storefront
- saleor-dashboard
- saleor-backend
- nginx/$domain.nginx
- nginx/cloudflare_ssl
- deployment
GCS (Google Cloud Storage)
We will need permissions for the backend to use the storage, so lets do it first:
Go to IAM &Admin and click Service Accounts. Follow the wizard by giving it a name and permission on the project.
After it was created click on it´s name, go to “KEYS”, click “Add Key” and choose “New Key”.
Select Json and download the file.
Put the file in /home/amelia/deployment.
Now go to the GCS Page and create 2 buckets:
1. For media Images (uploaded via dashboard)
2. For the static Images
For both buckets do this:
- Click on the name to get to it´s details.
- Click on Permissions
- You see a message about not being public. On the button is a text link saying make it public. Click it
- Still on the Permissions view click on the Add button a bit further down
- Enter the name of the service account in the opening dialog and confirm it
- To be quick give it “Storage Admin”
You´re set GCS wise.
GCP SQL PostgreSQL
Create Instance
In the Main Menu search for SQL and click it
Click Create Instance and follow the wizard.
Select the area next to your server, at least 10GB Storage. PostgreSQL Version 11-13 works.
Afer the wizard closed and redirected you, it make take a minute or two to finish.
Configure user
Click on the instance name, then “Users”.
Click ADD USER ACCOUNT. Use saleor as username and a password as you like (remember both).
Create Database
Click Databases and then CREATE DATABASE.
In the dialog give it the name saleor and press create.
Access/Networking
Directly after the Database Server Instance was created you need to allow access to it on the network level.
Click on Connections. Under “Authorized networks” click “ADD NETWORK”.
Give it any name you like and input the ip of your GCE Server.
If you e.g. use VPN with GCP, then it is a good idea to repeat this step and give access to that ip too.
Now you are set SQL wise.
Note:
Only the ip adresses or ranges you configure here are able to access the Database server. If you only add the ip of your GCE Server you can not access the Database from your computer.
Git(Hub)
You want to clone the repositories that hold the following projects into /home/amelia:
- saleor-store
- frontsaleor-dashboard
- saleor-backend
How you do your Git set up is over to you, as this depends on where you host the files, if you use GitHub, GitLab, BitBucket or any other hosting provider. Most of you will likely clone a GitHub repository . And set up a GitHub ssh key to not have to type your password all time.
For the first iterations we expect you to ssh into the machine, update the folders with git pull manually.
And at the end run docker-compose down && docker-compose up -d.
Saleor-backend
Settings.py
saleor-backend v2.x has a known bug, that requires you to add a line to your settings.py:
GS_BUCKET_NAME = GS_STORAGE_BUCKET_NAME
Feel free just to add it at the end.
Saleor Storefront
Adjust the Dockerfile to:
FROM node:10
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG API_URI
ENV API_URI ${API_URI:-http://api:8000/graphql/}
CMD API_URI=${API_URI} npm run build
Saleor Dashboard
Adjust the Dockerfile to:
FROM node:10
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG APP_MOUNT_URI
ARG API_URI
ARG STATIC_URL
ENV API_URI ${API_URI:-http://api:8000/graphql/}
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/}
ENV STATIC_URL ${STATIC_URL:-/dashboard/}
EXPOSE 9000
CMD npm run build
Nginx
Put this file in /home/amelia/nginx/$domain.nginx
The nginx config is mostly similar between v2.10.x and 2.11.x, but there is an important difference:
The genereated url for the app.x.js and app.x.css that are generated for the dashboard are different.
I don´t want you to search for the needle in the haystack, so you can directly go to the corresponding config.
Dashboard v 2.10.x
server {
listen 80;
server_name $domain;
root /app/;
location / {
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
}
server {
listen 443 ssl;
server_name $domain;
uwsgi_max_temp_file_size 20480m;
#Adding comression
gzip on;
gzip_proxied any;
gzip_types
text/css
text/plain
text/javascript
application/javascript
application/json
application/x-javascript
application/xml
application/xml+rss
application/xhtml+xml
application/x-font-ttf
application/x-font-opentype
application/vnd.ms-fontobject
image/svg+xml
image/x-icon
application/rss+xml
application/atom_xml;
gzip_comp_level 7;
gzip_http_version 1.1;
gzip_vary on;
gzip_buffers 24 8k;
gzip_min_length 256;
resolver 127.0.0.11 valid=30s;
ssl_certificate /etc/nginx/certs/$certificateName;
ssl_certificate_key /etc/nginx/certs/$privateKeyName;
root /app/;
location ~* \.(xml)$ {
try_files /sitemap.xml /;
add_header X-loc5 "5";
}
location / {
index index.html;
try_files $uri $uri/ /index.html;
gzip_static on;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
gzip_static on;
expires -1;
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|ico)$ {
expires 365d;
add_header Last-Modified $date_gmt;
}
location ~* \.(css|js|json|map)$ {
expires -1;
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-cache, no-store";
}
}
server {
listen 80;
server_name dashboard.$domain ;
root /dashboard/;
location /dashboard/ {
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
}
server {
listen 443 ssl;
server_name dashboard.$domain;
ssl_certificate /etc/nginx/certs/$certificateName;
ssl_certificate_key /etc/nginx/certs/$privateKeyName;
root /dashboard/;
location / {
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
location /api {
proxy_pass http://api:8000;
}
}
Use this as your Nginx base config.
Replace $domain with your domain name, $certificateName with the file name of your certificate and $privateKeyName with the filename of the private key of the certificate.
Dashboard v2.11.x
server {
listen 80;
server_name $domain;
root /app/;
location / {
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
}
server {
listen 443 ssl;
server_name $domain;
uwsgi_max_temp_file_size 20480m;
#Adding comression
gzip on;
gzip_proxied any;
gzip_types
text/css
text/plain
text/javascript
application/javascript
application/json
application/x-javascript
application/xml
application/xml+rss
application/xhtml+xml
application/x-font-ttf
application/x-font-opentype
application/vnd.ms-fontobject
image/svg+xml
image/x-icon
application/rss+xml
application/atom_xml;
gzip_comp_level 7;
gzip_http_version 1.1;
gzip_vary on;
gzip_buffers 24 8k;
gzip_min_length 256;
resolver 127.0.0.11 valid=30s;
ssl_certificate /etc/nginx/certs/$certificateName;
ssl_certificate_key /etc/nginx/certs/$privateKeyName;
root /app/;
location ~* \.(xml)$ {
try_files /sitemap.xml /;
add_header X-loc5 "5";
}
location / {
index index.html;
try_files $uri $uri/ /index.html;
gzip_static on;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
gzip_static on;
expires -1;
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|ico)$ {
expires 365d;
add_header Last-Modified $date_gmt;
}
location ~* \.(css|js|json|map)$ {
expires -1;
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-cache, no-store";
}
}
server {
listen 80;
server_name dashboard.$domain;
root /dashboard/;
location /dashboard/ {
root /;
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
}
server {
listen 443 ssl;
server_name dashboard.$domain;
ssl_certificate /etc/nginx/certs/$certificateName;
ssl_certificate_key /etc/nginx/certs/$privateKeyName;
root /dashboard/;
location /dashboard/ {
root /;
index index.html;
try_files $uri $uri/ /index.html;
}
location /graphql/ {
proxy_pass http://api:8000/graphql/;
}
location /api {
proxy_pass http://api:8000;
}
}
Use this as your Nginx base config.
Replace $domain with your domain name, $certificateName with the file name of your certificate and $privateKeyName with the filename of the private key of the certificate.
docker-compose.yml
Put this file in /home/amelia
version: '2'
services:
api:
ports:
- 8000:8000
build:
context: ./saleor-backend
dockerfile: ./Dockerfile
args:
STATIC_URL: '/static/'
restart: unless-stopped
networks:
- saleor-backend-tier
depends_on:
- redis
volumes:
- ./saleor-backend/saleor/:/app/saleor:Z
- ./saleor-backend/templates/:/app/templates:Z
- ./saleor-backend/tests/:/app/tests
# prevents overshadowing of build-time assets
- /app/saleor/static/assets
- /app/templates/templated_email/compiled
# shared volume between worker and api for media
- saleor-media:/app/media
logging:
driver: gcplogs
env_file: common.env
command: bash ./entrypoint.sh
environment:
- JAEGER_AGENT_HOST=jaeger
- STOREFRONT_URL=https://$domain/
- DASHBOARD_URL=https://dashboard.$domain/
- DEFAULT_CURRENCY=$yourCurrency
storefront:
build:
context: ./saleor-storefront
dockerfile: ./Dockerfile
ports:
- 3000:3000
volumes:
- ./saleor-storefront/:/app:cached
- /app/node_modules/
logging:
driver: gcplogs
environment:
- API_URI=https://$domain/graphql/
dashboard:
build:
context: ./saleor-dashboard
dockerfile: ./Dockerfile
ports:
- 9000:9000
volumes:
- ./saleor-dashboard/:/app:cached
- /app/node_modules/
logging:
driver: gcplogs
environment:
- API_URI=https://dashboard.$domain/graphql/
redis:
image: library/redis:5.0-alpine
ports:
- 6379:6379
restart: unless-stopped
networks:
- saleor-backend-tier
volumes:
- saleor-redis:/data
logging:
driver: gcplogs
worker:
build:
context: ./saleor-backend
dockerfile: ./Dockerfile
args:
STATIC_URL: '/static/'
command: celery -A saleor worker --app=saleor.celeryconf:app --loglevel=info
restart: unless-stopped
networks:
- saleor-backend-tier
env_file: common.env
depends_on:
- redis
volumes:
- ./saleor-backend/saleor/:/app/saleor:Z,cached
- ./saleor-backend/templates/:/app/templates:Z,cached
# prevents overshadowing of build-time assets
- /app/templates/templated_email/compiled
# shared volume between worker and api for media
- saleor-media:/app/media
logging:
driver: gcplogs
environment:
- JAEGER_AGENT_HOST=jaeger
- STOREFRONT_URL=https://$domain/
- DASHBOARD_URL=https://dashboard.$domain/
- DEFAULT_CURRENCY=$yourCurrency
nginx:
image: nginx:latest
restart: unless-stopped
networks:
- saleor-backend-tier
volumes:
- ./nginx/$domain.nginx:/etc/nginx/conf.d/default.conf
- ./nginx/cloudflare_ssl/:/etc/nginx/certs
- ./saleor-storefront/dist/:/app:cached
- ./saleor-storefront/sitemap.xml:/sitemap.xml
- ./saleor-dashboard/build/dashboard:/dashboard:cached
logging:
driver: gcplogs
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"; chown -R nginx:nginx /app;chown -R nginx:nginx /dashboard;'"
ports:
- 80:80
- 443:443
depends_on:
- api
- dashboard
- storefront
volumes:
saleor-redis:
driver: local
saleor-media:
networks:
saleor-backend-tier:
driver: bridge
As mentioned in a description, this config volume mounts some folders, compiles Dashboard and Storefront on the machine. The nginx container shares the volume mount and can serve the files accordingly.
Replace $yourCurrency with the currency your shop should use and $domain with the domain you want to run it on.
Common.env
Put this file in /home/amelia
DATABASE_URL=postgres://$dbUser:dbPassword@dbIp/$dbName
CELERY_BROKER_URL=redis://redis:6379/1
SECRET_KEY=$secretKey
ALLOWED_HOSTS=localhost,127.0.0.1,api,worker,$domain,dashboard.$domain,$ipAddressOfServer
ALLOWED_CLIENT_HOSTS=localhost,127.0.0.1,api,worker,$domain,dashboard.$domain,$ipAddressOfServer
EMAIL_URL=submission://$emailAddress:$password@$host
GS_PROJECT_ID=$projectId
GS_MEDIA_BUCKET_NAME=$mediaBucketName
GS_STORAGE_BUCKET_NAME=$staticBucketName
GOOGLE_APPLICATION_CREDENTIALS=./deployment/$pathToJsonForGCSServiceAccount
This is a rather default common.env for saleor with our kind of set up.
Replace:
DATABASE_URL should looke something like this: postgres://saleor:$dbPassword@$dbIp/saleor.
Use the credentials from the GCP SQL PostgreSQL Step.
CELERY_BROKER_URL can stay this way, except you have a differnt set up.
$secretKey with any string for the jwt token generation.
ALLOWED_HOSTS/ALLOWED_CLIENT_HOSTS You always edit them as a pair. If you rename the api and/or worker name on the docker-compose.yml, change it here too. Else only replace the $domain and $ipAddressOfServer.
This is important, it configures which (internal) source is allowed to connect. This happens before we even consider CORS.
EMAIL_URL: Replace $emailAddress, $password and $host host.
For Google Mail:
1. use smtp.google.com as the $host.
2. Enable less secure apps
GS_PROJECT_ID: On console.cloud.google.com you have the selector in the header for all projects. Click on it and copy the text that is in the ID column for the entry you want to use.
GS_MEDIA_BUCKET_NAME: Set the name of the bucket you want to use for media that is uploaded via the Dashboard
GS_STORAGE_BUCKET_NAME: Set the name of the bucket you want to use for media the static files (in the static folder in saleor backend, will be copied)
GOOGLE_APPLICATION_CREDENTIALS: The path to the json file for the Service Account that is used to serve content from the Buckets.
Result
You have now the following folder set up:
- /home/amelia
- /home/amelia/docker-compose.yml
- /home/amelia/common.env
- /home/amelia/deployment
- /home/amelia/deployment/$serviceAccount.json
- /home/amelia/nginx
- /home/amelia/nginx/$domain.nginx
- /home/amelia/nginx/cloudflare_ssl
- /home/amelia/saleor-backend
- /home/amelia/saleor-storefront
- /home/amelia/saleor-dashboard
The 3 saleor folders contain the source code of the projects.
Reminder:
You deploy by updating the source code (likely git pull).
Then you run “docker-compose down && docker-compose up -d”.
The whole stack will start. Storefront and Dashboard will start once, run the compilation of the source code into the volume mount folder of the host machine.
Nginx will pick up the changes and serve them.
If you have a caching issue with Nginx, exec into the container and simply run “nginx -s reload”.
That will empty the nginx internal cache, re-read the files and serve the Frontend right.
That also helps if Nginx throws 502 errors for the backend. The reason is normally that when the backend container changes the id, nginx takes a while to update it´s dns cache.
Let me know if it helped you.
Best,
Frank