← Back to posts

≈ 4 min

Zero-config local PHP/WordPress development environment on Arch Linux (without Docker)

Over the years of working with WordPress and PHP projects I accumulated a large number of local sites. Creating nginx vhosts, generating SSL certificates, editing /etc/hosts, and preparing project directories manually quickly becomes repetitive and annoying.

So I built a simple idea:

one command → fully ready local project

No Docker. No compose. No extra layers.

Just native Linux tooling.


Core idea

The architecture is based on a few simple principles:

  • one wildcard SSL certificate
  • one nginx vhost
  • dynamic document root via $host
  • automatic project bootstrap
  • automatic hosts management

Result:

mkcert-wildcard site.test

Creates:

~/www/site.test/public
/etc/hosts entry
SSL ready
nginx ready
php ready

Site works instantly.


nginx structure

My nginx layout:

/etc/nginx/
├── sites-available
│   ├── default.conf
│   ├── pma.test.conf
│   └── wildcard.test.conf
├── sites-enabled
├── ssl
│   ├── wildcard.test.pem
│   └── wildcard.test-key.pem

Classic layout:

sites-available → configs
sites-enabled → symlinks

Simple and predictable.


nginx main config

Minimal nginx.conf:

server {
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl default_server;
    http2 on;
    server_name _;
    root /home/sergey/www/$host/public;
    index index.php index.html;
    ssl_certificate     /etc/nginx/ssl/wildcard.test.pem;
    ssl_certificate_key /etc/nginx/ssl/wildcard.test-key.pem;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/sergey.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(jpg|jpeg|png|gif|css|js|svg|woff|woff2|webp)$ {
        expires max;
        log_not_found off;
    }
}

Nothing fancy. Just clean nginx.


The key part — wildcard vhost

wildcard.test.conf:

server {
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl default_server;
    http2 on;
    server_name _;
    root /home/sergey/www/$host/public;
    index index.php index.html;
    ssl_certificate     /etc/nginx/ssl/wildcard.test.pem;
    ssl_certificate_key /etc/nginx/ssl/wildcard.test-key.pem;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php-fpm/sergey.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(jpg|jpeg|png|gif|css|js|svg|woff|woff2|webp)$ {
        expires max;
        log_not_found off;
    }
}

The important idea is:

root /home/sergey/www/$host/public;

nginx dynamically resolves project directory based on domain.

No more vhosts needed.


PHP-FPM setup

I use a dedicated pool:

/etc/php/php-fpm.d/sergey.conf

With a unix socket:

/run/php-fpm/sergey.sock

Unix sockets are faster and cleaner for local environments.


SSL with mkcert

One certificate for all local domains:

mkcert localhost \
127.0.0.1 ::1 \
wordpress.test \
project.test \
client.test

Used across all projects.

No per-site SSL setup required.


Automation via fish

The real productivity gain comes from automation:

function mkcert-wildcard
    set ssl_dir /etc/nginx/ssl
    set cert_name wildcard.test.pem
    set key_name wildcard.test-key.pem
    mkcert $argv
    set cert (ls -t *.pem | grep -v key | head -n 1)
    set key  (ls -t *-key.pem | head -n 1)
    sudo cp $cert $ssl_dir/$cert_name
    sudo cp $key  $ssl_dir/$key_name
    sudo chmod 644 $ssl_dir/$cert_name
    sudo chmod 600 $ssl_dir/$key_name
    rm -f $cert $key
    for domain in $argv
        if string match -rq '^(127\.|::1|localhost$)' $domain
            continue
        end
        if not grep -qw "$domain" /etc/hosts
            echo "127.0.0.1 $domain" | sudo tee -a /etc/hosts > /dev/null
        end
        set project_dir ~/www/$domain
        if not test -d $project_dir
            mkdir -p $project_dir/public
            touch $project_dir/public/index.php
        end
    end
    sudo systemctl restart nginx
    sudo systemctl restart php-fpm
end

Now creating a new site is trivial:

mkcert-wildcard newproject.test

Project ready in seconds.


Why not Docker?

Docker makes sense when you need:

  • multiple PHP versions
  • multiple DB versions
  • exact production replication
  • team onboarding environments

But for daily PHP/WordPress development:

native stack often wins:

  • faster
  • simpler
  • easier debugging
  • fewer moving parts
  • no container overhead

Results

This setup provides:

  • zero config projects
  • zero vhosts
  • zero SSL setup
  • instant project bootstrap
  • production-like environment

And most importantly:

one command → working site


Final thoughts

If you maintain many local projects, this approach saves a significant amount of time and removes friction from daily development.

Sometimes the simplest native solution is still the most productive one.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *