Saleor Production Deploy (GCP, GCE, Docker, Cloudflare DNS, Nginx, GCS, GCP SQL)

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:

  1. By using docker volume mounts this set up sometimes has issues with caching in docker.
  2. 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:

  1. Type: A, Hostname: domain, Value: IP Address of GCE Host
  2. 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:

  1. Click on the name to get to it´s details.
  2. Click on Permissions
  3. You see a message about not being public. On the button is a text link saying make it public. Click it
  4. Still on the Permissions view click on the Add button a bit further down
  5. Enter the name of the service account in the opening dialog and confirm it
  6. 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:[email protected]/$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:[email protected]$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:[email protected]$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

Leave a Reply