Skip to content
Go back

Install a php script in PHP-FPM & Caddy via Docker

By SumGuy 5 min read
Install a php script in PHP-FPM & Caddy via Docker

Install docker

Either regular docker installregular docker install or rootlessrootless.

Create a new directory

Create a new directory where you will store your Docker Compose file and PHP/html etc files. For example, you can create a directory called “phpapp” in your home directory:

mkdir -p ./phpapp

write a docker compose file

paste the below code in a new file called docker-compose.yml.

This defines two services: “caddy” and “phpapp”. The “caddy” service uses the Caddy v2 image and mounts the Caddyfile and public directory as volumes. It also exposes ports 80 and 443. The “phpapp” service uses the PHP-FPM imagePHP-FPM image from my GitHub repoGitHub repo (you can find other PHP versions along with other base operating systems (debian/alpine for example)) that’s updated weekly with the newest upstream code and adds many useful php extensions along with composer and mounts the public directory as a volume.

services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./public:/srv
- ./data:/data
- ./caddy_config:/config
ports:
- "80:80"
- "443:443"
networks:
- net
phpapp:
image: ghcr.io/kingpin/php-docker:8.1-fpm-alpine
container_name: phpapp
restart: unless-stopped
volumes:
- ./public:/srv
networks:
- net
networks:
net:

write the Caddyfile

create a new file called Caddyfile in the same directory. This file defines the root directory as /srv and sets up a file server to serve files from that directory. It also sets up a FastCGI proxy to the PHP-FPM service on port 9000. This will also reach out to letsencrypt or zerossl and acquire an SSL cert for your domain example.com

example.com {
root * /srv
file_server
php_fastcgi phpapp:9000
}

PHP test file

Create a new directory called “public” in the same directory as your docker-compose.yml file and add an index.php file with the following code:

<?php
phpinfo();
?>

Start the Docker container stack

Start the services by running the following command in your terminal:

docker compose up -d && docker compose logs -f

This will start the two services in your docker compose file and attach them to your terminal. You should see logs from both services.

Test in your browser

Open your web browser and navigate to http://example.com. You should see a PHP info page displayed, indicating that PHP-FPM is working correctly.

Move your code into the public folder

Remove the index.php file or rename it to phpinfo.php and put your php code in the public folder along with any html/js/css/etc files.

That’s it! You now have PHP-FPM and Caddy v2 running via Docker Compose. You can customize the Caddyfile and docker-compose.yml files to suit your needs, and add additional services as necessary.

What actually breaks this setup

The happy path works great. Here’s what trips people up in practice.

PHP-FPM can’t reach your files

Both containers mount the same ./public:/srv volume — that’s the whole trick. If you see a blank page or a 502 from Caddy, the most common cause is that the phpapp container and the caddy container ended up with different volume mounts, or you moved your files somewhere other than public/. Check with:

Terminal window
docker compose exec phpapp ls /srv

If that comes back empty, your files aren’t where PHP-FPM is looking. Fix the volume path and docker compose restart phpapp.

502 Bad Gateway on first boot

Caddy starts fast. PHP-FPM takes a second longer to initialize the worker pool. If you hit the site in the first few seconds, Caddy can’t reach phpapp:9000 yet and you get a 502. It’ll clear on its own, but if you want to avoid it, add a healthcheck so Caddy waits:

docker-compose.yml
phpapp:
image: ghcr.io/kingpin/php-docker:8.1-fpm-alpine
container_name: phpapp
restart: unless-stopped
healthcheck:
test: ["CMD", "php-fpm", "-t"]
interval: 10s
timeout: 5s
retries: 3
volumes:
- ./public:/srv
networks:
- net

TLS fails and Caddy just… doesn’t tell you

Caddy will silently fall back to HTTP if it can’t get a cert, which is useful locally but alarming in prod. Most common reasons:

Caddyfile
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
example.com {
root * /srv
file_server
php_fastcgi phpapp:9000
}

Remove the global acme_ca block once everything’s working and you’re ready for the real cert.

Permissions headache with uploaded files

If your PHP app writes files (uploads, cache, sessions), the PHP-FPM process owns them as www-data (uid 82 on Alpine). If you then try to manage those files as your host user, you’ll get permission denied errors. You’ve got two options: run chown -R 82:82 ./public on the host, or add user: "1000:1000" to the phpapp service if your image supports it. The custom image from the repo handles this cleanly — check the README for the exact UID.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Ed25519 ssh keys
Next Post
Ubuntu & Bash tutorial & basic utilities

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts