Find us at:
📖Blog - get the latest guides and free courses on all things web/mobile development. 🙋Community - get friendly help from our community members. 🤵♂️Get Professional Help - get guaranteed responses within next business day. 💻GitHub - check out our other open source projects 📫Newsletter - skip the algorithms and get quality content right to your inbox 🐥Twitter - you can also follow Dan and Jay ❤️Sponsor Us - please consider sponsoring us so we can create more helpful resources
All of our software is free an open to the world. None of this can be brought to you without the financial backing of our sponsors.
Available Docker Images
This is a list of the docker images this repository creates:
👉 Warning: PHP 8.2 is still in BETA
PHP 8.2 is still considered a "pre-release" by the official PHP team. Learn more here →
Simply use this image name pattern in any of your projects:
For example... If I wanted to run PHP 8.0 with FPM + NGINX, I would use this image:
Real-life working example
You can see a bigger picture on how these images are used from Development to Production by viewing this video that shows a high level overview how we deploy "ROAST" which is a demo production app for our book.
Click the image below to view the video:
How these images are built
All images are built off of the official Ubuntu 22.04 docker image. We first build our CLI image, then our FPM, etc. Here is what this looks like:
graph TD; A[Ubuntu 22.04 + S6 Overlay] --> C[CLI]; C[CLI] --> D[FPM]; D[FPM] --> E[FPM-NIGNX]; D[FPM] --> F[FPM-APACHE];
Where do you host your stuff?
We get this question often. Our biggest principle is: your infrastructure should be able to run anywhere.
We believe privacy and control is the #1 priority when it comes to hosting infrastructure. We try to avoid the "big clouds" as much as possible because we're not comfortable that all 3 major players practice data mining of users and their products usually contain some sort of "vendor-lock".
We run all of our production servers on the latest LTS release of Ubuntu Server. The hosts we use are below. Some may be affiliate links that kick a few bucks at no extra cost to you, but they do not affect our recommendations at all.
Our current favorite. Excellent performance and value. Lots of datacenter options too.
Lots of developer love here. Not the best performing servers, but they do have a lot of awesome products!
Great performance and great support. These guys have really enhanced their offering over the last few years.
If you're shopping for a host, check out the benchmarks we've ran →
Can I run this on another host?
Sure! It all depends what platform you want to use, but if it supports Docker images, you likely can run it. These images are designed to give you freedom no matter where you want to run them.
About this project
We're taking the extra effort to open source as much as we can. Not only could this potentially help someone learn a little bit of Docker, but it makes it a heck of a lot easier for us to work with you on new open source ideas.
Project credits & inspiration
Majority of our knowledge came from Chris' course, Shipping Docker. If you have yet to discover his content, you will be very satisfied with every course he has to offer. He's a great human being and excellent educator.
This team has an excellent repository and millions of pulls per month. We really like how they structured their code.
These guys are absolute aces when it comes to Docker development. They are a great resource for tons of open source Docker images.
Why these images and not other ones?
These images have a few key differences. These images are:
🚀 These images are used in production
Our philosophy is: What you run in production is what you should be running in development.
You'd be shocked how many people create a Docker image and use it in the local development only. These images are designed with the intention of being deployed to the open and wild Internet.
🔧 Optimized for Laravel and WordPress
We have a ton of helpful scripts and security settings configured for managing Laravel and WordPress.
Automated tasks executed on every container start up
We automatically detect if Laravel is installed and give you the option to enable automatic migrations and apply storage linking.
php artisan migrate --force
Automatic migrations are DISABLED by default. To enable, set an environment variable of
AUTORUN_LARAVEL_MIGRATION=true on your container. We do not recommend enabling this on large or distributed applications. You should run your migrations manually for larger apps.
php artisan storage:link
Storage linking is ENABLED by default. You can disable this behavior by setting
Running a Laravel Task Scheduler
We need to run the schedule:work command from Laravel. Although the docs say "Running the scheduler locally", this is what we want in production. It will run the scheduler in the foreground and execute it every minute. You can configure your Laravel app for the exact time that a command should run through a scheduled task.
Task Scheduler Command:
php artisan schedule:work
Example Docker Compose File:
version: '3' services: php: image: my/laravel-app environment: PHP_POOL_NAME: "my-app_php" task: image: my/laravel-app # Switch to "webuser" before running `php artisan` # Declare command in list manner for environment variable expansion command: ["su", "webuser", "-c", "php artisan schedule:work"] environment: PHP_POOL_NAME: "my-app_task"
Running a Laravel Queue
All you need to do is pass the Laravel Queue command to the container and S6 will automatically monitor it for you.
php artisan queue:work --tries=3
Example Docker Compose File:
version: '3' services: php: image: my/laravel-app environment: PHP_POOL_NAME: "my-app_php" queue: image: my/laravel-app # Switch to "webuser" before running `php artisan` # Declare command in list manner for environment variable expansion command: ["su", "webuser", "-c", "php artisan queue:work --tries=3"] environment: PHP_POOL_NAME: "my-app_queue"
Running Laravel Horizon with a Redis Queue
By passing Laravel Horizon to our container, S6 will automatically monitor it.
php artisan horizon
Example Docker Compose File:
version: '3' services: php: image: my/laravel-app environment: PHP_POOL_NAME: "my-app_php" redis: image: redis:6 command: "redis-server --appendonly yes --requirepass redispassword" horizon: image: my/laravel-app # Switch to "webuser" before running `php artisan` # Declare command in list manner for environment variable expansion command: ["su", "webuser", "-c", "php artisan horizon"] environment: PHP_POOL_NAME: "my-app_horizon"
🔑 WordPress & Security Optimizations
- Hardening of Apache & NGINX included
- Disabling of XML-RPC
- Preventative access to sensitive version control or CI files
- Protection against other common attacks
Examples of running WordPress
🧐 Based off of S6 Overlay
S6 Overlay is very helpful in managing a container's lifecycle that has multiple processes.
Wait... Isn't Docker supposed to be a "single process per container"? Yes, that's what it's like in a perfect world. Unfortunately PHP isn't like that. You need both a web server and a PHP-FPM server to see your files in order for your application to load.
We follow the S6 Overlay Philosophy on how we can still get a single, disposable, and repeatable image of our application out to our servers.
We like to customize our images on a per app basis using environment variables. Look below to see what variables are available and what their defaults are. You can easily override them in your own docker environments (see Docker's documentation).
|PUID||User ID the webserver and PHP should run as.||all||9999|
|PGID||Group ID the webserver and PHP should run as.||all||9999|
|WEBUSER_HOME||BETA: You can change the home of the web user if needed.||all (except *-nginx)||/var/www/html|
|PHP_DATE_TIMEZONE||Control your timezone. (Official Docs)||fpm,
|PHP_DISPLAY_ERRORS||Show PHP errors on screen. (Official docs)||fpm,
|PHP_DISPLAY_STARTUP_ERRORS||Even when display_errors is on, errors that occur during PHP's startup sequence are not displayed. (Official docs)||Off|
|PHP_ERROR_REPORTING||Set PHP error reporting level. Must be a number. Use this tool for help. (Official docs)||fpm,
|PHP_MAX_EXECUTION_TIME||Set the maximum time in seconds a script is allowed to run before it is terminated by the parser. (Official docs)||fpm,
|PHP_MEMORY_LIMIT||Set the maximum amount of memory in bytes that a script is allowed to allocate. (Official docs)||fpm,
|PHP_PM_CONTROL||Choose how the process manager will control the number of child processes. (Official docs)||fpm,
|PHP_PM_MAX_CHILDREN||The number of child processes to be created when pm is set to static and the maximum number of child processes to be created when pm is set to dynamic. (Official docs)||fpm,
|PHP_PM_MAX_SPARE_SERVERS||The desired maximum number of idle server processes. Used only when pm is set to dynamic. (Official docs)||fpm,
|PHP_PM_MIN_SPARE_SERVERS||The desired minimum number of idle server processes. Used only when pm is set to dynamic. (Official docs)||fpm,
|PHP_PM_START_SERVERS||The number of child processes created on startup. Used only when pm is set to dynamic. (Official docs)||fpm,
|PHP_POOL_NAME||Set the name of your PHP-FPM pool (helpful when running multiple sites on a single server).||fpm,
|PHP_POST_MAX_SIZE||Sets max size of post data allowed. (Official docs)||fpm,
|PHP_UPLOAD_MAX_FILE_SIZE||The maximum size of an uploaded file. (Official docs)||fpm,
|PHP_OPEN_BASEDIR||Limit the files that can be accessed by PHP to the specified directory-tree, including the file itself.||fpm,
|AUTORUN_ENABLED||Enable or disable all autoruns. It's advised to set this to
|AUTORUN_LARAVEL_STORAGE_LINK||Automatically run "php artisan storage:link" on container start||fpm,
|AUTORUN_LARAVEL_MIGRATION||Automatically run "php artisan migrate --force" on container start. This is not recommended for large or distributed apps. Run your migrations manually instead.||fpm,
|MSMTP_RELAY_SERVER_HOSTNAME||Server that should relay emails for MSMTP. (Official docs)||fpm-nginx,
|MSMTP_RELAY_SERVER_PORT||Port the SMTP server is listening on. (Official docs)||fpm-nginx,
|"1025" (default port for Mailhog)|
|DEBUG_OUTPUT||Set this variable to
|APACHE_DOCUMENT_ROOT||Sets the directory from which Apache will serve files. (Official docs)||fpm-apache||"/var/www/html"|
|APACHE_MAX_CONNECTIONS_PER_CHILD||Sets the limit on the number of connections that an individual child server process will handle.(Official docs)||fpm-apache||"0"|
|APACHE_MAX_REQUEST_WORKERS||Sets the limit on the number of simultaneous requests that will be served. (Official docs)||fpm-apache||"150"|
|APACHE_MAX_SPARE_THREADS||Maximum number of idle threads. (Official docs)||fpm-apache||"75"|
|APACHE_MIN_SPARE_THREADS||Minimum number of idle threads to handle request spikes. (Official docs)||fpm-apache||"10"|
|APACHE_RUN_GROUP||Set the username of what Apache should run as.||fpm-apache||"webgroup"|
|APACHE_RUN_USER||Set the username of what Apache should run as.||fpm-apache||"webuser"|
|APACHE_START_SERVERS||Sets the number of child server processes created on startup.(Official docs)||fpm-apache||"2"|
|APACHE_THREAD_LIMIT||Set the maximum configured value for ThreadsPerChild for the lifetime of the Apache httpd process. (Official docs)||fpm-apache||"64"|
|APACHE_THREADS_PER_CHILD||This directive sets the number of threads created by each child process. (Official docs)||fpm-apache||"25"|
|COMPOSER_ALLOW_SUPERUSER||Disable warning about running as super-user||all||"1"|
|COMPOSER_HOME||The COMPOSER_HOME var allows you to change the Composer home directory. This is a hidden, global (per-user on the machine) directory that is shared between all projects.||all||"/composer"|
|COMPOSER_MAX_PARALLEL_HTTP||Set to an integer to configure how many files can be downloaded in parallel. This defaults to 12 and must be between 1 and 50. If your proxy has issues with concurrency maybe you want to lower this. Increasing it should generally not result in performance gains.||all||"24"|
|S6_VERBOSITY||Set the verbosity of "S6 Overlay" (the init system these images are based on). The default is "1" (print warnings and errors). The scale goes from 1 to 5, but the output will quickly become very noisy. If you're having issues, start here. You can also customize many other variables. (Official docs)||all||"1"|
|SSL_MODE||Configure how you would like to handle SSL. This can be "off" (HTTP only), "mixed" (HTTP + HTTPS), or "full" (HTTPS only)||fpm-nginx,
Installing additional PHP extensions
Let's say that we have a basic Docker compose image working in development:
version: '3.7' services: php: image: serversideup/php:8.0-fpm-nginx volumes: - .:/var/www/html/:cached
Now let's say we want to add the PHP ImageMagick extension. To do this, we will use the docker compose build option in our YAML file.
This means we would need to change our file above to look like:
version: '3.7' services: php: build: context: . dockerfile: Dockerfile volumes: - .:/var/www/html/:cached
services.php.build options. We set a
. to look for a dockerfile called
Dockerfile within the same directory as our
For extra clarity, my project directory would look like this:
. ├── Dockerfile ├── docker-compose.yml └── public └── index.php
The Dockerfile is where all the magic will happen. This is where we pull the Server Side Up image as a dependency, then run standard Ubuntu commands to add the extension that we need.
# Set our base image FROM serversideup/php:8.0-fpm-nginx # Install PHP Imagemagick using regular Ubuntu commands RUN apt-get update \ && apt-get install -y --no-install-recommends php8.0-imagick \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
The next time you run
docker compose up, Docker will build and cache the image for you automatically.
You can verify the CLI option installed correctly by echoing out the installed modules. Run this command in a new window while your containers are running via Docker Compose:
docker compose exec php php -m
To check that PHP-FPM loaded everything properly, use the phpinfo() functionally.
⚠️ Important note about caching
- You'll notice Docker likes to cache image builds (which is great for most functions)
- If you make changes to your Dockerfile, you may need to include
--buildwith your Docker compose command (read more here)
If you want to rebuild, then you would run this:
docker compose up --build
How do I know which package name to use?
Refer to the official instructions of the extension that you are trying to install. We use Ondrej's PHP repository, so chances are you might be able to find in in here: https://launchpad.net/~ondrej/+archive/ubuntu/php/+packages
Make sure to use the same version number as well. For example... If you are using
8.0 and want to install the php-imagick package, use the name
php8.0-imagick during install (see my examples above).
Production SSL Configurations
By default, we generate a self-signed certificate for simple local development. For production use, we recommend using as a proxy to your actual container.
You have a few options for using SSL in production. These configurations are only supported in the
|"off"||This will disable any SSL management and will use HTTP only. Direct all your container traffic to port 80.|
|"mixed"||This will support HTTP and HTTPS connections. You can send traffic to port 80 or 443.|
|"full" (default)||This will provide "end-to-end encryption" to your web server. Any HTTP traffic will be redirected to HTTPS.|
Using your own certificates
If you use
full for your "SSL_MODE", we will check for certificate pairs at the following locations:
Simply use Docker Volumes and mount the
/etc/ssl/web folder with these two files in that directory.
If we do not find a certificate pair, we will generate a self-signed certificate pair for you.
The easiest way to get a trusted certificate
- Use a proxy that supports Let's Encrypt (like Traefik or Caddy)
- Make sure you allow your proxy to direct traffic encrypted with self-signed certificates (if you're proxying to the container with a self-signed certificate)
This is what we do and it's really nice to use the automatic Let's Encrypt SSL management with these products.
Submitting issues and pull requests
Since there are a lot of dependencies on these images, please understand that it can make it complicated on merging your pull request.
We'd love to have your help, but it might be best to explain your intentions first before contributing.
Like we said -- we're always learning
If you find a critical security flaw, please open an issue or learn more about our responsible disclosure policy.