├── .gitignore ├── .env ├── docker-compose.override.yaml ├── LICENSE ├── composer.json ├── README.md ├── docker-compose.yaml ├── Makefile └── bin └── env /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /build/.secrets/ 3 | /build/ssl/ 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DOMAIN=localhost 2 | WEB_PORT=8080 3 | SSL_PORT=8443 4 | RUNTIME=fpm 5 | DB_NAME=main 6 | -------------------------------------------------------------------------------- /docker-compose.override.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | ## This file is meant to be used for developing on a local machine. 4 | ## Use only the main Compose file when deploying to production using: 5 | ## docker-compose -f docker-compose.yaml 6 | services: 7 | 8 | server: 9 | volumes: 10 | # Use self-generated certificates during development. 11 | - type: 'bind' 12 | source: './build/ssl' 13 | target: '/etc/letsencrypt' 14 | read_only: true 15 | 16 | database: 17 | volumes: 18 | # Use a volume for data during development instead of messing with 19 | # system directories. 20 | - type: 'volume' 21 | source: 'database' 22 | target: '/var/lib/mysql' 23 | read_only: false 24 | 25 | volumes: 26 | database: 27 | driver: 'local' 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/skeleton", 3 | "type": "project", 4 | "license": "MIT", 5 | "description": "A minimal Symfony project recommended to create bare bones applications", 6 | "minimum-stability": "dev", 7 | "prefer-stable": true, 8 | "require": { 9 | "php": ">=7.2.5", 10 | "ext-ctype": "*", 11 | "ext-iconv": "*", 12 | "symfony/flex": "^1.3.1", 13 | "runtime/roadrunner-symfony-nyholm": "^0.1", 14 | "runtime/swoole": "^0.1" 15 | }, 16 | "flex-require": { 17 | "symfony/console": "*", 18 | "symfony/dotenv": "*", 19 | "symfony/framework-bundle": "*", 20 | "symfony/runtime": "*", 21 | "symfony/yaml": "*" 22 | }, 23 | "require-dev": { 24 | }, 25 | "config": { 26 | "optimize-autoloader": true, 27 | "preferred-install": { 28 | "*": "dist" 29 | }, 30 | "sort-packages": true 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "App\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "App\\Tests\\": "tests/" 40 | } 41 | }, 42 | "replace": { 43 | "symfony/polyfill-ctype": "*", 44 | "symfony/polyfill-iconv": "*", 45 | "symfony/polyfill-php72": "*" 46 | }, 47 | "scripts": { 48 | "auto-scripts": [ 49 | ], 50 | "post-install-cmd": [ 51 | "@auto-scripts" 52 | ], 53 | "post-update-cmd": [ 54 | "@auto-scripts" 55 | ] 56 | }, 57 | "conflict": { 58 | "symfony/symfony": "*" 59 | }, 60 | "extra": { 61 | "symfony": { 62 | "allow-contrib": false, 63 | "require": "5.3.*" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This Project 2 | 3 | This is a working example of Symfony's Runtime component. 4 | Do not use this project, look through the code and pull out the parts you find interesting. Copy+paste is your friend. 5 | 6 | ## Contains: 7 | 8 | - Symfony v5.3 skeleton, 9 | - Docker Stack for both FPM, Swoole and RoadRunner, and 10 | - A handy Makefile for the SSL stuff. 11 | 12 | ## How? 13 | 14 | Depending on the Docker build target (`fpm`, `swoole`, or `roadrunner`) the PHP container does the following: 15 | 16 | - For build target `fpm`: 17 | - Execute `php-fpm --nodaemonize` 18 | - `APP_RUNTIME` is set to `Symfony\Component\Runtime\SymfonyRuntime` 19 | - For build target `swoole`: 20 | - Execute `php "/public/index.php"` 21 | - `APP_RUNTIME` is set to `Runtime\Swoole\Runtime` 22 | - Environment variables `SWOOLE_HOST` and `SWOOLE_PORT` are set. 23 | - For build target `roadrunner` 24 | - Execute `/sbin/rr serve` 25 | - `APP_RUNTIME` is set to `Runtime\RoadRunnerSymfonyNyholm\Runtime` 26 | - `.rr.yaml` configuration file is created 27 | 28 | In the Nginx container, build target `fpm` uses FastCGI, while build targets `swoole` and `roadrunner` use Reverse Proxy. 29 | 30 | ## Setup Locally 31 | 32 | - You will need [`git`](https://git-scm.com/), [`openssl`](https://www.openssl.org/), 33 | [`make`](https://www.gnu.org/software/make/), and [`mkcert`](https://mkcert.dev/). 34 | - Update values in `.env` (choose `fpm`, `swoole` or `roadrunner` for `${RUNTIME}`). 35 | - `docker-compose build --pull` 36 | - `make password` 37 | - `make mock-ssl` 38 | - `composer install` (or `bin/env composer install` to run it inside the PHP container) 39 | - `docker-compose up -d` 40 | - `mkcert -install` 41 | - Go to `https://${DOMAIN}:${SSL_PORT}` 42 | 43 | ## Production 44 | 45 | You probably shouldn't use this for production, but if you did: 46 | 47 | - `sudo mkdir -p "/etc/letsencrypt/challenges"` 48 | - `docker-compose -f "docker-compose.yaml" run -d --name "acme" server nginx -c "/etc/nginx/acme.conf"` 49 | - `sudo certbot certonly --webroot --webroot-path="/etc/letsencrypt/challenges" --cert-name="docker" -d "${YOUR_DOMAIN}"` 50 | - `sudo openssl dhparam -out "/etc/letsencrypt/dhparam.pem" 4096` 51 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | x-logging: 4 | &default-logging 5 | driver: 'json-file' 6 | options: 7 | max-size: '5m' 8 | max-file: '1' 9 | 10 | services: 11 | 12 | server: 13 | image: 'zanbaldwin/server' 14 | build: 15 | context: './build/docker/server' 16 | target: '${RUNTIME:-fpm}' 17 | args: 18 | NGINX_VERSION: '1.21.0-alpine' 19 | DOMAIN: '${DOMAIN:-localhost}' 20 | restart: 'unless-stopped' 21 | ports: 22 | - target: 80 23 | published: '${WEB_PORT:-80}' 24 | protocol: 'tcp' 25 | mode: 'host' 26 | - target: 443 27 | published: '${SSL_PORT:-443}' 28 | protocol: 'tcp' 29 | mode: 'host' 30 | volumes: 31 | - type: 'bind' 32 | source: './public' 33 | target: '/srv/public' 34 | read_only: true 35 | - type: 'bind' 36 | source: '/etc/letsencrypt' 37 | target: '/etc/letsencrypt' 38 | read_only: true 39 | logging: *default-logging 40 | 41 | php: 42 | image: 'zanbaldwin/php' 43 | build: 44 | context: './build/docker/php' 45 | target: '${RUNTIME:-fpm}' 46 | args: 47 | PHP_VERSION: '8.0-fpm-alpine3.13' 48 | restart: 'unless-stopped' 49 | volumes: 50 | - type: 'bind' 51 | source: './' 52 | target: '/srv' 53 | read_only: false 54 | logging: *default-logging 55 | 56 | database: 57 | image: 'zanbaldwin/database' 58 | build: 59 | context: './build/docker/database' 60 | args: 61 | MYSQL_VERSION: '8.0.25' 62 | restart: 'unless-stopped' 63 | environment: 64 | MYSQL_ROOT_PASSWORD_FILE: '/run/secrets/dbpass' 65 | MYSQL_DATABASE: '${DB_NAME:-main}' 66 | volumes: 67 | - type: 'bind' 68 | source: '/var/lib/mysql' 69 | target: '/var/lib/mysql' 70 | read_only: false 71 | secrets: 72 | - 'dbpass' 73 | logging: *default-logging 74 | 75 | secrets: 76 | dbpass: 77 | file: './build/.secrets/dbpass' 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .SHELLFLAGS := -eu -o pipefail -c 3 | .ONESHELL: 4 | .DELETE_ON_ERROR: 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rules 7 | ifeq ($(origin .RECIPEPREFIX), undefined) 8 | $(error This Make does not support .RECIPEPREFIX; Please use GNU Make 4.0 or later) 9 | endif 10 | .RECIPEPREFIX = > 11 | 12 | THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) 13 | THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd) 14 | THIS_MAKEFILE:=$(notdir $(THIS_MAKEFILE_PATH)) 15 | 16 | usage: 17 | > @grep -E '(^[a-zA-Z_-]+:\s*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.?## "}; {printf "\033[32m %-30s\033[0m%s\n", $$1, $$2}' | sed -e 's/\[32m ## /[33m/' 18 | .PHONY: usage 19 | .SILENT: usage 20 | 21 | mock-ssl: ## Mocks an SSL Certificate for Development 22 | mock-ssl: 23 | > command -v "mkcert" >/dev/null 2>&1 || { echo >&2 "Please install MkCert for Development."; exit 1; } 24 | > export $$(echo "$$(cat "$(THIS_DIR)/.env" | sed 's/#.*//g'| xargs)") 25 | > [ -z "$${DOMAIN}" ] && { echo >&2 "Could not determine domain from environment file."; exit 1; } 26 | > mkdir -p "$(THIS_DIR)/build/ssl/challenges" 27 | > mkdir -p "$(THIS_DIR)/build/ssl/live/docker" 28 | > (cd "$(THIS_DIR)/build/ssl"; mkcert "localhost" "$${DOMAIN}" "127.0.0.1") 29 | > mv "$(THIS_DIR)/build/ssl/localhost+2.pem" "$(THIS_DIR)/build/ssl/live/docker/fullchain.pem" 30 | > cp "$(THIS_DIR)/build/ssl/live/docker/fullchain.pem" "$(THIS_DIR)/build/ssl/live/docker/chain.pem" 31 | > mv "$(THIS_DIR)/build/ssl/localhost+2-key.pem" "$(THIS_DIR)/build/ssl/live/docker/privkey.pem" 32 | > openssl dhparam -out "$(THIS_DIR)/build/ssl/dhparam.pem" 512 33 | > echo >&2 "Check that $$(tput setaf 2)$${DOMAIN}$$(tput sgr0) has been added to \"/etc/hosts\" (add the line \"127.0.0.1 $${DOMAIN}\")." 34 | .PHONY: mock-ssl 35 | .SILENT: mock-ssl 36 | 37 | password: ## Generates a secure, random password for the database 38 | password: 39 | > mkdir -p "$(THIS_DIR)/build/.secrets" 40 | > [ ! -f "$(THIS_DIR)/build/.secrets/dbpass" ] || { \ 41 | echo >&2 "$$(tput setaf 1)A password has already been created. Remove the file \"$(THIS_DIR)/build/.secrets/dbpass\" to try again.$$(tput sgr0)"; \ 42 | echo >&2 "$$(tput setaf 1)Double check that you're NOT REMOVING THE ONLY COPY OF YOUR EXISTING PASSWORD.$$(tput sgr0)"; \ 43 | exit 1; \ 44 | } 45 | > echo "$$(date "+%s.%N" | sha256sum | base64 | head -c 40)" > "$(THIS_DIR)/build/.secrets/dbpass" 46 | > echo >&2 "$$(tput setaf 2)Database password generated and placed in file \"$(THIS_DIR)/build/.secrets/dbpass\".$$(tput sgr0)" 47 | .PHONY: password 48 | .SILENT: password 49 | -------------------------------------------------------------------------------- /bin/env: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ## 4 | # Common *nix PATH directories (such as /usr/local/bin) and therefore anything # 5 | # under them cannot be mounted using Docker for Mac (see # 6 | # https://stackoverflow.com/a/45123074). If you are using Docker for Mac, # 7 | # install Composer to a macOS-specific path (such as "${HOME}/.bin/composer"). # 8 | ## !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ## 9 | 10 | # The root folder of the application (where "composer.json" is). 11 | APP_DIR=".." 12 | # The root folder of the application (where "docker-compose.yaml" is; usually the same as the APP_DIR). 13 | PROJECT_DIR="${APP_DIR}" 14 | 15 | ################################################################################### 16 | ### ENVIRONMENT HELPER ### 17 | ### --------------------------------------------------------------------------- ### 18 | ### Run this script to create a one-off container based on the PHP service for ### 19 | ### CLI work. It will auto-detect if you have Composer installed on your host; ### 20 | ### if so it will mount Composer as a binary inside the container and also ### 21 | ### Composer's cache directory to reduce the amount of downloading required. ### 22 | ### ### 23 | ### Useful if you have differing PHP versions between host and container. ### 24 | ################################################################################### 25 | 26 | # Standardize Paths 27 | realpath() { 28 | # Can't be bothered to refactor this. If you're not using Bash on macOS then 29 | # I'm not going to bother making sure this is compatible. 30 | [[ $1 = /* ]] && echo "$1" || echo "${PWD}/${1#./}" 31 | } 32 | readlink -f "$0" >/dev/null 2>&1 33 | if [ $? -ne 0 ]; then 34 | DIR="$(dirname "$(realpath "$0")")" 35 | APP_DIR="$(realpath "${DIR}/${APP_DIR}")" 36 | PROJECT_DIR="$(realpath "${DIR}/${PROJECT_DIR}")" 37 | else 38 | DIR="$(dirname "$(readlink -f "$0")")" 39 | APP_DIR="$(readlink -f -- "${DIR}/${APP_DIR}")" 40 | PROJECT_DIR="$(readlink -f -- "${DIR}/${PROJECT_DIR}")" 41 | fi 42 | 43 | # Check for Docker Permissions 44 | DOCKER="${DOCKER:-"docker"}" 45 | command -v "${DOCKER}" >/dev/null 2>&1 || { 46 | echo >&2 "$(tput setaf 1)Docker Client \"${DOCKER}\" not available on \$PATH.$(tput sgr0)"; 47 | exit 1; 48 | } 49 | INFO=$("${DOCKER}" info >/dev/null 2>&1) 50 | if [ $? -ne 0 ]; then 51 | echo >&2 "$(tput setaf 1)Docker Daemon unavailable.$(tput sgr0)" 52 | if [ "$(id -u 2>/dev/null)" -ne "0" ]; then 53 | echo >&2 "$(tput setaf 1)Perhaps retry as root?$(tput sgr0)" 54 | fi 55 | exit 1 56 | fi 57 | COMPOSE="${COMPOSE:-"docker-compose"}" 58 | command -v "${COMPOSE}" >/dev/null 2>&1 || { 59 | echo >&2 "$(tput setaf 1)Docker Compose \"${COMPOSE}\" not available on \$PATH.$(tput sgr0)"; 60 | exit 1; 61 | } 62 | 63 | ################################################################################ 64 | ### DETECTING COMPOSER BINARY AND CACHE DIRECTORIES ### 65 | ### ------------------------------------------------------------------------ ### 66 | ### The following is a little unweildy because it will: ### 67 | ### - Attempt to detect the globally-installed Composer binary, but fallback ### 68 | ### onto a "composer.phar" file installed inside the "bin/" project ### 69 | ### directory. ### 70 | ### - Set appropriate "COMPOSER_HOME" env variable (falling back to a tmpfs ### 71 | ### folder in case Composer doesn't exist so that the non-root user can ### 72 | ### still create it if needed). ### 73 | ### - Figure out Composer's home (cache directory) and load it as a volume, ### 74 | ### falling back to creating one inside the "var/" project directory if it ### 75 | ### can't find it. ### 76 | ################################################################################ 77 | 78 | COMPOSER="" 79 | # Use this default when no Composer binary is added, because without it set it will try to create on the root filesystem 80 | # which the non-root user cannot create directories in. 81 | COMPOSER_HOME="/tmp/composer" 82 | # Set the default cache directory to be inside the "var/" project directory (project-specific rather than global). 83 | COMPOSER_HOST_CACHE="${APP_DIR}/var/.composer" 84 | # Try loading the local, project-specific composer.phar first (in case we're on macOS which won't allow mounting 85 | # /private (/usr, /var, etc). If it does not exist, try mounting the globally installed Composer binary. 86 | for COMPOSER_BINARY in "${APP_DIR}/bin/composer.phar" "$(command -v composer 2>/dev/null)"; do 87 | if [ -f "${COMPOSER_BINARY}" ]; then 88 | # Assuming that PHP is installed on the host machine, try determine Composer's global home (cache) directory. 89 | COMPOSER_HOST_HOMEDIR="$(php "${COMPOSER_BINARY}" global config home 2>/dev/null)" 90 | if [ $? -eq 0 ] && [ -d "${COMPOSER_HOST_HOMEDIR}" ]; then 91 | COMPOSER_HOST_CACHE="${COMPOSER_HOST_HOMEDIR}" 92 | elif [ ! -d "${COMPOSER_HOST_CACHE}" ]; then 93 | # Create the cache directory now as the host machine user, rather than let Docker create the volume 94 | # bind as the root user (because then permissions would be all out of whack). 95 | echo "$(tput setaf 2)Creating Composer cache directory...$(tput sgr0)" 96 | mkdir -p "${COMPOSER_HOST_CACHE}" 97 | fi 98 | COMPOSER_HOME="/.composer" 99 | COMPOSER="--volume \"${COMPOSER_BINARY}:/bin/composer:ro\" --volume \"${COMPOSER_HOST_CACHE}:${COMPOSER_HOME}\" -e \"COMPOSER_HOME=${COMPOSER_HOME}\"" 100 | # Hopefully at this point, the contents of the variable $COMPOSER should look something like: 101 | # --volume "/usr/local/bin/composer:/bin/composer:ro" --volume "~/.config/composer:/.composer" 102 | # Break from the loop (we don't want to overwrite the flag string we just constructed). 103 | break 104 | fi 105 | done 106 | 107 | # You can specify a different service defined in docker-compose.yaml by prefixing the command with a variable, like: 108 | # SERVICE=node bin/env npm i 109 | SERVICE="${SERVICE:-"php"}" 110 | 111 | # Specify the project directory to Docker Compose, but if a specific configuration file has been passed, also add that. 112 | COMPOSE_CONFIG="--project-directory=\"${PROJECT_DIR}\"" 113 | if [ ! -z "${COMPOSE_FILE}"]; then 114 | COMPOSE_FILE_PATH="${PROJECT_DIR}/${COMPOSE_FILE}" 115 | if [ ! -f "${COMPOSE_FILE_PATH}" ]; then 116 | echo >&2 "$(tput setaf 1)Docker Compose file \"${COMPOSE_FILE}\" not found.$(tput sgr0)" 117 | exit 1 118 | fi 119 | COMPOSE_CONFIG="${COMPOSE_CONFIG} --file=\"${COMPOSE_FILE}\"" 120 | fi 121 | 122 | COMMAND="sh" 123 | # This little snippet wraps every command-line argument (after "bin/env") in quotes so that arguments with spaces in 124 | # them (such as `bin/env bin/console oauth2:client:create "My Client Name"`) do not get turned into separate arguments 125 | # (such as `bin/env bin/console oauth2:client:create "My" "Client" "Name"`). 126 | if [ $# -gt 0 ]; then 127 | COMMAND="" 128 | for ARG in "$@"; do 129 | COMMAND="${COMMAND} \"${ARG}\"" 130 | done 131 | fi 132 | 133 | SCRIPT="\"${COMPOSE}\" ${COMPOSE_CONFIG} run --rm --user=\"$(id -u):$(id -g)\" ${COMPOSER} -e \"TERM=xterm\" \"${SERVICE}\" ${COMMAND}" 134 | 135 | "${SHELL:-"sh"}" -c "${SCRIPT}" 136 | exit $? 137 | --------------------------------------------------------------------------------