Hi! We're Dan and Jay. We're a two person team with a passion for open source products. We created Server Side Up to help share what we learn.
- ๐ 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.
![deligoez](https://github.com/deligoez.png)
![alexjustesen](https://github.com/alexjustesen.png)
This is a list of the docker images this repository creates:
โ๏ธ Variation | ๐ Version |
---|---|
cli | |
fpm | |
fpm-apache | |
fpm-nginx |
Simply use this image name pattern in any of your projects:
serversideup/php:{{version}}-{{variation-name}}
For example... If I wanted to run PHP 8.0 with FPM + NGINX, I would use this image:
serversideup/php:8.0-fpm-nginx
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:
โ The image builds automatically run weekly (Tuesday at 0800 UTC) for latest security updates.
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];
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 โ
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.
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.
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.
These images have a few key differences. These images are:
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.
We have a ton of helpful scripts and security settings configured for managing Laravel and WordPress.
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 AUTORUN_LARAVEL_STORAGE_LINK=false
.
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"
All you need to do is pass the Laravel Queue command to the container and S6 will automatically monitor it for you.
Task Command:
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"
By passing Laravel Horizon to our container, S6 will automatically monitor it.
Horizon Command:
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"
- Hardening of Apache & NGINX included
- Disabling of XML-RPC
- Preventative access to sensitive version control or CI files
- Protection against other common attacks
See our Apache security.conf and NGINX security.conf for more detail.
If you're looking for a deeper example on how we run our WordPress blog, Server Side Up, check out this repository for a boilerplate example: https://github.com/serversideup/docker-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).
๐ Variable Name | ๐ Description | โ๏ธ Used in variation | #๏ธโฃ Default Value |
---|---|---|---|
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, fpm-nginx, fpm-apache |
"UTC" |
PHP_DISPLAY_ERRORS | Show PHP errors on screen. (Official docs) | fpm, fpm-nginx, fpm-apache |
Off |
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, fpm-nginx, fpm-apache |
"22527" |
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, fpm-nginx, fpm-apache |
"99" |
PHP_MEMORY_LIMIT | Set the maximum amount of memory in bytes that a script is allowed to allocate. (Official docs) | fpm, fpm-nginx, fpm-apache |
"256M" |
PHP_PM_CONTROL | Choose how the process manager will control the number of child processes. (Official docs) | fpm, fpm-nginx, fpm-apache |
fpm: dynamic fpm-apache: ondemand fpm-nginx: ondemand |
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, fpm-nginx, fpm-apache |
"20" |
PHP_PM_MAX_SPARE_SERVERS | The desired maximum number of idle server processes. Used only when pm is set to dynamic. (Official docs) | fpm, fpm-nginx, fpm-apache |
"3" |
PHP_PM_MIN_SPARE_SERVERS | The desired minimum number of idle server processes. Used only when pm is set to dynamic. (Official docs) | fpm, fpm-nginx, fpm-apache |
"1" |
PHP_PM_START_SERVERS | The number of child processes created on startup. Used only when pm is set to dynamic. (Official docs) | fpm, fpm-nginx, fpm-apache |
"2" |
PHP_POOL_NAME | Set the name of your PHP-FPM pool (helpful when running multiple sites on a single server). | fpm, fpm-nginx, fpm-apache |
"www" |
PHP_POST_MAX_SIZE | Sets max size of post data allowed. (Official docs) | fpm, fpm-nginx, fpm-apache |
"100M" |
PHP_UPLOAD_MAX_FILE_SIZE | The maximum size of an uploaded file. (Official docs) | fpm, fpm-nginx, fpm-apache |
"100M" |
PHP_OPEN_BASEDIR | Limit the files that can be accessed by PHP to the specified directory-tree, including the file itself. | fpm, fpm-nginx, fpm-apache |
$WEBUSER_HOME:/dev/stdout:/tmp |
AUTORUN_ENABLED | Enable or disable all autoruns. It's advised to set this to false in certain CI environments (especially during a composer install) |
fpm, fpm-nginx, fpm-apache |
"true" |
AUTORUN_LARAVEL_STORAGE_LINK | Automatically run "php artisan storage:link" on container start | fpm, fpm-nginx, fpm-apache |
"true" |
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, fpm-nginx, fpm-apache |
"false" |
MSMTP_RELAY_SERVER_HOSTNAME | Server that should relay emails for MSMTP. (Official docs) | fpm-nginx, fpm-apache |
"mailhog" ๐จ IMPORTANT: Change this value if you want emails to work. (we set it to Mailhog so our staging sites do not send emails out) |
MSMTP_RELAY_SERVER_PORT | Port the SMTP server is listening on. (Official docs) | fpm-nginx, fpm-apache |
"1025" (default port for Mailhog) |
DEBUG_OUTPUT | Set this variable to true if you want to put PHP and your web server in debug mode. |
fpm-nginx, fpm-apache |
(undefined, false) |
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, fpm-apache |
"full" |
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
Notice the services.php.build
options. We set a .
to look for a dockerfile called Dockerfile
within the same directory as our docker-compose.yml
file.
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.
Dockerfile:
# 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.
- 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
--build
with your Docker compose command (read more here)
If you want to rebuild, then you would run this:
docker compose up --build
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).
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 php-apache
and php-nginx
configurations.
Value of $SSL_MODE |
Description |
---|---|
"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. |
If you use mixed
or full
for your "SSL_MODE", we will check for certificate pairs at the following locations:
- /etc/ssl/web/ssl.crt
- /etc/ssl/web/ssl.key
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.
- 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.
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.
If you find a critical security flaw, please open an issue or learn more about our responsible disclosure policy.