Small web infrastructure with Docker Compose, NGINX, WordPress + PHP-FPM, and MariaDB, hand-built from debian:bookworm, wired on a private bridge network reachable only through TLS on port 443(Port 80 is CLOSED, HTTP does not exist here :P). No pre-built images, no latest tags.
through docker0, its basically the default virtual bridge Docker creates on the host. Its role:
- Assigns IPs to each container (172.17.0.x)
- Routes traffic between containers (nginx → wordpress → mariadb all go through it)
- Entry point for incoming traffic from the host via iptables
- Isolates containers from the rest of the host network
We can say in one line that it's the virtual switch that makes containers able to talk to each other and to the outside world, take a look at this:
make up
|
+-- mkdir /root/data/wordpress (host volume dirs)
+-- mkdir /root/data/mariadb
|
+--> mariadb container starts
| mariadb.sh runs:
| 1. start service (setup mode)
| 2. create DB + user + grant
| 3. stop service
| 4. launch mysqld_safe with 0.0.0.0 so other containers can communicate with mariadb (foreground, PID 1)
| healthcheck: mysqladmin ping every 7s
|
+--> wordpress container starts (waits: mariadb healthy)
| wp-config.sh runs:
| 1. download wp-cli
| 2. if no wp-config.php:
| wp core download
| wp core config (points to mariadb:3306)
| wp core install (creates admin + editor users)
| 3. patch php-fpm to listen on 0.0.0.0:9000
| 4. launch php-fpm8.2 -F (foreground, PID 1)
|
+--> nginx container starts (waits: wordpress up)
Dockerfile baked a self-signed cert at build time
nginx.conf: listen 443 ssl, forward *.php -> wordpress:9000
CMD: nginx -g "daemon off;" (foreground, PID 1)
sysadmin-orbit/
├── Makefile <- build/run/clean commands
├── srcs/
│ ├── docker-compose.yml <- networks, volumes, services
│ ├── .env <- secrets (git-ignored)
│ ├── .env.example <- template for .env
│ └── requirements/
│ ├── mariadb/
│ │ ├── Dockerfile <- installs mariadb-server
│ │ └── tools/mariadb.sh <- init DB + run mysqld_safe
│ ├── nginx/
│ │ ├── Dockerfile <- installs nginx + openssl, bakes TLS cert
│ │ └── nginx.conf <- server config (443 ssl, fastcgi)
│ └── wordpress/
│ ├── Dockerfile <- installs php-fpm, php-mysql, curl
│ └── wp-config.sh <- wp-cli install + run php-fpm
Host path Docker volume name Mounted in
/root/data/mariadb --> mariadb (bind) --> mariadb:/var/lib/mysql
/root/data/wordpress -> wordpress (bind) --> wordpress:/var/www/wordpress
--> nginx:/var/www/wordpress (read)
Both volumes use driver: local with o: bind — they are plain host directories
bind-mounted into the containers. Data survives docker compose down but is wiped
by make fclean (which does rm -rf /root/data).
Why the wordpress volume is shared between two containers
HTTPS request for /wp-content/uploads/photo.jpg
│
▼
┌────────────────┐
│ NGINX │
│ container │
└────────┬───────┘
│
reads file directly from
/var/www/wordpress/wp-content/uploads/photo.jpg
│
▼
╔═══════════════════════╗
║ wordpress volume ║
║ (= /root/data/ ║
║ wordpress on ║
║ host) ║
╚═══════════════════════╝
▲
same path, same files
│
┌────────┴───────┐
│ WORDPRESS │
│ container │
│ (php-fpm) │
└────────────────┘
▲
│
HTTPS request for /index.php → FastCGI to wordpress:9000
php-fpm reads the .php file from the same shared volume
and executes it
Two containers, one volume, one path on disk. If WP writes a new upload via PHP-FPM, NGINX sees it instantly because it's literally the same file.
| Variable | Used by | Purpose |
|---|---|---|
MYSQL_DB |
MariaDB, WP | Database name |
MYSQL_USER |
MariaDB, WP | DB user WordPress connects as |
MYSQL_PASSWORD |
MariaDB, WP | Password for that user |
DOMAIN_NAME |
WordPress | Site URL (e.g. https://IP) |
WP_TITLE |
WordPress | Site title |
WP_ADMIN_N/P/E |
WordPress | Admin username / password / email |
WP_USER_NAME/EMAIL/PASS/ROLE |
WordPress | Second (editor) user |
.env is git-ignored. Copy .env.example and fill in real values.
- One service per container, no putting two processes in one image.
- No
latesttags. - No
network: hostor--privileged. - Every service runs as PID 1 in the foreground (no
daemon on). - Port 80 is never opened — HTTP does not exist.
- Secrets live in
.env, never baked into Dockerfiles. - WordPress only starts after MariaDB passes its
mysqladmin pinghealthcheck.
