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.

How to run local WordPress site for development on Fedora Silverblue 42
Podman has been busy.

You may be surprised to hear that Fedora Silverblue already includes the two things we need to run any website or web application locally:

  1. Dnsmasq: DNS server to configure catch-all rule for .test.
  2. Podman: Open source container engine to run:
    1. MariaDB: MySQL-compatible database server.
    2. WordPress: Web server with Apache, PHP, and WordPress.
    3. Caddy: Reverse proxy to route wordpress.test to site container.

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.

đź’ˇ
If you leave the wordpress directory empty, it will be populated with the latest version of WordPress when we create our WordPress container.

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      bridge

To 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 devnet

The 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 mariadb

We can confirm the volume was created with podman volume list:

# podman volume list

DRIVER      VOLUME NAME
local       mariadb

If you ever want to peek at the files in a volume, you can find them here:

cd ~/.local/share/containers/storage/volumes

Create a MariaDB container

Now that we have a volume for MariaDB, let's create and run the container:

đź’ˇ
If you restart Fedora, the containers will not automatically start. I like this, because there's no reason for them to run unless I'm developing. If you want them to automatically start, add --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.11
đź’ˇ
The slashes at the end of each line simply allow you to copy and paste this multi-line command into terminal; they're not part of the command itself.

I'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:

  • --detach Without 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.
  • --name Just 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.
  • --network This assigns the container to the devnet network we created earlier, which means it will get an IP address from our 10.111 subnet.
  • --env Allows 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.
  • --publish Maps 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.
  • --volume Is 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.11 This 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
đź’ˇ
Run 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-apache
đź’ˇ
Feel free to update the database name, user, and password to whatever you like, especially if you're setting up a specific project.

Most 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 -pfoobar
  • exec Allows us to execute a command inside a container.
  • --interactive Keeps us connected to the container after running a command.
  • --tty Makes it feel like we're directly connected to the container, which means we get to see colors, can do proper line editing, etc.
  • mariadb Is the name of the container we want to run the command on.
  • mysql... Lets us into the database. The -p is for password, and the password should follow directly after the -p (without a space unlike -u root). You can also omit -p to 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.

đź’ˇ
When visiting wordpress.test for the first time, your browser will likely complain due to the self-signed certificate, which is fine for local development. Click on Advanced in that warning and simply choose to accept the risk.

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:latest

We'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.conf

With the following configuration:

[Resolve]
DNS=127.0.0.1
Domains=~test

If you've never used vi, here's how it works:

  1. Press i to switch to insert mode.
  2. Press ctrl+shift+v to paste contents.
  3. Press esc to exit insert mode.
  4. 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-resolved

Configure 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.conf
đź’ˇ
Configuration files are loaded in the order they appear, so adding 99 to the name ensures it's loaded toward the end.

With the following contents:

address=/.test/127.0.0.1
server=127.0.0.53
đź’ˇ
Reminder for vi: Press i → ctrl+shift+v → esc → :wq

This 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 dnsmasq

Lower 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.conf

With the following configuration:

net.ipv4.ip_unprivileged_port_start=80

And then tell Fedora to apply it:

sudo sysctl --system

Troubleshooting

If something isn't working, here's how you might go about troubleshooting any issues.

Check status of all containers:

podman container list --all

Check the status column and ensure the container is up.

Start all containers if they're not running:

podman container start --all

Start 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.test

If 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-resolved

Check if Caddy is responding:

curl --show-headers http://wordpress.test
# Output

HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://wordpress.test/
Server: Caddy

Check 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.13

Conclusion

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.