When I first started experimenting with Docker, I assumed every container had to run exactly one process, no exceptions. That rule of thumb is good advice for production microservices, but it isn’t a hard technical limit, and there are legitimate situations where I want several processes living inside the same container. Running WordPress is the classic example: WordPress needs PHP-FPM (or Apache with mod_php) to execute PHP code, MySQL or MariaDB to store data, and often nginx to serve static files and proxy PHP requests. Splitting all of that across separate containers is the “correct” Docker way, but sometimes — for a quick demo, a legacy app migration, a single-VM deployment, or a training environment — I want everything bundled into one image that starts with a single docker run command. That’s where Supervisor comes in.
In this guide I’ll walk through exactly how I build a single Docker container that runs nginx, PHP-FPM, MySQL, and WordPress together, all managed by Supervisor as an init-like process manager.
Why a Single Container Needs a Process Manager
A Docker container’s main process (PID 1) is whatever command you specify in CMD or ENTRYPOINT. When that process exits, the container stops — Docker doesn’t automatically restart it. If I try to start MySQL and nginx from the same CMD using something like mysqld & nginx, the shell forks both into the background, but the shell itself exits almost immediately, and Docker kills the container. Even if I keep the shell alive with wait, I lose proper signal handling, zombie process reaping, and automatic restarts if one of the services crashes.
Supervisor solves this by acting as PID 1 itself. It reads a configuration file describing each program it should manage, starts them all, restarts any that die, and forwards logs to wherever I tell it to.
Prerequisites
- Docker installed and working (
docker --versionshould return something likeDocker version 27.x.x) - Basic familiarity with Dockerfiles
- About 500MB of free disk space for the image
Step 1: Plan the Directory Structure
I keep my build context organized like this:
wordpress-supervisor/
├── Dockerfile
├── supervisord.conf
├── nginx.conf
└── wp-config-docker.php
Step 2: Write the Dockerfile
I base the image on Ubuntu so I have full control over every package, though a Debian-slim base works just as well.
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
nginx \
mysql-server \
php8.1-fpm php8.1-mysql php8.1-gd php8.1-curl php8.1-xml php8.1-mbstring \
supervisor \
curl \
wget \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Download and extract WordPress
RUN curl -o wordpress.tar.gz -SL https://wordpress.org/latest.tar.gz \
&& tar -xzf wordpress.tar.gz -C /var/www/ \
&& rm wordpress.tar.gz \
&& chown -R www-data:www-data /var/www/wordpress
# Copy configs
COPY nginx.conf /etc/nginx/sites-available/default
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY wp-config-docker.php /var/www/wordpress/wp-config.php
# Prepare MySQL data directory
RUN mkdir -p /var/lib/mysql /var/run/mysqld \
&& chown -R mysql:mysql /var/lib/mysql /var/run/mysqld
EXPOSE 80
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
Step 3: Configure nginx to Talk to PHP-FPM
nginx.conf:
server {
listen 80;
server_name localhost;
root /var/www/wordpress;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
Step 4: Write the Supervisor Configuration
This is the heart of the whole setup. Each [program:x] block tells Supervisor how to start, monitor, and restart a service.
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:mysqld]
command=/usr/sbin/mysqld –user=mysql autostart=true autorestart=true priority=1 stdout_logfile=/var/log/mysql/mysql.out.log stderr_logfile=/var/log/mysql/mysql.err.log
[program:php-fpm]command=/usr/sbin/php-fpm8.1 -F autostart=true autorestart=true priority=10 stdout_logfile=/var/log/php-fpm.out.log stderr_logfile=/var/log/php-fpm.err.log
[program:nginx]command=/usr/sbin/nginx -g “daemon off;” autostart=true autorestart=true priority=20 stdout_logfile=/var/log/nginx.out.log stderr_logfile=/var/log/nginx.err.log
A few details matter here:
nodaemon=truekeeps Supervisor itself in the foreground so it can act as PID 1.prioritycontrols startup order — MySQL should come up before PHP-FPM tries to connect to it, and PHP-FPM before nginx starts serving requests.autorestart=trueis what gives me resilience: if PHP-FPM crashes, Supervisor brings it back without restarting the whole container.
Step 5: Handle the Database Bootstrap Problem
MySQL needs an initialized data directory before it can start, and WordPress needs a database and user to exist. I handle this with a small entrypoint wrapper instead of relying on Supervisor alone:
#!/bin/bash
if [ ! -d "/var/lib/mysql/mysql" ]; then
mysqld --initialize-insecure --user=mysql
service mysql start
mysql -e "CREATE DATABASE wordpress;"
mysql -e "CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'wppassword';"
mysql -e "GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"
mysqladmin shutdown
fi
exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf
I set this as the ENTRYPOINT in the Dockerfile instead of calling supervisord directly, so the database is guaranteed to exist before Supervisor starts managing the long-running processes.
Step 6: Build and Run
docker build -t wordpress-allinone .
docker run -d -p 8080:80 --name wp-single wordpress-allinone
Expected output for the build’s final lines:
Successfully built 8f3a2c9d1e4b
Successfully tagged wordpress-allinone:latest
Then check it’s alive:
docker ps
CONTAINER ID IMAGE COMMAND STATUS PORTS
7a1b2c3d4e5f wordpress-allinone "/entrypoint.sh" Up 10 seconds 0.0.0.0:8080->80/tcp
Visiting http://localhost:8080 should show the WordPress installation wizard.
Internal Working: What’s Actually Happening
Inside the container, supervisord runs as PID 1. It forks a child process for each [program] block using fork()/exec(), tracks each child’s PID, and listens for SIGCHLD signals to detect crashes. When a monitored process dies unexpectedly, Supervisor’s state machine moves it from RUNNING to EXITED, then, because autorestart=true, back to STARTING and RUNNING again — all without Docker seeing the container itself stop.
This matters for container lifecycle semantics: Docker only tracks PID 1. Everything else is invisible to docker inspect‘s exit code unless Supervisor itself dies. That’s a trade-off I accept knowingly — I lose Docker-level restart policies for individual services in exchange for simplicity.
Networking and Storage Considerations
Because nginx, PHP-FPM, and MySQL communicate over localhost (via Unix sockets or 127.0.0.1), there’s no Docker network overhead between them — they’re not separate containers, so there’s no bridge network hop. The trade-off is that I can’t scale nginx and PHP-FPM independently, and a MySQL crash takes down the database layer of an app that’s otherwise still serving static assets.
For persistence, I mount a volume over /var/lib/mysql so database data survives container recreation:
docker run -d -p 8080:80 \
-v wp_db_data:/var/lib/mysql \
-v wp_content_data:/var/www/wordpress/wp-content \
--name wp-single wordpress-allinone
Security Notes
Running MySQL as root inside a container that also runs a web server increases the blast radius of any compromise. In a real production setup I would:
- Run each service under its own non-root system user (which the packages already default to —
mysql,www-data). - Avoid baking credentials into the image; pass them via environment variables and template the config at container startup.
- Regularly rebuild the image to pick up security patches for nginx, PHP, and MySQL.
- Consider
read_onlyroot filesystem with explicit writable volumes for/var/lib/mysql,/var/www/wordpress/wp-content, and log directories.
Troubleshooting
Container exits immediately after docker run Check docker logs wp-single. Almost always this means one of the Supervisor-managed commands isn’t found (wrong binary path) or MySQL’s data directory initialization failed.
502 Bad Gateway from nginx This means nginx started before PHP-FPM’s socket was ready, or the socket path in nginx.conf doesn’t match the actual PHP-FPM configuration. Check with:
docker exec -it wp-single supervisorctl status
Expected healthy output:
mysqld RUNNING pid 45, uptime 0:02:15
php-fpm RUNNING pid 52, uptime 0:02:10
nginx RUNNING pid 58, uptime 0:02:05
MySQL “Access denied” errors Usually the initialization script ran but the credentials in wp-config.php don’t match what was created. I double-check the DB_USER, DB_PASSWORD, and DB_NAME constants line up exactly with the CREATE USER and GRANT statements.
Monitoring
Supervisor exposes an XML-RPC interface and supervisorctl for live status checks, which I use both interactively and in health-check scripts:
docker exec wp-single supervisorctl status
I can also wire this into Docker’s own HEALTHCHECK instruction:
HEALTHCHECK --interval=30s --timeout=5s \
CMD supervisorctl status | grep -q "RUNNING" || exit 1
Best Practices
- Treat this pattern as an exception, not the default — for anything beyond a demo or single-node deployment, split WordPress into separate
wordpress,mysql, andnginxcontainers connected by Docker Compose. - Always set
autorestart=truefor anything that must survive minor crashes. - Keep Supervisor’s own log file mounted to a volume so you don’t lose crash history when the container is removed.
- Pin package versions in the Dockerfile so rebuilds are reproducible.
Summary
Supervisor turns a single Docker container into a miniature multi-service host by acting as an init system that starts, monitors, and restarts nginx, PHP-FPM, and MySQL together. It’s not the pattern I’d choose for a scalable production WordPress deployment, but it’s genuinely useful for compact demos, legacy migrations, and single-instance environments where simplicity beats architectural purity. Once the Supervisor config is right, the container behaves like a tiny, self-healing virtual machine.
References
- Docker documentation: https://docs.docker.com/
- Supervisor documentation: http://supervisord.org/
- WordPress official Docker guidance: https://hub.docker.com/_/wordpress
- Docker “Run multiple services in a container” guide: https://docs.docker.com/config/containers/multi-service_container/
