Production-ready Docker images for PHP. Optimized for Laravel, WordPress, and more!
GNU General Public License v3.0

Docker Images Logo

Build Status License Support us
Docker Pulls Discourse users Discord

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.

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

Our Sponsors

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.


Individual Supporters

deligoez  alexjustesen  

Available Docker Images

This is a list of the docker images this repository creates:

⚙️ Variation 🚀 Version
cli serversideup/php:8.2-cli
fpm serversideup/php:8.2-fpm
fpm-apache serversideup/php:8.2-fpm-apache
fpm-nginx serversideup/php:8.2-fpm-nginx

👉 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:

Laravel + NuxtJS From Dev to production


The image builds automatically run weekly (Tuesday at 0800 UTC) for latest security updates.

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.

Digital Ocean

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

Chris Fidao

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.

Database Migrations:

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.

Storage Linking:

php artisan storage:link

Storage linking is ENABLED by default. You can disable this behavior by setting AUTORUN_LARAVEL_STORAGE_LINK=false.

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'
    image: my/laravel-app
      PHP_POOL_NAME: "my-app_php"

    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"]
      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.

Task Command:

php artisan queue:work --tries=3

Example Docker Compose File:

version: '3'
    image: my/laravel-app
      PHP_POOL_NAME: "my-app_php"

    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"]
      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.

Horizon Command:

php artisan horizon

Example Docker Compose File:

version: '3'
    image: my/laravel-app
      PHP_POOL_NAME: "my-app_php"

    image: redis:6
    command: "redis-server --appendonly yes --requirepass redispassword"

    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"]
      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

See our Apache security.conf and NGINX security.conf for more detail.

Examples of running WordPress

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:

🧐 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.

Environment Variables

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,
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,
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,
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 false in certain CI environments (especially during a composer install) fpm,
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,

🚨 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,
"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,
(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,

Other customizations

Installing additional PHP extensions

Let's say that we have a basic Docker compose image working in development:

version: '3.7'
    image: serversideup/php:8.0-fpm-nginx
      - .:/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'
      context: .
      dockerfile: Dockerfile
      - .:/var/www/html/:cached

Notice the 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.


# 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 --build with 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:

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 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.

Using your own certificates

If you use mixed or full for your "SSL_MODE", we will check for certificate pairs at the following locations:

  1. /etc/ssl/web/ssl.crt
  2. /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.

The easiest way to get a trusted certificate

  1. Use a proxy that supports Let's Encrypt (like Traefik or Caddy)
  2. 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.