├── 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 |
--------------------------------------------------------------------------------