├── base-images ├── .dockerignore ├── timezone.ini ├── uploads.ini ├── vhost.conf ├── Readme.md ├── build.sh └── Dockerfile.base ├── dt ├── phpunit.Dockerfile ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── _gitignore ├── docker ├── ldap.conf ├── custom_php.ini ├── app-healthcheck ├── app-start ├── docker-stack-wait.sh └── env_key_check.php ├── .drone.yml ├── .env.gitlab ├── .dockerignore ├── copyto ├── .env.github ├── phpunit-compose.yml ├── LICENSE ├── phpunit.github.xml ├── phpunit.gitlab.xml ├── .env.example ├── docker-compose.yml ├── .lando.yml ├── prod-stack.yml ├── Dockerfile ├── qa-stack.yml ├── .gitlab-ci.yml └── Readme.md /base-images/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /base-images/timezone.ini: -------------------------------------------------------------------------------- 1 | date.timezone = "Europe/London" 2 | 3 | -------------------------------------------------------------------------------- /dt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f phpunit-compose.yml up --build --exit-code-from phpunit 4 | 5 | -------------------------------------------------------------------------------- /base-images/uploads.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | memory_limit = 1024M 3 | upload_max_filesize = 64M 4 | post_max_size = 64M 5 | max_execution_time = 600 6 | -------------------------------------------------------------------------------- /base-images/vhost.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | DocumentRoot /var/www/html/public 4 | PassEnv HOSTNAME 5 | Header add X-Container "%{HOSTNAME}e" 6 | 7 | 8 | AllowOverride all 9 | Require all granted 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /phpunit.Dockerfile: -------------------------------------------------------------------------------- 1 | ### PHP version we are targetting 2 | ARG PHP_VERSION=7.4 3 | 4 | FROM uogsoe/soe-php-apache:${PHP_VERSION} as prod 5 | 6 | WORKDIR /var/www/html 7 | 8 | USER nobody 9 | 10 | ENV APP_ENV=testing 11 | ENV APP_DEBUG=1 12 | 13 | RUN php artisan migrate 14 | 15 | CMD ["./vendor/bin/phpunit", "--testdox", "--stop-on-defect"] 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | # Maintain dependencies for Composer 11 | - package-ecosystem: "composer" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /_gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /storage/minio_dev/bucket/* 6 | !/storage/minio_dev/bucket/.gitkeep 7 | /storage/minio_dev/.minio.sys 8 | /storage/meilisearch/* 9 | !/storage/meilisearch/.gitkeep* 10 | /vendor 11 | .env 12 | .env.backup 13 | .phpunit.result.cache 14 | docker-compose.override.yml 15 | Homestead.json 16 | Homestead.yaml 17 | npm-debug.log 18 | yarn-error.log 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /docker/ldap.conf: -------------------------------------------------------------------------------- 1 | # 2 | # LDAP Defaults 3 | # 4 | 5 | # See ldap.conf(5) for details 6 | # This file should be world readable but not world writable. 7 | 8 | #BASE dc=example,dc=com 9 | #URI ldap://ldap.example.com ldap://ldap-master.example.com:666 10 | 11 | #SIZELIMIT 12 12 | #TIMELIMIT 15 13 | #DEREF never 14 | 15 | # TLS certificates (needed for GnuTLS) 16 | TLS_CACERT /etc/ssl/certs/ca-certificates.crt 17 | TLS_REQCERT never 18 | 19 | -------------------------------------------------------------------------------- /docker/custom_php.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | 3 | memory_limit = 1024M 4 | upload_max_filesize = 64M 5 | post_max_size = 64M 6 | max_execution_time = 600 7 | 8 | [opcache] 9 | 10 | opcache.enable=1 11 | opcache.revalidate_freq=0 12 | opcache.validate_timestamps=0 13 | opcache.max_accelerated_files=20000 14 | opcache.memory_consumption=512 15 | opcache.max_wasted_percentage=10 16 | opcache.interned_strings_buffer=64 17 | opcache.fast_shutdown=1 18 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: build 6 | image: docker 7 | commands: 8 | - uname -a 9 | - apk --no-cache add bash wget 10 | - mkdir ~/.docker 11 | - mkdir ~/.docker/cli-plugins 12 | - wget -O ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.3.0/buildx-v0.3.0.linux-amd64 13 | - chmod +x ~/.docker/cli-plugins/docker-buildx 14 | - docker buildx create --name mybuilder 15 | - docker buildx use mybuilder 16 | - ./base-images/build.sh 17 | 18 | -------------------------------------------------------------------------------- /.env.gitlab: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=mysql 12 | DB_PORT=3306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=homestead 15 | DB_PASSWORD=secret 16 | 17 | BROADCAST_DRIVER=log 18 | CACHE_DRIVER=file 19 | SESSION_DRIVER=file 20 | SESSION_LIFETIME=120 21 | QUEUE_CONNECTION=sync 22 | QUEUE_NAME=whatever 23 | 24 | REDIS_HOST=redis 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_DRIVER=log 29 | 30 | LDAP_SERVER= 31 | LDAP_OU= 32 | LDAP_USERNAME= 33 | LDAP_PASSWORD= 34 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug 2 | Dockerfile 3 | stack.yml 4 | npm-debug.log 5 | yarn-error.log 6 | .dockerignore 7 | **/mix-manifest.json 8 | .env 9 | .DS_Store 10 | .idea 11 | .git/ 12 | node_modules/ 13 | vendor/ 14 | /public/hot 15 | /public/storage 16 | /public/js/* 17 | /public/css/* 18 | /public/chunks/* 19 | /storage/*.key 20 | /storage/framework/views/* 21 | /storage/framework/sessions/* 22 | /storage/framework/testing/* 23 | /storage/framework/cache/data/* 24 | /storage/debugbar/* 25 | /storage/logs/* 26 | /storage/medialibrary/* 27 | /storage/app/* 28 | /tests/Browser/console/* 29 | /tests/Browser/images/* 30 | /tests/Browser/screenshots/* 31 | /bootstrap/cache/* 32 | 33 | -------------------------------------------------------------------------------- /docker/app-healthcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | role=${CONTAINER_ROLE:-app} 6 | 7 | if [ "$role" = "app" ]; then 8 | 9 | curl -f http://localhost/ || exit 1 10 | exit 0 11 | 12 | elif [ "$role" = "queue" ]; then 13 | 14 | php /var/www/html/artisan horizon:status | grep -q 'Horizon is running' || exit 1 15 | exit 0 16 | 17 | elif [ "$role" = "scheduler" ]; then 18 | 19 | # need to figure something out for this... if at all checkable 20 | exit 0 21 | 22 | elif [ "$role" = "migrations" ]; then 23 | 24 | # nothing to do here 25 | exit 0 26 | 27 | else 28 | echo "Could not match the container role \"$role\"" 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /copyto: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple script to copy the files to a project 4 | 5 | set -e 6 | 7 | if [ "$#" -ne 1 ]; then 8 | echo "You need to supply a path..." 9 | echo 10 | echo "copyto ~/code/some-project-name/" 11 | exit 1 12 | fi 13 | 14 | cp -av docker .dockerignore .env.example .lando.yml .env.gitlab .github .env.github .gitlab-ci.yml Dockerfile prod-stack.yml qa-stack.yml docker-compose.yml LICENSE .github .env.github phpunit.github.xml phpunit.gitlab.xml "$1" 15 | cp -av _gitignore "$1/.gitignore" 16 | mkdir -p "$1/storage/minio_dev/bucket" 17 | mkdir -p "$1/storage/meilisearch" 18 | touch "$1/storage/minio_dev/bucket/.gitkeep" 19 | touch "$1/storage/meilisearch/.gitkeep" 20 | 21 | -------------------------------------------------------------------------------- /base-images/Readme.md: -------------------------------------------------------------------------------- 1 | # Dockerfile for PHP7 and Apache 2 | 3 | This is just a base Dockerfile used as the base for our CI and production images. It gives you Apache, PHP 7.x and a few commonly used libraries that PHP/Laravel apps use. 4 | 5 | ## To use from the docker hub 6 | 7 | Eg: 8 | 9 | ``` 10 | // for php7.2 + composer 11 | docker pull uogsoe/soe-php-apache:7.2 12 | // for php7.2 + composer + pcov 13 | docker pull uogsoe/soe-php-apache:7.2-ci 14 | ``` 15 | 16 | ## Building the images 17 | 18 | Just run the `build.sh` script. It needs to have the [buildx](https://github.com/docker/buildx) docker feature enabled if you don't already have it. 19 | 20 | The script will by default build images for each PHP version in the `VERSIONS` array defined in the script. It uses buildx to do multiple architectures for each PHP version : 21 | 22 | - linux/amd64 23 | - linux/arm/v7 24 | 25 | -------------------------------------------------------------------------------- /.env.github: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY=base64:Q2CI9s88ePYqdrGvLc/q+r524KYMO6ON7C3Ujkn/OBw= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=127.0.0.1 12 | DB_PORT=33306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=root 15 | DB_PASSWORD=homestead 16 | 17 | BROADCAST_DRIVER=log 18 | CACHE_DRIVER=array 19 | SESSION_DRIVER=array 20 | SESSION_LIFETIME=120 21 | QUEUE_CONNECTION=sync 22 | QUEUE_NAME=whatever 23 | 24 | REDIS_HOST=redis 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_MAILER=log 29 | 30 | LDAP_SERVER= 31 | LDAP_OU= 32 | LDAP_USERNAME= 33 | LDAP_PASSWORD= 34 | 35 | MINIO_KEY=abcd1234 36 | MINIO_SECRET=hello 37 | MINIO_REGION=us-east-1 38 | MINIO_BUCKET=test 39 | MINIO_ENDPOINT=http://localhost:9000 40 | 41 | WKHTML_PDF_BINARY=/usr/bin/wkhtmltopdf 42 | PDF_API_URL=http://whatever:9005/convert/url 43 | -------------------------------------------------------------------------------- /phpunit-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | # See https://github.com/UoGSoE/docker-stuff for the origins of this file 4 | 5 | services: 6 | phpunit: 7 | build: 8 | context: . 9 | dockerfile: phpunit.Dockerfile 10 | args: 11 | PHP_VERSION: "${PHP_VERSION:-7.4}" 12 | depends_on: 13 | mysql: 14 | condition: service_healthy 15 | tmpfs: 16 | - /var/www/html/storage/logs 17 | - /var/www/html/storage/framework/cache 18 | environment: 19 | DB_CONNECTION: mysql 20 | DB_HOST: mysql-test 21 | DB_DATABASE: homestead 22 | DB_USERNAME: homestead 23 | DB_PASSWORD: secret 24 | volumes: 25 | - .:/var/www/html:delegated 26 | 27 | mysql-test: 28 | image: mysql:5.7 29 | environment: 30 | MYSQL_ROOT_PASSWORD: root 31 | MYSQL_DATABASE: homestead 32 | MYSQL_USER: homestead 33 | MYSQL_PASSWORD: secret 34 | healthcheck: 35 | test: /usr/bin/mysql --host=127.0.0.1 --user=homestead --password=secret --silent --execute \"SELECT 1;\" 36 | interval: 3s 37 | timeout: 20s 38 | retries: 5 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 University of Glasgow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /phpunit.github.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Feature 15 | 16 | 17 | 18 | ./tests/Unit 19 | 20 | 21 | 22 | 23 | ./app 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /phpunit.gitlab.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Feature 15 | 16 | 17 | 18 | ./tests/Unit 19 | 20 | 21 | 22 | 23 | ./app 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docker/app-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | role=${CONTAINER_ROLE:-app} 6 | env=${APP_ENV:-production} 7 | 8 | if [ "${env}" != "production" ]; 9 | then 10 | # in QA/testing we wait for the local mysql container to come up 11 | until nc -z -v -w30 mysql 3306 12 | do 13 | echo "Waiting for database connection..." 14 | sleep 5 15 | done 16 | fi 17 | 18 | until echo 'PING' | nc -w 1 redis 6379 | grep -q PONG 19 | do 20 | echo "Waiting for Redis connection..." 21 | sleep 5 22 | done 23 | 24 | php /var/www/html/artisan config:cache 25 | 26 | if [ "$role" = "app" ]; then 27 | 28 | exec apache2-foreground 29 | 30 | elif [ "$role" = "queue" ]; then 31 | 32 | trap "gosu www-data php /var/www/html/artisan horizon:terminate" SIGTERM 33 | exec gosu www-data nice php /var/www/html/artisan horizon 34 | 35 | elif [ "$role" = "scheduler" ]; then 36 | 37 | exec gosu www-data nice php /var/www/html/artisan schedule:work --verbose --no-interaction 38 | # while [ true ] 39 | # do 40 | # gosu www-data nice php /var/www/html/artisan schedule:run --verbose --no-interaction & 41 | # sleep 60 42 | # done 43 | 44 | elif [ "$role" = "migrations" ]; then 45 | 46 | gosu www-data php /var/www/html/artisan migrate --force 47 | while [ true ] 48 | do 49 | sleep 86400 50 | done 51 | 52 | elif [ "$role" = "test" ]; then 53 | 54 | exec gosu www-data php /var/www/html/vendor/bin/phpunit --colors=never 55 | 56 | else 57 | echo "Could not match the container role \"$role\"" 58 | exit 1 59 | fi 60 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY=base64:UQ8adVyjpWckL0gvNLfoq2eWaIDYt8CaLnYi17oHTHU= 4 | APP_DEBUG=true 5 | APP_URL=http://projman.test 6 | 7 | APP_LOCALE=en 8 | APP_FALLBACK_LOCALE=en 9 | APP_FAKER_LOCALE=en_US 10 | 11 | APP_MAINTENANCE_DRIVER=file 12 | # APP_MAINTENANCE_STORE=database 13 | 14 | PHP_CLI_SERVER_WORKERS=4 15 | 16 | BCRYPT_ROUNDS=12 17 | 18 | LOG_CHANNEL=stack 19 | LOG_STACK=single 20 | LOG_DEPRECATIONS_CHANNEL=null 21 | LOG_LEVEL=debug 22 | 23 | DB_CONNECTION=mysql 24 | DB_HOST=database 25 | DB_DATABASE=laravel 26 | DB_USERNAME=laravel 27 | DB_PASSWORD=laravel 28 | DB_PORT=3306 29 | 30 | SESSION_DRIVER=redis 31 | SESSION_LIFETIME=120 32 | SESSION_ENCRYPT=false 33 | SESSION_PATH=/ 34 | SESSION_DOMAIN=null 35 | 36 | BROADCAST_CONNECTION=log 37 | FILESYSTEM_DISK=local 38 | QUEUE_CONNECTION=redis 39 | 40 | CACHE_STORE=redis 41 | # CACHE_PREFIX= 42 | 43 | MEMCACHED_HOST=127.0.0.1 44 | 45 | REDIS_CLIENT=phpredis 46 | REDIS_HOST=cache 47 | REDIS_PASSWORD=null 48 | REDIS_PORT=6379 49 | 50 | MAIL_MAILER=smtp 51 | MAIL_SCHEME=null 52 | MAIL_HOST=sendmailhog 53 | MAIL_PORT=1025 54 | MAIL_USERNAME=null 55 | MAIL_PASSWORD=null 56 | MAIL_FROM_ADDRESS="hello@example.ac.uk" 57 | MAIL_FROM_NAME="${APP_NAME}" 58 | 59 | AWS_ACCESS_KEY_ID= 60 | AWS_SECRET_ACCESS_KEY= 61 | AWS_DEFAULT_REGION=us-east-1 62 | AWS_BUCKET= 63 | AWS_USE_PATH_STYLE_ENDPOINT=false 64 | 65 | VITE_APP_NAME="${APP_NAME}" 66 | 67 | LDAP_SERVER= 68 | LDAP_OU= 69 | LDAP_USERNAME= 70 | LDAP_PASSWORD= 71 | LDAP_AUTHENTICATION=false 72 | 73 | MINIO_KEY=minioadmin 74 | MINIO_SECRET=minioadmin 75 | MINIO_BUCKET=bucket 76 | MINIO_REGION=us-east-1 77 | MINIO_ENDPOINT=http://minio:9000/ 78 | MINIO_URL=http://minio:9000/bucket/ 79 | 80 | SCOUT_DRIVER=meilisearch 81 | MEILISEARCH_HOST=http://meilisearch:7700 82 | MEILISEARCH_KEY=secretkey 83 | 84 | -------------------------------------------------------------------------------- /base-images/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Note: you need the 'buildx' feature of docker enabled to run this so we can 4 | # build images for x86, arm etc. eg : 5 | # $ docker buildx create --name mybuilder 6 | # $ docker buildx use mybuilder 7 | # $ docker buildx build --build-arg PHP_VERSION=7.3 --platform linux/amd64,linux/arm64,linux/arm/v7 -t myimage:latest . 8 | # 9 | 10 | ABSOLUTE_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 11 | DOCKER_FILE="${ABSOLUTE_PATH}/Dockerfile.base" 12 | BASE_NAME="uogsoe/soe-php-apache" 13 | # Note: these should be in ascending order - the ':latest' tag is taken from the last element 14 | VERSIONS=( "7.1" "7.2" "7.3" "7.4" ) 15 | LATEST=${VERSIONS[@]: -1:1} 16 | #CMD="docker buildx build --pull --push --no-cache --platform linux/amd64,linux/arm/v7" 17 | CMD="docker buildx build --pull --push --platform linux/amd64,linux/arm/v7" 18 | PNAME=`basename $0` 19 | LOGFILE=`mktemp /tmp/${PNAME}.XXXXXX` || exit 1 20 | export DOCKER_BUILDKIT=1 21 | 22 | docker buildx &> /dev/null 23 | if [ $? -ne 0 ] 24 | then 25 | echo "Aborting." 26 | echo "You need to have docker buildx available. See https://github.com/docker/buildx" 27 | exit 1 28 | fi 29 | 30 | set -e 31 | 32 | echo "Logging to ${LOGFILE}" 33 | 34 | for VERSION in "${VERSIONS[@]}"; 35 | do 36 | echo "Building ${VERSION}..." 37 | $CMD --target=prod --build-arg PHP_VERSION=${VERSION} -t "${BASE_NAME}":"${VERSION}" -f ${DOCKER_FILE} ${ABSOLUTE_PATH} >> "${LOGFILE}" 38 | 39 | echo "Building ${VERSION}-ci..." 40 | ${CMD} --target=ci --build-arg PHP_VERSION=${VERSION} -t "${BASE_NAME}":"${VERSION}"-ci -f ${DOCKER_FILE} ${ABSOLUTE_PATH} >> "${LOGFILE}" 41 | done 42 | 43 | echo "Tagging latest from ${LATEST}..." 44 | $CMD --target=prod --build-arg PHP_VERSION=${LATEST} -t "${BASE_NAME}":latest -f ${DOCKER_FILE} ${ABSOLUTE_PATH} >> "${LOGFILE}" 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | # See https://github.com/UoGSoE/docker-stuff for the origins of this file 4 | 5 | x-env: 6 | environment: &default-env 7 | MAIL_DRIVER: smtp 8 | MAIL_HOST: mailhog 9 | MAIL_PORT: 1025 10 | REDIS_HOST: redis 11 | QUEUE_CONNECTION: redis 12 | SESSION_DRIVER: redis 13 | DB_CONNECTION: mysql 14 | DB_HOST: mysql 15 | DB_PORT: 3306 16 | DB_DATABASE: homestead 17 | DB_USERNAME: homestead 18 | DB_PASSWORD: secret 19 | 20 | services: 21 | app: 22 | image: ${IMAGE_NAME} 23 | environment: 24 | CONTAINER_ROLE: app 25 | <<: *default-env 26 | ports: 27 | - "${APP_PORT:-3000}:80" 28 | build: 29 | context: . 30 | target: dev 31 | volumes: 32 | - .:/var/www/html 33 | secrets: 34 | - source: dotenv 35 | target: .env 36 | depends_on: 37 | - redis 38 | - mysql 39 | - mailhog 40 | 41 | scheduler: 42 | image: ${IMAGE_NAME} 43 | environment: 44 | CONTAINER_ROLE: scheduler 45 | <<: *default-env 46 | depends_on: 47 | - app 48 | volumes: 49 | - .:/var/www/html 50 | secrets: 51 | - source: dotenv 52 | target: .env 53 | 54 | queue: 55 | image: ${IMAGE_NAME} 56 | environment: 57 | CONTAINER_ROLE: queue 58 | <<: *default-env 59 | depends_on: 60 | - app 61 | volumes: 62 | - .:/var/www/html 63 | secrets: 64 | - source: dotenv 65 | target: .env 66 | 67 | migrations: 68 | image: ${IMAGE_NAME} 69 | environment: 70 | CONTAINER_ROLE: migrations 71 | <<: *default-env 72 | depends_on: 73 | - app 74 | volumes: 75 | - .:/var/www/html 76 | secrets: 77 | - source: dotenv 78 | target: .env 79 | 80 | redis: 81 | image: redis:5.0.4 82 | volumes: 83 | - redis:/data 84 | 85 | mysql: 86 | image: mysql:5.7 87 | volumes: 88 | - mysql:/var/lib/mysql 89 | environment: 90 | MYSQL_DATABASE: homestead 91 | MYSQL_ROOT_PASSWORD: root 92 | MYSQL_USER: homestead 93 | MYSQL_PASSWORD: secret 94 | 95 | mailhog: 96 | image: mailhog/mailhog 97 | ports: 98 | - "3025:8025" 99 | 100 | volumes: 101 | redis: 102 | driver: "local" 103 | mysql: 104 | driver: "local" 105 | 106 | secrets: 107 | dotenv: 108 | file: ./.env 109 | -------------------------------------------------------------------------------- /.lando.yml: -------------------------------------------------------------------------------- 1 | name: 2 | recipe: laravel 3 | config: 4 | webroot: public 5 | php: '8.4' 6 | cache: redis 7 | services: 8 | mail: 9 | type: mailhog 10 | portforward: true 11 | hogfrom: 12 | - appserver 13 | appserver: 14 | scanner: 15 | okCodes: 16 | 401 17 | 200 18 | # meilisearch: 19 | # type: compose 20 | # app_mount: false 21 | # services: 22 | # image: getmeili/meilisearch:v0.27.0 23 | # command: tini -- /bin/meilisearch 24 | # volumes: 25 | # - ./storage/meilisearch:/meili_data 26 | # minio: 27 | # type: compose 28 | # app_mount: false 29 | # services: 30 | # image: quay.io/minio/minio:RELEASE.2022-03-08T22-28-51Z 31 | # command: minio server /data --console-address ":9001" 32 | # volumes: 33 | # - ./storage/minio_dev:/data 34 | # environment: 35 | # MINIO_ROOT_USER: minioadmin 36 | # MINIO_ROOT_PASSWORD: minioadmin 37 | # MINIO_REGION: "us-east-1" 38 | # MINIO_BUCKET: "exampapers" 39 | # MINIO_ENDPOINT: "${MINIO_QA_ENDPOINT}" 40 | node: 41 | type: node 42 | build: 43 | - npm install 44 | 45 | excludes: 46 | - node_modules 47 | tooling: 48 | mfs: 49 | service: appserver 50 | description: "Drop db, migrate and seed" 51 | cmd: php artisan migrate:fresh && php artisan db:seed --class=TestDataSeeder 52 | horizon: 53 | service: appserver 54 | description: "Run horizon" 55 | cmd: php artisan horizon 56 | test: 57 | service: appserver 58 | description: "Run phpunit" 59 | cmd: CI=1 php artisan test --parallel 60 | testf: 61 | service: appserver 62 | description: "Run phpunit with --filter" 63 | cmd: php artisan test --filter 64 | fixldap: 65 | service: appserver 66 | description: "Set up insecure ldap" 67 | user: root 68 | cmd: apt-get update && apt-get install -y libldap-common && printf "\nTLS_REQCERT never\n" >> /etc/ldap/ldap.conf 69 | fixmysql: 70 | service: database 71 | description: "Fix AIO stopping mysql starting up" 72 | user: root 73 | cmd: echo "\n\n" >> /opt/bitnami/mysql/conf/bitnami/my_custom.cnf 74 | ci: 75 | service: appserver 76 | description: "Run pseudo CI run" 77 | cmd: 78 | - set -e 79 | - composer global require enlightn/security-checker 80 | - curl -OL -o /usr/local/bin/phpcs https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar 81 | - php /usr/local/bin/phpcs --config-set ignore_warnings_on_exit 1 82 | - php /usr/local/bin/phpcs --standard=PSR12 app 83 | - egrep -r '[^a-zA-Z](dd\(|dump\(|ray\()' app 84 | - php /root/.composer/vendor/bin/security-checker security:check ./composer.lock 85 | npm: 86 | service: node 87 | description: "Run npm command" 88 | cmd: npm 89 | npmd: 90 | service: node 91 | description: "Run npm run dev" 92 | cmd: npm run development 93 | npmp: 94 | service: node 95 | description: "Run npm run prod" 96 | cmd: npm run production 97 | npmw: 98 | service: node 99 | description: "Run npm run watch" 100 | cmd: npm run watch 101 | -------------------------------------------------------------------------------- /prod-stack.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | x-logging: 4 | &default-logging 5 | options: 6 | max-size: '12m' 7 | max-file: '5' 8 | driver: json-file 9 | x-deploy-php: 10 | &default-php-deploy 11 | replicas: 1 12 | labels: 13 | - "traefik.enable=false" 14 | placement: 15 | constraints: 16 | - "node.labels.weaksauce==false" 17 | resources: 18 | limits: 19 | memory: 2048M 20 | cpus: "1" 21 | reservations: 22 | memory: 200M 23 | update_config: 24 | parallelism: 1 25 | failure_action: rollback 26 | order: start-first 27 | x-deploy: 28 | &default-deploy 29 | replicas: 1 30 | labels: 31 | - "traefik.enable=false" 32 | placement: 33 | constraints: 34 | - "node.labels.weaksauce==false" 35 | resources: 36 | limits: 37 | memory: 2048M 38 | update_config: 39 | parallelism: 1 40 | failure_action: rollback 41 | order: start-first 42 | 43 | services: 44 | app: 45 | image: ${IMAGE_NAME} 46 | networks: 47 | - proxy 48 | - default 49 | expose: 50 | - "80" 51 | environment: 52 | CONTAINER_ROLE: app 53 | secrets: 54 | - source: dotenv 55 | target: .env 56 | deploy: 57 | replicas: 2 58 | placement: 59 | constraints: 60 | - "node.labels.weaksauce==false" 61 | preferences: 62 | - spread: node.labels.site 63 | update_config: 64 | parallelism: 1 65 | failure_action: rollback 66 | order: start-first 67 | resources: 68 | limits: 69 | memory: 2048M 70 | reservations: 71 | memory: 128M 72 | labels: 73 | # note: this assumes traefik v2 74 | - "traefik.enable=true" 75 | - "traefik.http.routers.${TRAEFIK_BACKEND}.rule=Host(`${TRAEFIK_HOSTNAME}`)" 76 | - "traefik.http.routers.${TRAEFIK_BACKEND}.entrypoints=web" 77 | - "traefik.http.services.${TRAEFIK_BACKEND}.loadbalancer.server.port=80" # it seems you always need to give traefik a port so it 'notices' the service 78 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.rule=Host(`${TRAEFIK_HOSTNAME}`)" 79 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.entrypoints=web-secured" 80 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.tls.certresolver=mytlschallenge" 81 | stop_grace_period: 2m 82 | logging: *default-logging 83 | 84 | scheduler: 85 | image: ${IMAGE_NAME} 86 | deploy: *default-php-deploy 87 | stop_grace_period: 1m 88 | networks: 89 | - default 90 | environment: 91 | CONTAINER_ROLE: scheduler 92 | secrets: 93 | - source: dotenv 94 | target: .env 95 | logging: *default-logging 96 | 97 | queue: 98 | image: ${IMAGE_NAME} 99 | deploy: *default-php-deploy 100 | stop_grace_period: 1m 101 | networks: 102 | - default 103 | environment: 104 | CONTAINER_ROLE: queue 105 | secrets: 106 | - source: dotenv 107 | target: .env 108 | logging: *default-logging 109 | 110 | migrations: 111 | image: ${IMAGE_NAME} 112 | deploy: *default-php-deploy 113 | stop_grace_period: 1m 114 | networks: 115 | - default 116 | environment: 117 | CONTAINER_ROLE: migrations 118 | secrets: 119 | - source: dotenv 120 | target: .env 121 | 122 | redis: 123 | # redis v5.0.5 tag as of 2019-06-25 124 | # you can get the sha of an image by doing : 125 | # docker pull redis:5.0.5 126 | # docker images --digests | grep redis 127 | image: redis@sha256:f0957bcaa75fd58a9a1847c1f07caf370579196259d69ac07f2e27b5b389b021 128 | deploy: *default-deploy 129 | logging: *default-logging 130 | networks: 131 | - default 132 | healthcheck: 133 | test: ["CMD", "redis-cli", "ping"] 134 | 135 | secrets: 136 | dotenv: 137 | external: true 138 | name: ${DOTENV_NAME} 139 | 140 | networks: 141 | default: 142 | proxy: 143 | external: true 144 | -------------------------------------------------------------------------------- /base-images/Dockerfile.base: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION 2 | 3 | FROM php:${PHP_VERSION}-apache as prod 4 | 5 | ARG PHP_VERSION 6 | ARG PHP_REDIS_VERSION=5.0.2 7 | ARG COMPOSER_VERSION=2.0.1 8 | 9 | LABEL org.opencontainers.image.source=https://github.com/UoGSoE/docker-stuff/ \ 10 | org.opencontainers.image.vendor="University of Glasgow, School of Engineering" \ 11 | org.opencontainers.image.licenses="MIT" \ 12 | org.opencontainers.image.title="PHP ${PHP_VERSION} + Apache" \ 13 | org.opencontainers.image.description="PHP ${PHP_VERSION} with Apache and a set of php/os packages suitable for running Laravel apps" 14 | 15 | # our default timezone and langauge 16 | ENV TZ=Europe/London 17 | ENV LANG=en_GB.UTF-8 18 | 19 | # Note: we only install reliable/core 1st-party php extensions here. 20 | # If your app needs custom ones install them in the apps own 21 | # Dockerfile _and pin the versions_! Eg: 22 | # RUN pecl install memcached-2.2.0 23 | 24 | RUN apt-get update \ 25 | # install some OS packages we need 26 | && apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl sqlite3 libsqlite3-dev libzip-dev unzip vim-tiny gosu git \ 27 | # install php extensions 28 | && case "${PHP_VERSION}" in "7.4"|"rc") docker-php-ext-configure gd --with-freetype --with-jpeg ;; *) docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ ;; esac \ 29 | && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_sqlite zip gmp bcmath pcntl ldap sysvmsg exif \ 30 | # install the redis php extension 31 | && pecl install redis-${PHP_REDIS_VERSION} \ 32 | && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini \ 33 | # clear the apt cache 34 | && rm -rf /var/lib/apt/lists/* /var/cache/debconf/templates* /var/lib/dpkg/status/* /var/log/dpkg.log /var/log/apt/term.log /var/cache/debconf/config.dat \ 35 | # enable apache mod_rewrite for 'pretty' urls and mod_headers so we can add the container id 36 | && a2enmod rewrite \ 37 | && a2enmod headers \ 38 | # give apache access to the system hostname (ie, the container-id) 39 | && echo 'export HOSTNAME=`uname -n`' >> /etc/apache2/envvars \ 40 | # install composer 41 | && curl -o /tmp/composer-setup.php https://getcomposer.org/installer \ 42 | && curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \ 43 | && php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \ 44 | && php /tmp/composer-setup.php --version=${COMPOSER_VERSION} --no-ansi --install-dir=/usr/local/bin --filename=composer --snapshot \ 45 | && rm -f /tmp/composer-setup.* \ 46 | # set the system timezone 47 | && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ 48 | && echo $TZ > /etc/timezone 49 | 50 | # copy in the generic vhost for our 'default' app setup 51 | COPY vhost.conf /etc/apache2/sites-available/000-default.conf 52 | # add in the basic php ini settings for uploading files and our timezone 53 | COPY uploads.ini timezone.ini /usr/local/etc/php/conf.d/ 54 | # and expose apache to docker 55 | EXPOSE 80 56 | 57 | FROM prod as ci 58 | 59 | ARG PCOV_VERSION=1.0.6 60 | 61 | # The only additions for CI/QA is the 'pcov' extension by PHP internals developer 62 | # Joe Watkins (it provides code-coverage statistics without slowing down code. 63 | # https://github.com/krakjoe/pcov) 64 | ENV DRIVER pcov 65 | # pecl install currently commented out as no pre-built package for ARM 66 | #RUN pecl install pcov \ 67 | # && docker-php-ext-enable pcov \ 68 | # && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini 69 | WORKDIR /tmp 70 | RUN apt-get update \ 71 | && curl -sLo pcov.tar.gz https://github.com/krakjoe/pcov/archive/v${PCOV_VERSION}.tar.gz \ 72 | && tar -xvzf pcov.tar.gz \ 73 | && cd pcov-${PCOV_VERSION} \ 74 | && phpize \ 75 | && ./configure --enable-pcov \ 76 | && make \ 77 | && make install \ 78 | && cd .. \ 79 | && rm -fr pcov* 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### PHP version we are targetting 2 | ARG PHP_VERSION=8.4 3 | 4 | 5 | ### Placeholder for basic dev stage for use with docker-compose 6 | FROM uogsoe/soe-php-apache:${PHP_VERSION} as dev 7 | 8 | COPY docker/app-start docker/app-healthcheck /usr/local/bin/ 9 | RUN chmod u+x /usr/local/bin/app-start /usr/local/bin/app-healthcheck 10 | CMD ["tini", "--", "/usr/local/bin/app-start"] 11 | 12 | 13 | 14 | ### Prod php dependencies 15 | FROM dev as prod-composer 16 | ARG FLUX_USERNAME 17 | ARG FLUX_LICENSE_KEY 18 | ENV APP_ENV=production 19 | ENV APP_DEBUG=0 20 | 21 | WORKDIR /var/www/html 22 | 23 | USER nobody 24 | 25 | #- make paths that the laravel composer.json expects to exist 26 | RUN mkdir -p database 27 | #- copy the seeds and factories so that composer generates autoload entries for them 28 | COPY database/seeders database/seeders 29 | COPY database/factories database/factories 30 | 31 | 32 | COPY --chown=nobody composer.* ./ 33 | RUN echo ${FLUX_USERNAME} 34 | RUN echo ${FLUX_LICENSE_KEY} 35 | 36 | RUN composer config http-basic.composer.fluxui.dev "${FLUX_USERNAME}" "${FLUX_LICENSE_KEY}" 37 | 38 | RUN composer install \ 39 | --no-interaction \ 40 | --no-plugins \ 41 | --no-scripts \ 42 | --no-dev \ 43 | --prefer-dist 44 | 45 | ### QA php dependencies 46 | FROM prod-composer as qa-composer 47 | ARG FLUX_USERNAME 48 | ARG FLUX_LICENSE_KEY 49 | ENV APP_ENV=local 50 | ENV APP_DEBUG=1 51 | 52 | RUN composer config http-basic.composer.fluxui.dev "${FLUX_USERNAME}" "${FLUX_LICENSE_KEY}" 53 | 54 | RUN composer install \ 55 | --no-interaction \ 56 | --no-plugins \ 57 | --no-scripts \ 58 | --prefer-dist 59 | 60 | ### Build JS/css assets 61 | FROM node:20.13.1 as frontend 62 | 63 | # workaround for mix.version() webpack bug 64 | RUN ln -s /home/node/public /public 65 | 66 | USER node 67 | WORKDIR /home/node 68 | 69 | RUN mkdir -p /home/node/public/css /home/node/public/js /home/node/resources 70 | 71 | COPY --chown=node:node package*.json *.js .babelrc* /home/node/ 72 | COPY --chown=node:node resources/js* /home/node/resources/js 73 | COPY --chown=node:node resources/sass* /home/node/resources/sass 74 | COPY --chown=node:node resources/scss* /home/node/resources/scss 75 | COPY --chown=node:node resources/css* /home/node/resources/css 76 | COPY --chown=node:node resources/views* /home/node/resources/views 77 | COPY --chown=node:node --from=qa-composer /var/www/html/vendor /home/node/vendor 78 | 79 | RUN npm install && \ 80 | npm run build && \ 81 | npm cache clean --force 82 | 83 | 84 | ### And build the prod app 85 | FROM dev as prod 86 | 87 | WORKDIR /var/www/html 88 | 89 | ENV APP_ENV=production 90 | ENV APP_DEBUG=0 91 | 92 | #- Copy our start scripts and php/ldap configs in 93 | COPY docker/ldap.conf /etc/ldap/ldap.conf 94 | COPY docker/custom_php.ini /usr/local/etc/php/conf.d/custom_php.ini 95 | 96 | #- Copy in our prod php dep's 97 | COPY --from=prod-composer /var/www/html/vendor /var/www/html/vendor 98 | 99 | #- Copy in our front-end assets 100 | RUN mkdir -p /var/www/html/public/build 101 | COPY --from=frontend /home/node/public/build /var/www/html/public/build 102 | 103 | #- Copy in our code 104 | COPY . /var/www/html 105 | 106 | #- Clear any cached composer stuff 107 | RUN rm -fr /var/www/html/bootstrap/cache/*.php 108 | 109 | #- If horizon is installed force it to rebuild it's public assets 110 | RUN if grep -q horizon composer.json; then php /var/www/html/artisan horizon:publish ; fi 111 | 112 | #- Symlink the docker secret to the local .env so Laravel can see it 113 | RUN ln -sf /run/secrets/.env /var/www/html/.env 114 | 115 | #- Clean up and production-cache our apps settings/views/routing 116 | RUN php /var/www/html/artisan storage:link && \ 117 | php /var/www/html/artisan view:cache && \ 118 | php /var/www/html/artisan route:cache && \ 119 | chown -R www-data:www-data storage bootstrap/cache 120 | 121 | #- Set up the default healthcheck 122 | HEALTHCHECK --start-period=30s CMD /usr/local/bin/app-healthcheck 123 | 124 | 125 | 126 | ### Build the ci version of the app (prod+dev packages) 127 | FROM prod as ci 128 | 129 | ENV APP_ENV=local 130 | ENV APP_DEBUG=0 131 | 132 | #- Copy in our QA php dep's 133 | COPY --from=qa-composer /var/www/html/vendor /var/www/html/vendor 134 | 135 | #- Clear the caches 136 | ENV CACHE_STORE=array 137 | RUN php /var/www/html/artisan optimize:clear 138 | -------------------------------------------------------------------------------- /qa-stack.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | x-logging: 4 | &default-logging 5 | options: 6 | max-size: '12m' 7 | max-file: '5' 8 | driver: json-file 9 | x-deploy: 10 | &default-deploy 11 | replicas: 1 12 | labels: 13 | - "traefik.enable=false" 14 | update_config: 15 | parallelism: 1 16 | failure_action: rollback 17 | order: start-first 18 | 19 | 20 | services: 21 | app: 22 | image: ${IMAGE_NAME} 23 | logging: *default-logging 24 | environment: 25 | CONTAINER_ROLE: app 26 | build: 27 | context: . 28 | secrets: 29 | - source: dotenv 30 | target: .env 31 | depends_on: 32 | - redis 33 | - mysql 34 | - mailhog 35 | networks: 36 | - proxy 37 | - private 38 | expose: 39 | - "80" 40 | deploy: 41 | update_config: 42 | parallelism: 1 43 | failure_action: rollback 44 | order: start-first 45 | replicas: 1 46 | labels: 47 | # note: this assumes traefik v2 48 | - "traefik.enable=true" 49 | - "traefik.http.routers.${TRAEFIK_BACKEND}.rule=Host(`${TRAEFIK_HOSTNAME}`)" 50 | - "traefik.http.routers.${TRAEFIK_BACKEND}.entrypoints=web" 51 | - "traefik.http.services.${TRAEFIK_BACKEND}.loadbalancer.server.port=80" # it seems you always need to give traefik a port so it 'notices' the service 52 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.rule=Host(`${TRAEFIK_HOSTNAME}`)" 53 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.entrypoints=web-secured" 54 | - "traefik.http.routers.${TRAEFIK_BACKEND}-secured.tls.certresolver=mytlschallenge" 55 | 56 | scheduler: 57 | image: ${IMAGE_NAME} 58 | logging: *default-logging 59 | deploy: *default-deploy 60 | environment: 61 | CONTAINER_ROLE: scheduler 62 | depends_on: 63 | - app 64 | networks: 65 | - private 66 | secrets: 67 | - source: dotenv 68 | target: .env 69 | 70 | queue: 71 | image: ${IMAGE_NAME} 72 | logging: *default-logging 73 | deploy: *default-deploy 74 | stop_grace_period: 30s 75 | environment: 76 | CONTAINER_ROLE: queue 77 | depends_on: 78 | - app 79 | networks: 80 | - private 81 | secrets: 82 | - source: dotenv 83 | target: .env 84 | 85 | migrations: 86 | image: ${IMAGE_NAME} 87 | logging: *default-logging 88 | deploy: *default-deploy 89 | networks: 90 | - private 91 | environment: 92 | CONTAINER_ROLE: migrations 93 | depends_on: 94 | - app 95 | secrets: 96 | - source: dotenv 97 | target: .env 98 | 99 | redis: 100 | image: redis:8.2.2 101 | deploy: *default-deploy 102 | networks: 103 | - private 104 | volumes: 105 | - redis:/data 106 | healthcheck: 107 | test: ["CMD", "redis-cli", "ping"] 108 | 109 | mysql: 110 | image: mysql:5.7 111 | deploy: *default-deploy 112 | networks: 113 | - private 114 | volumes: 115 | - mysql:/var/lib/mysql 116 | environment: 117 | MYSQL_DATABASE: homestead 118 | MYSQL_ROOT_PASSWORD: root 119 | MYSQL_USER: homestead 120 | MYSQL_PASSWORD: secret 121 | 122 | mailhog: 123 | image: mailhog/mailhog 124 | deploy: 125 | labels: 126 | # note: this assumes traefik v2 127 | - "traefik.enable=true" 128 | - "traefik.http.routers.mailhog-${TRAEFIK_BACKEND}.rule=Host(`mail-${TRAEFIK_HOSTNAME}`)" 129 | - "traefik.http.routers.mailhog-${TRAEFIK_BACKEND}.entrypoints=web" 130 | - "traefik.http.services.mailhog-${TRAEFIK_BACKEND}.loadbalancer.server.port=8025" # it seems you always need to give traefik a port so it 'notices' the service 131 | - "traefik.http.routers.mailhog-${TRAEFIK_BACKEND}-secured.rule=Host(`mail-${TRAEFIK_HOSTNAME}`)" 132 | - "traefik.http.routers.mailhog-${TRAEFIK_BACKEND}-secured.entrypoints=web-secured" 133 | - "traefik.http.routers.mailhog-${TRAEFIK_BACKEND}-secured.tls.certresolver=mytlschallenge" 134 | expose: 135 | - "8025" 136 | networks: 137 | - proxy 138 | - private 139 | 140 | volumes: 141 | redis: 142 | driver: "local" 143 | mysql: 144 | driver: "local" 145 | 146 | networks: 147 | private: 148 | proxy: 149 | external: true 150 | 151 | secrets: 152 | dotenv: 153 | external: true 154 | name: ${DOTENV_NAME} 155 | -------------------------------------------------------------------------------- /docker/docker-stack-wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # By: Brandon Mitchell 4 | # License: MIT 5 | # Source repo: https://github.com/sudo-bmitch/docker-stack-wait 6 | 7 | set -e 8 | trap "{ exit 1; }" TERM INT 9 | opt_h=0 10 | opt_r=0 11 | opt_s=5 12 | opt_t=3600 13 | start_epoc=$(date +%s) 14 | 15 | usage() { 16 | echo "$(basename $0) [opts] stack_name" 17 | echo " -f filter: only wait for services matching filter, may be passed multiple" 18 | echo " times, see docker stack services for the filter syntax" 19 | echo " -h: this help message" 20 | echo " -n name: only wait for specific service names, overrides any filters," 21 | echo " may be passed multiple times, do not include the stack name prefix" 22 | echo " -r: treat a rollback as successful" 23 | echo " -s sec: frequency to poll service state (default $opt_s sec)" 24 | echo " -t sec: timeout to stop waiting" 25 | [ "$opt_h" = "1" ] && exit 0 || exit 1 26 | } 27 | check_timeout() { 28 | # timeout when a timeout is defined and we will exceed the timeout after the 29 | # next sleep completes 30 | if [ "$opt_t" -gt 0 ]; then 31 | cur_epoc=$(date +%s) 32 | cutoff_epoc=$(expr ${start_epoc} + $opt_t - $opt_s) 33 | if [ "$cur_epoc" -gt "$cutoff_epoc" ]; then 34 | echo "Error: Timeout exceeded" 35 | exit 1 36 | fi 37 | fi 38 | } 39 | get_service_ids() { 40 | if [ -n "$opt_n" ]; then 41 | service_list="" 42 | for name in $opt_n; do 43 | service_list="${service_list:+${service_list} }${stack_name}_${name}" 44 | done 45 | docker service inspect --format '{{.ID}}' ${service_list} 46 | else 47 | docker stack services ${opt_f} -q "${stack_name}" 48 | fi 49 | } 50 | service_state() { 51 | # output the state when it changes from the last state for the service 52 | service=$1 53 | # strip any invalid chars from service name for caching state 54 | service_safe=$(echo "$service" | sed 's/[^A-Za-z0-9_]/_/g') 55 | state=$2 56 | if eval [ \"\$cache_${service_safe}\" != \"\$state\" ]; then 57 | echo "Service $service state: $state" 58 | eval cache_${service_safe}=\"\$state\" 59 | fi 60 | } 61 | 62 | while getopts 'f:hn:rs:t:' opt; do 63 | case $opt in 64 | f) opt_f="${opt_f:+${opt_f} }-f $OPTARG";; 65 | h) opt_h=1;; 66 | n) opt_n="${opt_n:+${opt_n} } $OPTARG";; 67 | r) opt_r=1;; 68 | s) opt_s="$OPTARG";; 69 | t) opt_t="$OPTARG";; 70 | esac 71 | done 72 | shift $(expr $OPTIND - 1) 73 | 74 | if [ $# -ne 1 -o "$opt_h" = "1" -o "$opt_s" -le "0" ]; then 75 | usage 76 | fi 77 | 78 | stack_name=$1 79 | 80 | # 0 = running, 1 = success, 2 = error 81 | stack_done=0 82 | while [ "$stack_done" != "1" ]; do 83 | stack_done=1 84 | # run get_service_ids outside of the for loop to catch errors 85 | service_ids=$(get_service_ids) 86 | for service_id in ${service_ids}; do 87 | service_done=1 88 | service=$(docker service inspect --format '{{.Spec.Name}}' "$service_id") 89 | 90 | # hardcode a "new" state when UpdateStatus is not defined 91 | state=$(docker service inspect -f '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{else}}new{{end}}' "$service_id") 92 | 93 | # check for failed update states 94 | case "$state" in 95 | paused|rollback_paused) 96 | service_done=2 97 | ;; 98 | rollback_*) 99 | if [ "$opt_r" = "0" ]; then 100 | service_done=2 101 | fi 102 | ;; 103 | esac 104 | 105 | # identify/report current state 106 | if [ "$service_done" != "2" ]; then 107 | replicas=$(docker service ls --format '{{.Replicas}}' --filter "id=$service_id" | cut -d' ' -f1) 108 | current=$(echo "$replicas" | cut -d/ -f1) 109 | target=$(echo "$replicas" | cut -d/ -f2) 110 | if [ "$current" != "$target" ]; then 111 | # actively replicating service 112 | service_done=0 113 | state="replicating $replicas" 114 | fi 115 | fi 116 | service_state "$service" "$state" 117 | 118 | # check for states that indicate an update is done 119 | if [ "$service_done" = "1" ]; then 120 | case "$state" in 121 | new|completed|rollback_completed) 122 | service_done=1 123 | ;; 124 | *) 125 | # any other state is unknown, not necessarily finished 126 | service_done=0 127 | ;; 128 | esac 129 | fi 130 | 131 | # update stack done state 132 | if [ "$service_done" = "2" ]; then 133 | # error condition 134 | stack_done=2 135 | elif [ "$service_done" = "0" -a "$stack_done" = "1" ]; then 136 | # only go to an updating state if not in an error state 137 | stack_done=0 138 | fi 139 | done 140 | if [ "$stack_done" = "2" ]; then 141 | echo "Error: This deployment will not complete" 142 | exit 1 143 | fi 144 | if [ "$stack_done" != "1" ]; then 145 | check_timeout 146 | sleep "${opt_s}" 147 | fi 148 | done 149 | 150 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build-images 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "**" 8 | branches: 9 | - "**" 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | build-and-test: 16 | runs-on: ubuntu-latest 17 | services: 18 | mysql: 19 | image: mysql:5.7 20 | env: 21 | MYSQL_ROOT_PASSWORD: homestead 22 | MYSQL_DATABASE: homestead 23 | ports: 24 | - 33306:3306 25 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 26 | registry: 27 | image: registry:2 28 | ports: 29 | - 5000:5000 30 | 31 | steps: 32 | - 33 | name: Checkout 34 | uses: actions/checkout@v3 35 | - 36 | name: Set up QEMU 37 | uses: docker/setup-qemu-action@v1 38 | - 39 | name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v1 41 | with: 42 | driver-opts: network=host 43 | - 44 | name: Cache Docker layers 45 | uses: actions/cache@v2 46 | with: 47 | path: /tmp/.buildx-cache 48 | key: ${{ runner.os }}-buildx-${{ github.sha }} 49 | restore-keys: | 50 | ${{ runner.os }}-buildx- 51 | - 52 | name: Build and push local test image 53 | uses: docker/build-push-action@v2 54 | with: 55 | context: . 56 | file: ./Dockerfile 57 | platforms: linux/amd64 #,linux/arm/v7,linux/arm64 58 | push: true 59 | cache-from: type=local,src=/tmp/.buildx-cache 60 | cache-to: type=local,dest=/tmp/.buildx-cache 61 | target: ci 62 | build-args: | 63 | PHP_VERSION=8.0 64 | tags: | 65 | localhost:5000/${{ github.event.repository.name }}:qa-${{ github.sha }} 66 | - 67 | name: Run tests 68 | uses: addnab/docker-run-action@v3 69 | with: 70 | # username: ${{ secrets.DOCKERHUB_USERNAME }} 71 | # password: ${{ secrets.DOCKERHUB_TOKEN }} 72 | options: "--network host" 73 | image: localhost:5000/${{ github.event.repository.name }}:qa-${{ github.sha }} 74 | run: rm -fv .env && cp -v .env.github .env && php artisan config:clear && CI=1 APP_ENV=testing ./vendor/bin/phpunit -c phpunit.github.xml 75 | - 76 | name: Get tag 77 | id: get_version 78 | run: echo ::set-output name=VERSION::${GITHUB_REF##*/} 79 | - 80 | name: Login to DockerHub 81 | if: startsWith(steps.get_version.outputs.VERSION, 'v') || startsWith(steps.get_version.outputs.VERSION, 'qa') || github.ref == 'refs/heads/master' 82 | uses: docker/login-action@v1 83 | with: 84 | username: ${{ secrets.DOCKERHUB_USERNAME }} 85 | password: ${{ secrets.DOCKERHUB_TOKEN }} 86 | - 87 | name: Retag and publish local QA image 88 | if: startsWith(steps.get_version.outputs.VERSION, 'qa') 89 | uses: akhilerm/tag-push-action@v1.1.0 90 | with: 91 | src: localhost:5000/${{ github.event.repository.name }}:qa-${{ github.sha }} 92 | dst: | 93 | docker.io/uogsoe/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }} 94 | - 95 | name: Build and push prod 96 | if: github.ref == 'refs/heads/master' 97 | uses: docker/build-push-action@v2 98 | with: 99 | context: . 100 | file: ./Dockerfile 101 | platforms: linux/amd64 #,linux/arm/v7,linux/arm64 102 | push: true 103 | target: prod 104 | cache-from: type=local,src=/tmp/.buildx-cache 105 | cache-to: type=local,dest=/tmp/.buildx-cache 106 | build-args: | 107 | PHP_VERSION=8.0 108 | tags: | 109 | uogsoe/${{ github.event.repository.name }}:prod-${{ github.sha }} 110 | - 111 | name: Build and push versioned tag 112 | if: startsWith(steps.get_version.outputs.VERSION, 'v') 113 | uses: docker/build-push-action@v2 114 | with: 115 | context: . 116 | file: ./Dockerfile 117 | platforms: linux/amd64 #,linux/arm/v7,linux/arm64 118 | push: true 119 | target: prod 120 | cache-from: type=local,src=/tmp/.buildx-cache 121 | cache-to: type=local,dest=/tmp/.buildx-cache 122 | build-args: | 123 | PHP_VERSION=8.0 124 | tags: | 125 | docker.io/uogsoe/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }} 126 | - 127 | name: Create automatic release on new versioned tag 128 | if: startsWith(steps.get_version.outputs.VERSION, 'v') 129 | uses: "marvinpinto/action-automatic-releases@latest" 130 | with: 131 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 132 | draft: false 133 | prerelease: false 134 | automatic_release_tag: ${{ steps.get_version.outputs.VERSION }} 135 | title: ${{ steps.get_version.outputs.VERSION }} 136 | -------------------------------------------------------------------------------- /docker/env_key_check.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | true, 70 | CURLOPT_FOLLOWLOCATION => true, 71 | CURLOPT_CONNECTTIMEOUT => 10, 72 | CURLOPT_TIMEOUT => 20, 73 | CURLOPT_USERAGENT => 'env-checker/1.0', 74 | ]); 75 | $body = curl_exec($ch); 76 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 77 | curl_close($ch); 78 | if ($body !== false && $code >= 200 && $code < 300) return $body; 79 | return null; 80 | } else { 81 | $ctx = stream_context_create([ 82 | 'http' => [ 83 | 'method' => 'GET', 84 | 'header' => "User-Agent: env-checker/1.0\r\n", 85 | 'timeout' => 20 86 | ], 87 | 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true] 88 | ]); 89 | $body = @file_get_contents($url, false, $ctx); 90 | if ($body !== false) return $body; 91 | return null; 92 | } 93 | } 94 | 95 | function fetchLaravelEnvExample(?int $major): string { 96 | $candidates = []; 97 | if ($major !== null) { 98 | $candidates[] = "https://raw.githubusercontent.com/laravel/laravel/{$major}.x/.env.example"; 99 | } 100 | // Fallbacks in case major is unknown or branch naming changes 101 | $candidates[] = "https://raw.githubusercontent.com/laravel/laravel/main/.env.example"; 102 | $candidates[] = "https://raw.githubusercontent.com/laravel/laravel/master/.env.example"; 103 | 104 | foreach ($candidates as $url) { 105 | $body = tryFetch($url); 106 | if ($body !== null) return $body; 107 | } 108 | fwrite(STDERR, "ERROR: Failed to fetch Laravel .env.example from GitHub (tried: ".implode(', ', $candidates).")\n"); 109 | exit(2); 110 | } 111 | 112 | function extractKeys(string $envText): array { 113 | $keys = []; 114 | $lines = preg_split('/\R/', $envText); 115 | $re = '/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/'; 116 | foreach ($lines as $line) { 117 | $t = trim($line); 118 | if ($t === '' || str_starts_with($t, '#')) continue; 119 | if (preg_match($re, $t, $m)) { 120 | $keys[$m[1]] = true; 121 | } 122 | } 123 | return array_keys($keys); 124 | } 125 | 126 | function getLocalEnvText(?string $fileOpt, string $envVar): string { 127 | if ($fileOpt) { 128 | if (!is_file($fileOpt)) { 129 | fwrite(STDERR, "ERROR: --file provided but not found: {$fileOpt}\n"); 130 | exit(3); 131 | } 132 | $txt = file_get_contents($fileOpt); 133 | if ($txt === false) { 134 | fwrite(STDERR, "ERROR: Could not read file: {$fileOpt}\n"); 135 | exit(3); 136 | } 137 | return $txt; 138 | } 139 | 140 | $fromVar = getenv($envVar); 141 | if ($fromVar !== false && $fromVar !== '') { 142 | return $fromVar; 143 | } 144 | 145 | if (is_file('.env')) { 146 | $txt = file_get_contents('.env'); 147 | if ($txt !== false) return $txt; 148 | } 149 | 150 | fwrite(STDERR, "ERROR: No env provided. Set \${$envVar}, or use --file=PATH, or ensure .env exists.\n"); 151 | exit(3); 152 | } 153 | 154 | /* ---------- Main ---------- */ 155 | 156 | $major = detectLaravelMajor(); 157 | $official = fetchLaravelEnvExample($major); 158 | $local = getLocalEnvText($fileOpt, $envVar); 159 | 160 | $upstreamKeys = extractKeys($official); 161 | $localKeys = extractKeys($local); 162 | 163 | $upstreamSet = array_fill_keys($upstreamKeys, true); 164 | $localSet = array_fill_keys($localKeys, true); 165 | 166 | // Missing = in upstream but not local 167 | $missing = array_values(array_diff(array_keys($upstreamSet), array_keys($localSet))); 168 | sort($missing); 169 | 170 | echo "Detected Laravel major: ".($major !== null ? $major : 'unknown (using fallback branch)').PHP_EOL; 171 | echo "Upstream keys: ".count($upstreamKeys)." | Local keys: ".count($localKeys).PHP_EOL.PHP_EOL; 172 | 173 | if (!empty($missing)) { 174 | echo "Missing keys (present in official .env.example, not in your local env):".PHP_EOL; 175 | foreach ($missing as $k) echo " - {$k}".PHP_EOL; 176 | } 177 | 178 | if ($showExtra) { 179 | $extra = array_values(array_diff(array_keys($localSet), array_keys($upstreamSet))); 180 | sort($extra); 181 | echo PHP_EOL."Extra local keys (not in official .env.example):".PHP_EOL; 182 | if ($extra) { 183 | foreach ($extra as $k) echo " - {$k}".PHP_EOL; 184 | } else { 185 | echo " (none)".PHP_EOL; 186 | } 187 | } 188 | 189 | if (!empty($missing)) { 190 | exit(1); // fail job 191 | } 192 | 193 | echo "✅ No missing keys. Your env has all keys from the official .env.example.".PHP_EOL; 194 | 195 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Variables which should be set in the gitlab project or group : 3 | # 4 | # PHP_VERSION (eg '8.1') 5 | # STACK_NAME (eg, 'amazingapp' - as in 'docker stack deploy $STACK_NAME') 6 | # TRAEFIK_BACKEND (eg 'amazingapp-web' - label for traefik dashboard) 7 | # TRAEFIK_QA_HOSTNAME (eg, amazing.qa-domain.ac.uk - hostname your app will be available on) 8 | # TRAEFIK_PROD_HOSTNAME (eg, amazing.prod-domain.ac.uk - hostname your app will be available on) 9 | # QA_DOTENV (*QA* .env file contents) 10 | # QA_SERVER (eg, qaserver.domain.ac.uk) 11 | # QA_SSH_KEY (private key for the deployment ssh user on QA) 12 | # QA_SSH_USER (username for deployment ssh user on QA) 13 | # PROD_DOTENV (*production* .env file contents) 14 | # PROD_SERVER (see above) 15 | # PROD_SSH_KEY (see above) 16 | # PROD_SSH_USER (see above) 17 | # 18 | # There should also be two environments in your gitlab project - 'prod' and 'qa' 19 | # 20 | 21 | stages: 22 | - build-qa 23 | - test 24 | - build-prod 25 | - deploy 26 | 27 | variables: 28 | QA_IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH:qa-$CI_COMMIT_SHA 29 | PROD_IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH:prod-$CI_COMMIT_SHA 30 | LOCAL_QA_IMAGE_NAME: localhost:5000/$CI_PROJECT_PATH:qa-$CI_COMMIT_SHA 31 | LOCAL_PROD_IMAGE_NAME: localhost:5000/$CI_PROJECT_PATH:prod-$CI_COMMIT_SHA 32 | 33 | cache: 34 | paths: 35 | # - vendor/ 36 | # - node_modules/ 37 | 38 | unit-tests: 39 | stage: test 40 | image: $QA_IMAGE_NAME 41 | services: 42 | - mysql:5.7 43 | variables: 44 | MYSQL_DATABASE: homestead 45 | MYSQL_ROOT_PASSWORD: secret 46 | MYSQL_USER: homestead 47 | MYSQL_PASSWORD: secret 48 | script: 49 | - cd /var/www/html 50 | - mkdir -p /run/secrets 51 | - cp -f .env.gitlab /run/secrets/.env 52 | - export APP_ENV=testing 53 | - php artisan key:generate 54 | - php artisan config:clear 55 | - php artisan migrate:fresh 56 | - cat .env 57 | - echo "Add code coverage and upload artifact to gitlab" 58 | - php ./vendor/bin/pest -c phpunit.gitlab.xml 59 | 60 | php-codestyle-check: 61 | stage: test 62 | image: $QA_IMAGE_NAME 63 | script: 64 | - php ./vendor/bin/pint --test 65 | 66 | stray-die-and-dump-check: 67 | stage: test 68 | image: $QA_IMAGE_NAME 69 | script: 70 | - egrep -r '[^a-zA-Z](dd\(|dump\()' app || exit 0 71 | 72 | php-security-scan: 73 | stage: test 74 | image: $QA_IMAGE_NAME 75 | script: 76 | - composer audit 77 | 78 | laravel-env-key-check: 79 | stage: test 80 | image: $QA_IMAGE_NAME 81 | script: 82 | - echo "Checking prod dotenv for keys missing compared to laravel repo" 83 | - php docker/env_key_check.php --env-var=PROD_DOTENV 84 | - echo "Checking qa dotenv for keys missing compared to laravel repo" 85 | - php docker/env_key_check.php --env-var=QA_DOTENV 86 | 87 | dotenv-example-missing-keys-check: 88 | stage: test 89 | image: $QA_IMAGE_NAME 90 | artifacts: 91 | paths: 92 | - envdiff.txt 93 | expire_in: 1 week 94 | script: 95 | - set +eo pipefail 96 | - echo 'diff --new-line-format='\'''\'' --unchanged-line-format='\'''\'' <(sort $1 | egrep -v '\''^#'\'' | sed -e '\''s/=.*//'\'') <(sort $2 | egrep -v '\''^#'\'' | sed -e '\''s/=.*//'\'')' > envdiff.sh 97 | - chmod +x envdiff.sh 98 | - echo "Checking prod dotenv for keys missing in .env.example" | tee -a envdiff.txt 99 | - echo "$PROD_DOTENV" > .env 100 | - ./envdiff.sh .env .env.example | tee -a envdiff.txt 101 | - echo "Checking .env.example for keys missing in prod dotenv" | tee -a envdiff.txt 102 | - echo "$PROD_DOTENV" > .env 103 | - ./envdiff.sh .env.example .env | tee -a envdiff.txt 104 | - echo "Checking qa dotenv for keys missing in .env.example" | tee -a envdiff.txt 105 | - echo "$QA_DOTENV" > .env 106 | - ./envdiff.sh .env .env.example | tee -a envdiff.txt 107 | - echo "Checking .env.example for keys missing in qa dotenv" | tee -a envdiff.txt 108 | - echo "$QA_DOTENV" > .env 109 | - ./envdiff.sh .env.example .env | tee -a envdiff.txt 110 | 111 | build-qa: 112 | stage: build-qa 113 | extends: 114 | - .build 115 | environment: qa 116 | variables: 117 | DOCKER_TARGET: ci 118 | IMAGE_NAME: $QA_IMAGE_NAME 119 | FLUX_USERNAME: ${FLUX_USERNAME} 120 | FLUX_LICENSE_KEY: ${FLUX_LICENSE_KEY} 121 | 122 | build-prod: 123 | stage: build-prod 124 | extends: 125 | - .build 126 | environment: prod 127 | only: 128 | - master 129 | variables: 130 | DOCKER_TARGET: prod 131 | IMAGE_NAME: $PROD_IMAGE_NAME 132 | 133 | deploy_to_qa: 134 | stage: deploy 135 | extends: 136 | - .deployment 137 | when: manual 138 | environment: 139 | name: qa 140 | url: http://${TRAEFIK_QA_HOSTNAME} 141 | needs: 142 | - build-qa 143 | variables: 144 | IMAGE_NAME: ${LOCAL_QA_IMAGE_NAME} 145 | TRAEFIK_BACKEND: ${TRAEFIK_QA_BACKEND} 146 | TRAEFIK_HOSTNAME: ${TRAEFIK_QA_HOSTNAME} 147 | SSH_KEY: ${QA_SSH_KEY} 148 | SSH_USER: ${QA_SSH_USER} 149 | SERVER: ${QA_SERVER} 150 | DOTENV: ${QA_DOTENV} 151 | 152 | deploy_to_prod: 153 | stage: deploy 154 | extends: 155 | - .deployment 156 | when: manual 157 | only: 158 | - master 159 | needs: 160 | - build-prod 161 | environment: 162 | name: prod 163 | url: https://${TRAEFIK_PROD_HOSTNAME} 164 | variables: 165 | IMAGE_NAME: ${LOCAL_PROD_IMAGE_NAME} 166 | TRAEFIK_BACKEND: ${TRAEFIK_PROD_BACKEND} 167 | TRAEFIK_HOSTNAME: ${TRAEFIK_PROD_HOSTNAME} 168 | SSH_KEY: ${PROD_SSH_KEY} 169 | SSH_USER: ${PROD_SSH_USER} 170 | SERVER: ${PROD_SERVER} 171 | DOTENV: ${PROD_DOTENV} 172 | 173 | .build: 174 | image: docker:stable 175 | variables: 176 | DOCKER_TARGET: "" 177 | IMAGE_NAME: "" 178 | script: 179 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 180 | - DOCKER_BUILDKIT=1 docker build --build-arg PHP_VERSION=${PHP_VERSION} --build-arg FLUX_USERNAME=${FLUX_USERNAME} --build-arg FLUX_LICENSE_KEY=${FLUX_LICENSE_KEY} --target="${DOCKER_TARGET}" -t $IMAGE_NAME . 181 | - docker push $IMAGE_NAME 182 | 183 | .deployment: 184 | when: manual 185 | image: docker:stable 186 | variables: 187 | IMAGE_NAME: "" 188 | TRAEFIK_BACKEND: "" 189 | TRAEFIK_HOSTNAME: "" 190 | SSH_KEY: "" 191 | SSH_USER: "" 192 | SERVER: "" 193 | DOTENV: "" 194 | script: 195 | - apk add -qU openssh curl 196 | - eval $(ssh-agent -s) 197 | - echo "$SSH_KEY" | tr -d '\r' | ssh-add - > /dev/null 198 | - mkdir ~/.ssh 199 | - chmod 700 ~/.ssh 200 | - ssh-keyscan ${SERVER} > ~/.ssh/known_hosts 201 | - chmod 644 ~/.ssh/known_hosts 202 | - export NOW=`date +%Y-%m-%d-%H-%M-%S` 203 | - export DOTENV_NAME="${CI_PROJECT_PATH_SLUG}-${CI_ENVIRONMENT_NAME}-dotenv-${NOW}" 204 | - export DOCKER_HOST=ssh://${SSH_USER}@${SERVER} 205 | - echo "${DOTENV}" | docker secret create ${DOTENV_NAME} - 206 | - echo "Deploying stack ${STACK_NAME} image ${IMAGE_NAME} with secret ${DOTENV_NAME}" 207 | - docker stack deploy -c ${CI_ENVIRONMENT_NAME}-stack.yml --prune ${STACK_NAME} 208 | - ./docker/docker-stack-wait.sh ${STACK_NAME} 209 | - > 210 | if [ ! -z "${DISCORD_WEBHOOK}" ]; then 211 | CLEAN_COMMIT_MESSAGE=$(echo "${CI_COMMIT_MESSAGE}" | tr '\n' ' ' | sed 's/"/\\"/g') 212 | curl -X POST -H "Content-Type: application/json" -d '{"embeds": [{"title": "'"${STACK_NAME}"' deployed", "description": "'"${CLEAN_COMMIT_MESSAGE}"'", "color": 3447003, "fields": [{"name": "Environment", "value": "'"${CI_ENVIRONMENT_NAME}"'", "inline": true}, {"name": "Deployed By", "value": "'"${GITLAB_USER_NAME:-unknown}"'", "inline": true}], "timestamp": "'"$(date -u +'%Y-%m-%dT%H:%M:%SZ')"'"}]}' "${DISCORD_WEBHOOK}"; 213 | fi 214 | 215 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Docker stuff 2 | 3 | This is the base repo for our docker/swarm app configs. Generic as far as possible for our Laravel/PHP apps and used as the base on new projects. Ideally they can be used 'as is' outside of special-case apps. 4 | 5 | 6 | ## If you're interested 7 | 8 | Each app gets a copy of the docker files (you can run `./copyto ../code/my-project` to copy them in). The stack file 9 | is pretty generic and used as the base for all our apps. To use it you need to set a few environment variables and create a secret in swarm. For instance, for an app called 'bingo' you might do : 10 | 11 | ``` 12 | # set some env variables 13 | export PHP_VERSION=7.3 14 | export IMAGE_NAME=127.0.0.1:5000/bingo 15 | export TRAEFIK_BACKEND=bingo-web 16 | export TRAEFIK_HOSTNAME=bingo.yourdomain.com 17 | export DOTENV_NAME=bingo-dotenv-20190428 18 | 19 | # enable docker buildkit 20 | export DOCKER_BUILDKIT=1 21 | 22 | # build the image and push to a local registry (with the 'production' target - there is a 'ci' for testing/qa too) 23 | docker build --build-arg=PHP_VERSION=${PHP_VERSION} --target=prod -t ${IMAGE_NAME} . 24 | docker push ${IMAGE_NAME} 25 | 26 | # create a docker secret from a file called docker.env - this should be your normal production laravel app '.env' stuff 27 | docker secret create ${DOTENV_NAME} docker.env 28 | 29 | # and deploy 30 | docker stack deploy -c prod-stack.yml bingo 31 | ``` 32 | 33 | There is a 'qa-stack.yml' which is more like the provided `docker-compose` setup in that it will spin-up a stand-alone mysql/redis/maihog for the app. 34 | 35 | There is a `docker-compose.yml` file that you can use for dev/demo-ing. It will use a local `.env` file as the laravel .env inside the containers. You also need to set a couple of environment variables as above before starting it, specifically the `IMAGE_NAME`, and `APP_PORT` (the port the app will be available on - defaults to 3000). 36 | 37 | The 'compose' version will run the app, and also a local copy of mysql and redis. It will also spin up a copy of [Mailhog](https://github.com/mailhog/MailHog) to trap outgoing mail and make it available at http://localhost:3025. 38 | 39 | ``` 40 | # example for docker-compose 41 | export PHP_VERSION=7.3 # only needed if you are building the image as part of this 42 | export IMAGE_NAME=bingo:1.2.7 43 | export APP_PORT=3002 44 | 45 | docker-compose up --build 46 | ``` 47 | 48 | ### Assumptions 49 | 50 | You are using [Traefik](https://traefik.io/) as your proxy and there is a swarm overlay network for it called 'proxy'. 51 | 52 | You have a mysql database server (or mysql-router) available in an overlay network called 'mysql' and it's docker container name is 'mysql'. 53 | 54 | It defaults to doing a 'healthcheck' by making an http get request to '/' every 30 seconds. If you want to use something else then change the curl command in `docker/app-healthcheck` and/or altering the HEALTHCHECK line in the Dockerfile. 55 | 56 | You have an environment variable called PHP_VERSION that targets the major.minor version you are wanting to use, eg `export PHP_VERSION=7.3`. The default PHP_VERSION is at the top of the dockerfile if you don't want to use an env variable. 57 | 58 | ### Base images 59 | 60 | To build the base php images themselves we use the `build.sh` script and files inside the `base-images/` directory. 61 | 62 | ### Example dotenv that matches the stack 63 | 64 | ``` 65 | APP_NAME="Bingo" 66 | APP_ENV=production 67 | APP_KEY=base64:jxTSe1f8UnLnQWJyG0xMOQKnExy+MuXJLo6Yju/8iRM= 68 | APP_DEBUG=false 69 | APP_LOG_LEVEL=debug 70 | APP_URL=http://bingo.yourdomain.com/ 71 | 72 | DB_CONNECTION=mysql 73 | DB_HOST=mysql 74 | DB_PORT=3306 75 | DB_DATABASE=your_db_name 76 | DB_USERNAME=your_db_user 77 | DB_PASSWORD=your_db_password 78 | 79 | BROADCAST_DRIVER=redis 80 | CACHE_DRIVER=redis 81 | SESSION_DRIVER=redis 82 | SESSION_LIFETIME=120 83 | QUEUE_CONNECTION=redis 84 | QUEUE_NAME=bingo-queue 85 | 86 | LOG_CHANNEL=errorlog 87 | 88 | REDIS_HOST=redis 89 | REDIS_PASSWORD=null 90 | REDIS_PORT=6379 91 | 92 | MAIL_DRIVER=smtp 93 | MAIL_HOST=smtp.yourdomain.com 94 | MAIL_PORT=25 95 | MAIL_USERNAME=null 96 | MAIL_PASSWORD=null 97 | MAIL_ENCRYPTION=null 98 | MAIL_FROM_ADDRESS=bingo-app@yourdomain.com 99 | MAIL_FROM_NAME="Bingo App" 100 | 101 | LDAP_SERVER=ldap.yourdomain.com 102 | LDAP_OU=Users 103 | LDAP_USERNAME='whatever' 104 | LDAP_PASSWORD=secret 105 | 106 | ``` 107 | 108 | ## Gitlab-ci 109 | 110 | There's `.env.gitlab` and `.gitlab-ci.yml` files with the settings we use to run gitlab's CI process. Feel free to steal them. Our gitlab assumed you will have an environment variable set up in gitlab's CI settings for the php version you are targetting, eg `PHP_VERSION` `7.3`. There are a few other variables you should set too - they are detailed at the top of the `.gitlab-ci.yml` file. 111 | 112 | The gitlab CI setup will build two images : 113 | 114 | * `your/repo:qa-${git_sha}` - all the code & prod+dev php packages 115 | * `your/repo:prod-${git_sha}` - all the code & only production php packages (only built when pushing to the master branch) 116 | 117 | ## GitHub Actions 118 | There's also a `.github` directory and a matching `.env.github` and `phpunit.gihub.xml` file for running tests and builds of docker images. By default, the action run will do : 119 | 120 | * any push to the repo will build a local image and run phpunit. 121 | * If you push/merge to `master` it will also build & push an production image with `your/repo:prod-${git_sha}` 122 | * If you push a git tag starting with `qa` (eg, `git tag -a qa-test-new-feature`) it will build and push a development/debug image named `your/repo:qa-test-new-feature` 123 | * If you push a git tag starting with a `v` and a semver-looking value after it (eg, `git tag -a v1.2.3`) it will build and push a production image named `your/repo:v1.2.3` and also publish a github 'release' of `v1.2.3`. 124 | 125 | Note that for the image pushes to work you need to define two secrets in your repo or organisation - `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. 126 | ## Our current setup 127 | 128 | We have a small(ish) docker swarm. Each node runs a local container registry on 127.0.0.1:5000. We have an on-premise Gitlab install which acts as our source controller, CI runner and container registry. 129 | 130 | All of the container registries are backed by 'S3'-alike storage provided using a local [Minio](https://www.minio.io/) server. That means when we push an image to Gitlab, it ends 131 | up being available on all of the swarm nodes too as they're all pointing at the same bucket. That just means we avoid some tls/auth stuff - it's all on premise behind the corporate firewall - don't hate on me ;-) 132 | 133 | The config to do that with the registry is just : 134 | 135 | ``` 136 | version: 0.1 137 | log: 138 | level: debug 139 | formatter: text 140 | fields: 141 | service: registry 142 | environment: staging 143 | loglevel: debug 144 | http: 145 | secret: some-long-string 146 | storage: 147 | s3: 148 | accesskey: some-other-string 149 | secretkey: an-even-longer-string 150 | region: us-east-1 151 | regionendpoint: http://our.minio.server:9000 152 | # Make sure you've created the following bucket. 153 | bucket: "docker-registry" 154 | encrypt: false 155 | secure: true 156 | v4auth: true 157 | delete: 158 | enabled: true 159 | maintenance: 160 | uploadpurging: 161 | enabled: true 162 | age: 168h 163 | interval: 24h 164 | dryrun: false 165 | readonly: 166 | enabled: false 167 | http: 168 | addr: :5000 169 | ``` 170 | 171 | And the config for gitlab is just : 172 | 173 | ``` 174 | ... your other config 175 | registry['storage'] = { 176 | 's3' => { 177 | 'accesskey' => 'some-other-string', 178 | 'secretkey' => 'an-even-longer-string', 179 | 'bucket' => 'docker-registry', 180 | 'regionendpoint' => 'http://our.minio.server:9000', 181 | 'region' => 'us-east-1', 182 | 'path_style' => true 183 | } 184 | } 185 | ``` 186 | --------------------------------------------------------------------------------