Skip to content

Instantly share code, notes, and snippets.

@dwhoop55
Last active June 18, 2024 15:59
Show Gist options
  • Save dwhoop55/9ee2ad2615f50429eb69fdd5bd7c6631 to your computer and use it in GitHub Desktop.
Save dwhoop55/9ee2ad2615f50429eb69fdd5bd7c6631 to your computer and use it in GitHub Desktop.
Deploy versioned Laravel application from git for integration in CI/CD

Laravel zero-downtime deployment

Use this for a directory structure like this:

project-root/

  • [file] deploy.sh
  • staging
    • [dir] storage
    • [file] .env
    • [dir] repos
      • [dir] 12ab34hg67
        • [symlink] .env
        • [symlink] resources
        • ...
      • [dir] 12345678
        • like above
      • [dir] abcdefgh
        • like above
    • [symlink] live
  • production
    • same as with staging, with own resources and .env
  1. "live" will be symlinked to the latest /repos/git-sha deployed
  2. ".env" and "resources" within /repos/git-sha/{.env|resources} will be symlinked to the root .env and resources.
  3. Migrations will be run
  4. Point your webserver to "live" and you have a zero-downtime deployment
  5. To use gitlab CI you need to setup SSH login for user deploy ("deploy") beforehand and add the priv key to gitlab secret variables
  6. You need to setup /etc/sudoers.d/laravel-deploy for the "deploy" user to have sufficient permissions

Attached files

I've attached the files you need to setup supervisor for the queue, sudoer and the logrotate config. Put them into the specified directories. Edit the sudoer config with visudo /etc/sudoers.d/laravel-deploy!

/var/log/laravel-worker-*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
}
deploy ALL=(root) NOPASSWD: /usr/bin/supervisorctl restart laravel-worker-*\:*
deploy ALL=(root) NOPASSWD: /usr/sbin/service php*-fpm restart
deploy ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/*/artisan cache\:clear
# Set this up for staging and production independently!
[program:laravel-worker-{production|staging}]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/ci_cd/project-root/{production|staging}/live/artisan queue:work database --sleep=5 --memory=512 --timeout=600 --tries=5 --delay=120
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker-{production|staging}.log
stopwaitsecs=3600
variables:
DEPLOY_SCRIPT: "/var/www/ci_cd/project-root/deploy"
STAGING_DIR: "staging"
PRODUCTION_DIR: "production"
SSH: "$DEPLOY_USER@$DEPLOY_HOST"
GIT: "git@git.domain.de:group/project.git"
stages:
- deploy
default:
image: alpine
before_script:
- apk update
- apk add openssh
- mkdir -p ~/.ssh
- echo -e "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\tIdentityFile ~/.ssh/id_ed25519\n\n" > ~/.ssh/config'
deploy_staging:
stage: deploy
script:
- ssh $SSH "$DEPLOY_SCRIPT $GIT $STAGING_DIR $CI_COMMIT_SHA"
only:
- master
environment:
name: staging
url: https://staging.domain.de/api/v1
deploy_production:
stage: deploy
script:
- ssh $SSH "$DEPLOY_SCRIPT $GIT $PRODUCTION_DIR $CI_COMMIT_SHA"
only:
- production
environment:
name: production
url: https://production.domain.de/api/v1
#!/bin/bash
# exit when any command fails
set -e
HIGHLIGHT='\e[97m\e[40m\e[1m'
NC='\033[39m\033[49m\e[0m' # Normal
script=$(realpath "$0")
repo="$1"
environment="$2"
commit="$3"
home="$(dirname "$script")/$environment"
user=deploy
repos="$home/repos"
keep=3
gitDepth=5
mkdir -p "$repos"
cd $home
usage() { echo -e "Usage:\n\t${HIGHLIGHT}$0 git@git.domain.de/group/repo.git environment-folder full-commit-sha${NC}\n"; }
say() { echo -e "\n-->> ${HIGHLIGHT}$1${NC}"; }
if [ `whoami` != $user ]; then
echo "Script must be run as user $user."; exit 1
fi
if [ "$repo" == "" ]; then
usage; echo "No git repo given."; exit 1
fi
if [ "$environment" == "" ]; then
usage; echo "No environment folder given."; exit 1
fi
if [ "$commit" == "" ]; then
usage; commit="HEAD"; echo "No commit given. To fetch latest use 'HEAD'"; exit 1
fi
if ! command -v php &> /dev/null; then
echo "Php could not be found."; exit 1
fi
if ! command -v composer &> /dev/null; then
echo "Composer could not be found."; exit 1
fi
clone="$repos/${commit:0:8}"
echo -e "Will use git at\t\t$repo"
echo -e "Will use commit\t\t$commit"
echo -e "Home is set to\t\t$home"
echo -e "Will cone to\t\t$clone"
### Fetching from git
# Clone the repo
if [ ! -d "$clone" ]; then
say "Will now clone to $clone:"
git clone --depth $gitDepth --recurse-submodules "$repo" "$clone"
else
say "Using existing repo at "$clone""
fi
# Set the desired commit
# Notice: git reset will fail with --git-dir and instead clone to current dir (pwd)
say "Hard-reset git at $clone:"
cd "$clone"
git reset --hard $commit
cd "$home"
### Preparing application
# Install Laravel & requirements
say "Installing Laravel and dependencies:"
composer install --working-dir "$clone" --no-dev
composer dumpautoload --working-dir "$clone"
# Comment out if you don't have a post-deploy script defined
composer run-script post-deploy --working-dir "$clone"
# Remove link storage and .env to existing
say "Removing freshly-cloned storage folder from git, and linking it and .env to global:"
rm -rf "$clone/storage"
ln -nfsv "$PWD/storage" "$clone/storage"
ln -nfsv "$PWD/.env" "$clone/.env"
# Remove caches
say "Renewing Laravel caches:"
php "$clone/artisan" opcache:clear
php "$clone/artisan" config:cache
php "$clone/artisan" route:cache
sudo -u www-data php "$clone/artisan" cache:clear
# Will make sure FPM looses its cache
sudo /usr/sbin/service php*-fpm restart
# Give www-data permission to the newly generated caches
chgrp -R www-data "$clone/bootstrap/"
### Migrate database if required
say "Migrating database if required:"
php "$clone/artisan" migrate --force
### Bring the app to live
ln -nfs "$clone" "$home/live"
# Restart queue worker
sudo /usr/bin/supervisorctl restart "laravel-worker-$environment:*"
# Remove any but the last 3 cloned repos
say "Will now purge any but the last $keep versions:"
ls -tp $repos | tail -n +$(($keep+1)) | xargs -I {} rm -rf $repos/{}
say "Process complete. New application from $clone is now live."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment