How to run local WordPress site for development on Fedora Silverblue 42
With Dnsmasq to create .test domains, and Podman to build containers for our stack, we can run almost any website or web application locally.
You may be surprised to hear that Fedora Silverblue already includes the two things we need to run any website or web application locally:
- Dnsmasq: DNS server to configure catch-all rule for .test.
- Podman: Open source container engine to run:
We’ll configure each component together, step by step, so you understand how it works and why, but most importantly, feel comfortable making changes and adding containers to support your future projects.
Create project directories
We need a place for projects and configuration files to live. I like to create a Projects folder in my home directory with subfolders for individual projects.
Create a Projects folder with a caddy and wordpress subfolder:
mkdir --parents ~/Projects/{caddy,wordpress}caddy will contain its Caddyfile configuration file and a data folder for local SSL certificates (among other things), and wordpress will contain the entire WordPress project.
I've named the WordPress project generically wordpress, because I plan to use it as a playground / test environment. If you're setting up a specific site, I recommend naming it something more meaningful.
Create a Podman network
The three containers (MariaDB, WordPress, and Caddy) we're creating need to talk to each other. For instance, caddy needs to see wordpress, and wordpress needs to see mariadb. We could get more granular with networking, but let's keep it simple for local development.
Podman has a default network called podman, which all containers automatically join when you don’t specify a network.
Run podman network list in terminal to see it:
# podman network list
NETWORK ID NAME DRIVER
2f259bab93aa podman bridgeTo see even more details about this network, such as which subnet it operates on or when it was created, you can inspect it with podman inspect podman:
# podman inspect podman
[
{
"name": "podman",
"id": "2f259bab93aaaaa2542b...",
"driver": "bridge",
"network_interface": "podman0",
"created": "2025-10-05T23:39:24.545648589-04:00",
"subnets": [
{
"subnet": "10.88.0.0/16",
"gateway": "10.88.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
},
"containers": {}
}
]While we could use this default network, I like to put my development containers into their own network. That way, if I spin up unrelated containers in the future (using the default network), there is no chance they will interfere with my development stack.
We saw in the network details up above that podman operates on the 10.88 subnet, so we just need to pick something other than that. Let's go with 10.111:
podman network create --subnet 10.111.0.0/24 devnetThe last part of that command, devnet, is the name of the network, which is how we'll refer to it when we build our containers. You can name it whatever you like as long as you're consistent later on.
Create a MariaDB volume
Containers should be replaceable, but that means any data a container creates would be lost when that container is deleted. The answer to this are volumes, which allows us to mount a directory inside a container to a directory on our file system, persisting anything stored beyond the life of the container.
Create a volume called mariadb for MariaDB to store its databases:
podman volume create mariadbWe can confirm the volume was created with podman volume list:
# podman volume list
DRIVER VOLUME NAME
local mariadbIf you ever want to peek at the files in a volume, you can find them here:
cd ~/.local/share/containers/storage/volumesCreate a MariaDB container
Now that we have a volume for MariaDB, let's create and run the container:
--restart=always to all podman run commands moving forward.podman run \
--detach \
--name mariadb \
--network devnet \
--env MYSQL_ROOT_PASSWORD=foobar \
--publish 3306:3306 \
--volume mariadb:/var/lib/mysql \
docker.io/library/mariadb:10.11I'm big fan of long-form options when writing guides (--detach vs. -d), because they often provide a clue as to what they're doing, but let's break everything down:
--detachWithout this, we'd have to keep the terminal window open for as long as we want the container to run. With this, it will continue to run in the background and allow us to run other commands in that same window.--nameJust like we named our network (devnet) and volume (mariadb), we can give this container a name so we can refer to it by name instead of IP address.--networkThis assigns the container to the devnet network we created earlier, which means it will get an IP address from our 10.111 subnet.--envAllows us to set environment variables. Every container image has instructions on what environment variables it supports. For mariadb, we can set a root password to connect to it.--publishMaps a port on our host (Fedora) to a port in our container. In this case, connecting to 3306 on Fedora means connecting to 3306 on our MariaDB container.--volumeIs a storage link between our host and our container. In other words, everything the container stores in /var/lib/mysql will be visible to us in our mariadb volume.docker.io/library/mariadb:10.11This is the image our container will be built from. Breaking this down, docker.io/library is where the image is stored, mariadb is the image's name, and 10.11 is the version we want.
Once we run this command, our container will be launched. We can verify this with podman container list, which shows all running containers:
# podman container list
CONTAINER ID
a30bed17a9b7
IMAGE
docker.io/library/mariadb:10.11
COMMAND
mariadbd
CREATED
2 days ago
STATUS
Up 1 second
PORTS
0.0.0.0:3306->3306/tcp
NAMES
mariadb
podman container list --all to see both running and stopped containers.Create a WordPress container
With your database ready to go, we can now create our WordPress container:
podman run \
--detach \
--name wordpress \
--network devnet \
--env WORDPRESS_DB_HOST=mariadb:3306 \
--env WORDPRESS_DB_USER=wordpress \
--env WORDPRESS_DB_PASSWORD=foobar \
--env WORDPRESS_DB_NAME=wordpress \
--volume $HOME/Projects/wordpress:/var/www/html:Z \
docker.io/library/wordpress:php8.4-apacheMost of these options will look familiar now, but note how we reference the database host with mariadb:3306. That's because we named our database container mariadb and we know it listens on port 3306.
For the volume, we map ~/Projects/wordpress to /var/www/html within the container, which is essentially the WordPress server's web root. If you created a different project folder earlier, be sure to update the reference in the command.
The little :Z at the end of the volume line is not a typo, it's a SELinux (security-enhanced Linux) option, which tells SELinux that the container is allowed to access that particular directory on our host machine.
Create a MariaDB database for WordPress
Our WordPress site is running now, but we still have to connect to MariaDB and create a database for this project:
podman exec --interactive --tty mariadb mysql -u root -pfoobarexecAllows us to execute a command inside a container.--interactiveKeeps us connected to the container after running a command.--ttyMakes it feel like we're directly connected to the container, which means we get to see colors, can do proper line editing, etc.mariadbIs the name of the container we want to run the command on.mysql...Lets us into the database. The-pis for password, and the password should follow directly after the-p(without a space unlike-u root). You can also omit-pto be prompted for a password.
We should now see the MySQL prompt, which means we can create a database and user with the following MySQL statements:
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress'@'%' IDENTIFIED BY 'foobar';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'%';
FLUSH PRIVILEGES;
EXIT;CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Creates a database called wordpress.CREATE USER 'wordpress'@'%' IDENTIFIED BY 'foobar';Creates a user called wordpress with a password of foobar.GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'%';The first wordpress reference is the database, and the second wordpress reference is the user.FLUSH PRIVILEGES;Ensures the privileges we just granted immediately take effect.EXIT;Exits the database.
Be sure to update any of the bold parts if you changed them during the WordPress container creation.
Create a Caddy configuration file
Next, we're going to create a Caddyfile (no file extension) in ~/Projects/caddy with the following contents:
wordpress.test {
reverse_proxy wordpress:80
tls internal
}When we visit wordpress.test, it will pass the request to our wordpress container on port 80, which Apache listens on. With tls internal, we specify that we'd like an internally-generated SSL certificate vs. using Let's Encrypt, which is suitable for external domains.
If you launch another project in the future, just add another directive:
wordpress.test {
reverse_proxy wordpress:80
tls internal
}
laravel.test {
reverse_proxy laravel:80
tls internal
}Assuming the domain is laravel.test and the container is called laravel.
Create a Caddy container
With the Caddyfile created, we can proceed with creating the Caddy container:
podman run \
--detach \
--name caddy \
--network devnet \
--publish 80:80 \
--publish 443:443 \
--volume $HOME/Projects/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z \
--volume $HOME/Projects/caddy/data:/data/caddy:Z \
docker.io/library/caddy:latestWe're using a volume to give the Caddy container read-only (ro) access to the Caddyfile, and another volume to save its SSL certificates.
We also expose port 80 (http) and 443 (https), so whenever we reference those ports on our Fedora host, they map to the ports of our Caddy container.
Configure systemd‑resolved for .test
Whenever you make a request to a domain, it's handled by Fedora's systemd‑resolved, which passes the request to your DNS server, but in our case, we need to intercept requests ending in .test to route them locally. We can do this by adding a configuration file.
Create the resolved.conf.d directory:
sudo mkdir /etc/systemd/resolved.conf.d
Then the dnsmasq-test.conf file:
sudo vi /etc/systemd/resolved.conf.d/dnsmasq-test.confWith the following configuration:
[Resolve]
DNS=127.0.0.1
Domains=~testIf you've never used vi, here's how it works:
- Press i to switch to insert mode.
- Press ctrl+shift+v to paste contents.
- Press esc to exit insert mode.
- Press :wq to write contents to file and quit vi.
The configuration above tells systemd‑resolved to pass requests ending in test to 127.0.0.1 (dnsmasq), which means anything other than test will resolve normally.
Restart systemd-resolved for the configuration to take effect:
sudo systemctl restart systemd-resolvedConfigure dnsmasq for .test
Now that requests for test are being passed from systemd‑resolved to dnsmasq, we need to tell dnsmasq how to respond.
Create the 99-test-wildcard.conf configuration file:
sudo vi /etc/dnsmasq.d/99-test-wildcard.confWith the following contents:
address=/.test/127.0.0.1
server=127.0.0.53This configuration file tells dnsmasq to respond with 127.0.0.1 for any request ending in .test. Anything else is sent back to systemd‑resolved to be routed normally.
While dnsmasq is installed, it's not running by default, so we're going to run it --now, but we'll also enable it to automatically start when Fedora restarts:
sudo systemctl enable --now dnsmasqLower unprivileged port starting range
In order for us to visit wordpress.test without ports in the address bar, we need Caddy to listen on port 80 and 443, but Fedora Silverblue, being a security-conscious steward, has a policy that any port below 1024 is privileged and can therefore not be used.
I tried to work within those constraints by configuring Caddy to listen on 8080 and 8443, and then tried to use a firewall rule to forward 80 and 443 to 8080 and 8443 respectively, but it never quite worked.
The solution is to set Fedora's unprivileged port to start at 80 instead of 1024, which will allow Caddy to listen on both 80 and 443.
We do this by creating the 60-caddy.conf configuration file:
sudo vi /etc/sysctl.d/60-caddy.confWith the following configuration:
net.ipv4.ip_unprivileged_port_start=80And then tell Fedora to apply it:
sudo sysctl --systemTroubleshooting
If something isn't working, here's how you might go about troubleshooting any issues.
Check status of all containers:
podman container list --allCheck the status column and ensure the container is up.
Start all containers if they're not running:
podman container start --allStart specific container that's not running:
podman container start <name>Restart running container:
podman container restart <name>Check container logs:
podman container logs <name>Stop and remove specific container:
podman container stop <name>
podman container rm <name>You can also add --all to stop and remove them all. Scroll up to the relevant section to find that container's podman run command to rebuild.
Check if wordpress.test resolves to 127.0.0.1:
dig +short wordpress.testIf it doesn't, check both the systemd‑resolved and dnsmasq configuration file. Make sure they're in the right place, with the right filename, and if you do make changes, restart both services (see below).
Restart systemd-resolve and dnsmasq:
sudo systemctl restart dnsmasq
sudo systemctl restart systemd-resolvedCheck if Caddy is responding:
curl --show-headers http://wordpress.test# Output
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://wordpress.test/
Server: CaddyCheck if Apache/WordPress is responding:
curl --insecure --show-headers https://wordpress.test # Output
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/html; charset=UTF-8
link: <https://wordpress.test/wp-json/>; rel="https://api.w.org/"
server: Apache/2.4.65 (Debian)
via: 1.1 Caddy
x-powered-by: PHP/8.4.13Conclusion
Hopefully you were able to follow along and got WordPress running, and gained enough knowledge in the process to launch more projects locally in the future. If you have any questions or comments, feel free to leave them below.
Featured image by CHUTTERSNAP.