A while back I decided I wanted my own corner of the internet — not on someone else’s hosting platform, but running on hardware sitting on my desk. This post walks through how I turned a Raspberry Pi 4B into a WordPress server using Docker, the mistakes I made along the way, and what I’d tell someone starting the same project today.

Why a Raspberry Pi?
I already had the Pi running Docker for a Prometheus monitoring project, so when I decided to build a website, using the same device made sense instead of paying for shared hosting or a VPS. It’s low-power, always on, and I get full control over the stack — no restrictions from a hosting provider, and everything runs exactly the way I configure it.
The trade-off is that I’m responsible for everything: updates, backups, security, and keeping the site online. That’s part of the appeal, honestly — it’s as much a learning project as it is a website.
Why Docker?
If you’re new to Docker: a container is a lightweight, isolated environment that packages an application together with everything it needs to run (its own mini-filesystem, dependencies, and configuration) without needing to install any of that directly on your system. Instead of installing WordPress, PHP, Apache, and MySQL/MariaDB directly onto the Pi’s operating system — where they’d tangle with each other and with anything else installed later — each piece runs in its own container. If something breaks, I can tear down and rebuild a single container without touching the rest of the system.
Docker Compose lets you define multiple containers, their settings, and how they connect to each other in one file (docker-compose.yml), then start everything with a single command instead of running a long list of docker run commands by hand.
Since I already had Docker installed on the Pi from an earlier Prometheus monitoring project, adding WordPress this way was the obvious choice.
The Setup
My stack is two containers:
wp_db— a MariaDB 11 container that stores all of WordPress’s data (posts, pages, users, settings)wp_site— the officialwordpress:php8.3-apacheimage, which bundles WordPress with PHP 8.3 and an Apache web server
Here’s the docker-compose.yml that defines both:
services:
db:
image: mariadb:11
container_name: wp_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- wp_net
wordpress:
image: wordpress:php8.3-apache
container_name: wp_site
restart: unless-stopped
depends_on:
- db
ports:
- "80:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME','http://errfun.gotdns.ch:8080');
define('WP_SITEURL','http://errfun.gotdns.ch:8080');
volumes:
- wp_data:/var/www/html
networks:
- wp_net
volumes:
db_data:
wp_data:
networks:
wp_net:
A few things worth pointing out:
.envfile — the actual passwords and database name aren’t in the compose file at all. They live in a separate.envfile in the same folder, and Docker Compose substitutes them in automatically wherever you see${...}. This means I can safely share or back up mydocker-compose.ymlwithout leaking credentials.volumes—db_dataandwp_dataare Docker-managed volumes. They store the database files and WordPress files respectively outside the containers, so if I ever rebuild or update a container, my site’s content and database survive.wp_net— a private Docker network that lets the two containers talk to each other (WordPress reaches the database atdb:3306) without exposing the database to the outside world at all.WORDPRESS_CONFIG_EXTRA— this injects extra lines directly into WordPress’swp-config.phpon startup. I used it to hardcodeWP_HOMEandWP_SITEURLto my domain, which avoids some issues WordPress can have figuring out its own URL behind port forwarding.ports: "80:80"— the WordPress container listens on port 80 inside the Pi. That detail matters for the next section.

Getting a Domain: Dynamic DNS with No-IP
Home internet connections almost always have a dynamic IP — your ISP can change your public IP address at any time, which would break a domain pointing to it. To solve this, I use No-IP, and specifically their noip-duc (Dynamic Update Client), running as another Docker container on the Pi. It periodically checks my public IP and updates the DNS record for errfun.gotdns.ch whenever it changes, so the domain always points to my current home IP.

Port Forwarding
The WordPress container is only listening on port 80 inside the Pi’s local network. To make the site reachable from the internet, I had to configure my modem/router to forward incoming traffic to the Pi:
- External port 8080 (on the modem) → internal port 80 (on the Raspberry Pi’s local IP)
That’s why the site is accessed at errfun.gotdns.ch:8080 instead of just errfun.gotdns.ch — port 80 is the default for HTTP but I forwarded 8080 externally, so visitors need to include it explicitly in the URL.

The Problem: One Character, One Broken Site
The first time I tried loading the site, nothing worked — WordPress couldn’t connect to the database at all. After going back through the compose file line by line, I found the issue: in my .env file, I had misspelled the password variable — I’d written ASSWORD instead of PASSWORD. Since the compose file was looking for ${MYSQL_PASSWORD} but the .env file didn’t define a matching variable, it resolved to an empty value, so MariaDB and WordPress were never given a real password to authenticate with.
It’s a good reminder of how unforgiving .env files are — there’s no autocomplete, no validation, nothing tells you a variable name is wrong. It just silently fails. Now I double-check variable names match exactly between docker-compose.yml and .env before running anything.
Final Result
With the containers running, DNS pointing at my home IP, and the port forward in place, the site is live at errfun.gotdns.ch:8080 — running entirely on a Raspberry Pi 4B sitting on my desk.

What’s Next
This setup works, but it’s not done. A few things I want to tackle in follow-up posts:
- Setting up HTTPS (currently the site is plain HTTP, which browsers increasingly flag)
- Automating backups of the
db_dataandwp_datavolumes - Maybe moving to a reverse proxy so I can host more than one service on the Pi without juggling ports
If you’re thinking about doing something similar, my honest takeaway is: it’s not hard once Docker is set up, but read your .env file twice before you docker compose up.