Deploying a complex e-commerce application like PrestaShop requires a meticulous approach to ensure zero downtime, efficient dependency management, and smooth rollbacks. By leveraging Laravel Envoy and Bitbucket Pipelines, we can automate the entire deployment process while keeping the application online and accessible to users.

In this article, we’ll walk you through a step-by-step process to deploy your PrestaShop application using Laravel Envoy. We’ll cover the directory structure, the Envoy script setup, and a detailed breakdown of each deployment step. The goal is to ensure that your deployment process is efficient, reliable, and maintains seamless operations for your PrestaShop site.

Step 1: Understanding the Directory Structure

The first step in creating a robust deployment strategy is defining a clear directory structure. For this guide, we’ll be using the following directory structure:

$base_dir = '/var/www/prestashop/app';
$releases_dir = "{$base_dir}/releases";
$repo_dir = "{$base_dir}/repo";
$current_dir = "{$base_dir}/current";
$new_release_dir = "{$releases_dir}/{$release}";

Explanation of Directory Structure

  1. $base_dir = '/var/www/prestashop/app':
    This is the main base directory of your PrestaShop application. All other subdirectories, such as releases, repo, and current, are contained within this directory. This setup provides a centralized location for managing multiple releases and deployment files.

  2. $releases_dir = "{$base_dir}/releases":
    This directory stores each release of your application. Every time a new deployment is made, a subdirectory (named using a timestamp) is created within releases. This structure allows you to maintain multiple versions of your application, enabling quick rollbacks if necessary.

  3. $repo_dir = "{$base_dir}/repo":
    The repo directory contains a cloned copy of your Bitbucket repository. Instead of pulling code changes directly into a live environment, they are fetched into this directory first. This approach ensures that your application is not affected by partially completed code pulls.

  4. $current_dir = "{$base_dir}/current":
    The current directory is a symbolic link that points to the latest active release of your application. During deployment, the current link is updated to point to the new release directory without interrupting the live site, ensuring zero downtime.

  5. $new_release_dir = "{$releases_dir}/{$release}":
    This is the directory for the new release being deployed. It is named using a timestamp to ensure each release has a unique directory. The new release is prepared and verified here before updating the current symlink.

Why Use This Directory Structure?

This structure provides an efficient way to handle atomic deployments. By preparing new releases in a separate directory and only switching the current symlink once the deployment is verified, you minimize the risk of downtime or deployment issues. If a problem arises, you can quickly revert to the previous release by updating the current symlink, ensuring a seamless rollback process.

Step 2: Setting Up Laravel Envoy

Laravel Envoy is a deployment tool that simplifies the management of deployment tasks with a minimal syntax. To use it, you need to install Envoy on your local machine and server:

Installing Laravel Envoy

  1. Install Composer (if not already installed):

sudo apt update
sudo apt install composer
  1. Install Laravel Envoy Globally:

composer global require laravel/envoy
  1. Add the Composer Global Vendor Directory to Your PATH:

Open your .bashrc or .bash_profile file and add:

    export PATH="$HOME/.config/composer/vendor/bin:$PATH"

Save the file and run:

    source ~/.bashrc
  1. Verify the Installation:

Run the following command to verify:

    envoy --version

You should see the installed version of Laravel Envoy, indicating that the installation was successful.

Step 3: Creating the Envoy.blade.php File

Now that Envoy is installed, let’s create an Envoy.blade.php file in the root directory of your PrestaShop project. This file will define the deployment tasks and structure.

Envoy File Structure

@servers(['web' => ['deployer@localhost']])

@setup
    // Define the new base directory for the application
    $repository = 'git@bitbucket.org:myproject/prestashop.git';
    $release = date('YmdHis');
    $modulesToInstall = isset($modules) && !empty($modules) && $modules != "_" ? explode(",", $modules) : [];

    // Directory structure with new base path
    $base_dir = '/var/www/prestashop/app';
    $releases_dir = "{$base_dir}/releases";
    $repo_dir = "{$base_dir}/repo";
    $current_dir = "{$base_dir}/current";
    $new_release_dir = "{$releases_dir}/{$release}";
@endsetup

@story('deploy')
    prepare_directories
    update_repository
    install_vendor_dependencies
    copy_assets
    update_permissions
    link_shared_directories
    run_post_update_tasks
    finalize_deployment
    cleanup_old_releases
@endstory

@task('prepare_directories')
    echo 'Preparing directories...';
    [ -d {{ $releases_dir }} ] || mkdir -p {{ $releases_dir }};
@endtask

@task('update_repository')
    echo 'Updating repository and preparing new release...';
    cd {{ $repo_dir }};
    git reset --hard;
    git fetch;
    git pull origin master;

    // Sync the repository contents to the new release directory
    rsync -a --exclude='.git' {{ $repo_dir }}/ {{ $new_release_dir }};
@endtask

@task('install_vendor_dependencies')
    echo 'Installing vendor dependencies...';
    cd {{ $new_release_dir }};
    
    // Install the vendor folder using Composer
    composer install --prefer-dist --no-scripts --no-dev -q -o
@endtask

@task('copy_assets')
    echo 'Copying updated assets...';
    find {{ $repo_dir }}/img -type f -exec cp {} {{ $base_dir }}/img \;
@endtask

@task('update_permissions')
    echo 'Updating file permissions...';
    sudo chown -R www-data:www-data {{ $base_dir }};
    sudo chmod -R 755 {{ $base_dir }};
@endtask

@task('link_shared_directories')
    echo 'Linking shared directories and files...';

    // Link shared directories and files
    ln -nfs {{ $base_dir }}/img {{ $new_release_dir }}/img;
    ln -nfs {{ $base_dir }}/cache {{ $new_release_dir }}/var/cache;
    ln -nfs {{ $base_dir }}/logs {{ $new_release_dir }}/var/logs;
    ln -nfs {{ $base_dir }}/.htaccess {{ $new_release_dir }}/.htaccess;
    ln -nfs {{ $base_dir }}/parameters.php {{ $new_release_dir }}/app/config/parameters.php;

    // Update the current symlink to point to the new release
    ln -nfs {{ $new_release_dir }} {{ $current_dir }};
@endtask

@task('run_post_update_tasks')
    echo 'Running post-deployment tasks...';

    @if (count($modulesToInstall) > 0)
        cd {{ $current_dir }};
        @foreach ($modulesToInstall as $module)
            echo "Installing module: {{ $module }}";
            php bin/console prestashop:module install {{ $module }} --no-debug;
        @endforeach
    @endif

    echo 'Migrating database...';
    cd {{ $current_dir }} && php bin/console doctrine:migrations:migrate --no-interaction;

    echo 'Running custom scripts...';
    cd {{ $current_dir }} && php bin/console custom:scripts;

    echo 'Clearing cache...';
    cd {{ $current_dir }} && php bin/console cache:clear;
@endtask

@task('finalize_deployment')
    echo 'Finalizing deployment...';
    sudo chown -R www-data:www-data {{ $current_dir }};
@endtask

@task('cleanup_old_releases')
    echo 'Cleaning up old releases...';
    # Keep only the last 3 releases to save space
    cd {{ $releases_dir }};
    ls -dt {{ $releases_dir }}/* | tail -n +4 | xargs -d '\n' rm -rf; */
@endtask

@error
    echo 'Deployment failed. Rolling back...';
    ln -nfs {{ $releases_dir }}/previous {{ $current_dir }};
@enderror

Detailed Explanation of Each Task

  1. @setup Block:
    This block initializes all necessary variables, such as the base directory, repository URL, and directory paths. It sets up the release directory structure and the modulesToInstall array if any modules are specified.

  2. prepare_directories:
    Creates the releases and repo directories if they don’t exist. This ensures that the environment is correctly set up before starting the deployment.

  3. update_repository:
    Updates the repository by pulling the latest changes from the master branch. The changes are then copied to the new_release_dir using rsync, excluding the .git directory.

  4. install_vendor_dependencies:
    Installs the vendor folder and dependencies using Composer. This task checks for the presence of composer.json and installs dependencies with optimized autoloading and no development packages.

  5. copy_assets:
    Copies modified assets (e.g., images) from the repository to the live application directory to ensure that new files are correctly updated.

  6. update_permissions:
    Updates ownership and permissions to ensure that the web server has the necessary access to all application files.

  7. link_shared_directories:
    Creates symbolic links for shared directories (e.g., img, cache, logs) to persist data across deployments. It also updates the current symlink to point to the new release directory.

  8. run_post_update_tasks:
    Executes post-deployment tasks like installing modules, running database migrations, executing custom scripts, and clearing the cache.

  9. finalize_deployment:
    Finalizes the deployment by setting ownership of the current release directory to the web server user.

  10. cleanup_old_releases:
    Deletes old releases to free up disk space, keeping only the last three releases for rollback purposes.

@error Handling:
If any task fails during deployment, the @error directive rolls back to the previous stable release by updating the current symlink.

Step 4: Setting Up Bitbucket Pipelines

To automate the deployment process using Bitbucket Pipelines, create a bitbucket-pipelines.yml file:

image: atlassian/default-image:latest

pipelines:
  custom: 
    deploy-to-prod:
      - step:
          name: Deploy to Production
          script:
            - pipe: atlassian/rsync-deploy:0.7.1
              variables:
                USER: $USER
                SERVER: $SERVER
                SSH_PORT: $PORT
                REMOTE_PATH: '/var/www/prestashop/app/repo/Envoy.blade.php'
                LOCAL_PATH: '${BITBUCKET_CLONE_DIR}/Envoy.blade.php'
            - pipe: atlassian/ssh-run:0.7.1
              variables:
                SSH_USER: $USER
                SERVER: $SERVER
                PORT: $PORT
                COMMAND: 'cd /var/www/prestashop/app/repo && ~/.config/composer/vendor/bin/envoy run deploy --modules=${MODULES}'
            - pipe: atlassian/slack-notify:2.2.0
              variables:
                WEBHOOK_URL: $SLACK_WEBHOOK_URL
                MESSAGE: "The project has been successfully deployed to production. Please review the latest changes."

Pipeline Breakdown

  1. Rsync Deployment: Copies the Envoy.blade.php file from your Bitbucket repository to the remote server’s repo directory.

  2. SSH Execution: Executes the envoy run deploy command on the remote server, passing any necessary variables.

  3. Slack Notification: Sends a message to the configured Slack channel to notify the team that the deployment was successful.

Conclusion

By leveraging Laravel Envoy and Bitbucket Pipelines, you have a robust, zero-downtime deployment strategy for your PrestaShop application. This setup ensures that your site remains operational throughout the deployment process, minimizes risks, and allows for easy rollbacks if necessary. With a clear directory structure and automated tasks, your deployment process is more efficient, reliable, and easier to manage.