Skip to main content

Automated Sylius Deployment

· 4 min read
Stephan Hochdörfer
Head of IT Business Operations

GitLab and GitLab CI are crucial for deploying our projects. In our latest Sylius project, we set up an automated CI pipeline for code quality checks and for deploying the Sylius application on the production and staging environment.

Luckily, Sylius already has a Docker setup and a Docker Compose file included to launch all needed containers. One thing less to take care of. However, since it was decided to run both staging and production on the same machine, we had to make a few modifications.

The crucial part is to configure Traefik to make use of multiple Docker networks to avoid any Docker container name collisions. I've blogged about how we usually set this up a while ago.

Compared to the default Docker Compose file that comes with Sylius, we changed the image names, the environment variable section as well as the Docker volumes. The configuration for the PHP service looks like this (and can be adapted to all other services):

services:
php:
container_name: php
image: nexus.loc/customer/sylius/php:${TAG:-latest}
build:
context: .
target: sylius_php_prod
environment:
- "APP_DEBUG=${APP_DEBUG}"
- "APP_ENV=${APP_ENV}"
- "APP_SECRET=${APP_SECRET}"
- "DATABASE_URL=${DATABASE_URL}"
- "MAILER_URL=${MAILER_URL}"
- "MESSENGER_TRANSPORT_DSN=${MESSENGER_TRANSPORT_DSN}"
- "SYLIUS_MESSENGER_TRANSPORT_MAIN_DSN=${SYLIUS_MESSENGER_TRANSPORT_MAIN_DSN}"
- "SYLIUS_MESSENGER_TRANSPORT_MAIN_FAILED_DSN=${SYLIUS_MESSENGER_TRANSPORT_MAIN_FAILED_DSN}"
- "SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_DSN=${SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_DSN}"
- "SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_FAILED_DSN=${SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_FAILED_DSN}"
- "PHP_DATE_TIMEZONE=${PHP_DATE_TIMEZONE}"
- "LOAD_FIXTURES=${LOAD_FIXTURES}"
volumes:
# use a bind-mounted host directory, as we want to keep the sessions
- "/var/www/${TRAEFIK_ENV}/sessions:/srv/sylius/var/sessions:rw"
# use a bind-mounted host directory, as we want to keep the media
- "/var/www/${TRAEFIK_ENV}/public/media/:/srv/sylius/public/media:rw"

For the docker image name, we include the Git tag that was set to kick off the build process. The Git tag is used while building & pushing the docker images to our Docker registry and used during the deployment to pull the matching docker image.

The environment variables are defined in GitLab and passed over to Docker Compose. This way, we can define all the variables for both the staging and the production environment in GitLab, which makes it easier to change them if needed.

Because both the production and staging systems run on the same server, we must ensure that the volume mounts point to different directories. That's what the $TRAEFIK_ENV variable is for. It will either contain the string "staging" or "production". And besides setting the volume mounts for the instance, it is also used for launching the container in the matching docker network, as outlined here.

Since we are using Traefik as Reverse Proxy, the nginx container needs some Traefik labels set:

  nginx:
container_name: nginx
image: nexus.loc/customer/sylius/nginx:${TAG:-latest}
build:
context: .
target: sylius_nginx
depends_on:
- php
volumes:
- "/var/www/${TRAEFIK_ENV}/public/media/:/srv/sylius/public/media:rw"
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.sylius_${TRAEFIK_ENV}_https.redirectscheme.scheme=https"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}.entrypoints=web"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`)"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}.middlewares=sylius_${TRAEFIK_ENV}_https@docker"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`)"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.tls.certresolver=myresolver"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.tls=true"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.entrypoints=websecure"

This instructs Traefik to point the domain defined as $TRAEFIK_HOST GitLab CI variable and its www. subdomain to point to the matching container and enforce HTTPS connections.

Since in this project, we've been using a custom React UI which is served from a different docker container, we can configure Traefik to just route requests for /api, /admin, /build, /bundles, /media urls to this nginx container. Any other request is handled by the nginx container serving the frontend:

  nginx:
container_name: nginx
image: nexus.loc/customer/sylius/nginx:${TAG:-latest}
build:
context: .
target: sylius_nginx
depends_on:
- php
volumes:
- "/var/www/${TRAEFIK_ENV}/public/media/:/srv/sylius/public/media:rw"
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.sylius_${TRAEFIK_ENV}_https.redirectscheme.scheme=https"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}.entrypoints=web"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`) && PathPrefix(`/api`, `/admin`, `/build`, `/bundles`, `/media`)"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}.middlewares=sulu_${TRAEFIK_ENV}_https@docker"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}_https.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`) && PathPrefix(`/api`, `/admin`, `/build`, `/bundles`, `/media`)"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.tls.certresolver=myresolver"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.tls=true"
- "traefik.http.routers.sylius_${TRAEFIK_ENV}_https.entrypoints=websecure"

The configuration for the frontend container looks like this:

services:
frontend:
container_name: frontend
image: nexus.loc/customer/react-nginx:${TAG:-latest}
build:
context: .
target: sylius_frontend
environment:
- REACT_APP_API_URL=https://${TRAEFIK_HOST}
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.frontend_${TRAEFIK_ENV}_https.redirectscheme.scheme=https"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}.entrypoints=web"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`)"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}.middlewares=frontend_${TRAEFIK_ENV}_https@docker"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}_https.rule=Host(`${TRAEFIK_HOST}`, `www.${TRAEFIK_HOST}`)"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}_https.tls.certresolver=myresolver"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}_https.tls=true"
- "traefik.http.routers.frontend_${TRAEFIK_ENV}_https.entrypoints=websecure"

In the GitLab CI configuration, the deployment job is configured like this:

deploy:stage:
stage: deploy
tags: [ customer ]
only:
- tags
except:
- branches
environment:
name: staging
url: https://staging.customer.loc/
script:
- export TAG="${CI_COMMIT_TAG}"
- export COMPOSE_PROJECT_NAME="customer_staging"
- docker compose -f docker-compose.deploy.yml pull
- docker compose -f docker-compose.deploy.yml stop || true
- docker compose -f docker-compose.deploy.yml up -d
allow_failure: false
when: manual

The deployment jobs run on the customer's machine via a dedicated GitLab CI shell runner. Deployments only run when a Git Tag gets created on the master branch and can be kicked off manually by running the deployment job. It first pulls the latest version of the containers, stops the old ones, and starts the new containers. Obviously, this leads to minimal downtime, but that is not a real issue for this specific customer project.

The GitLab CI job for the production deployment looks similar, except that the environment url is different, and the COMPOSE_PROJECT_NAME is set to customer_production.