Skip to content

Instantly share code, notes, and snippets.

@Somrlik
Last active May 29, 2018 22:37
Show Gist options
  • Save Somrlik/eff1b64c6951d135971cd5c65be0a296 to your computer and use it in GitHub Desktop.
Save Somrlik/eff1b64c6951d135971cd5c65be0a296 to your computer and use it in GitHub Desktop.

Setting up a nice php dev on localhost

This guide was written mainly for myself, as not to forget all the little things and not google all the answers again. But of course anyone can use the guide to get a few pointers and ideas to improve their local developement stack. Note that most of the commands here have to be run with elevated permissions, so I omitted writing sudo of # in front of everything.

I am of course not responsible for breaking your system. Make sure you understand what you are doing, common sense etc.

DNS resolving for different .dev/.test domains

When working on multiple projects at once, it is often practical to not have them all on the same domain. Entering localhost/foo or localhost/bar to switch can work, but sometimes integrations with frontend can lead to problems with relative paths. Sometimes you also need to parse domain names and/or paths and as most of your applications will run in the webroot I found it easier to just write foo.dev or bar.dev into the addressbar.

Browsers are also handle autocomplete on different domains better. I grow tired of having to enter credentials/fill forms when I am developing/fixing bugs.

Since google bought .dev TLD (and is currently forcing https in chrome for these dmoains), you will have to use .test instead. IMO using .test and your own DNS server is more stylish but since I suck at networking I wasn't able to properly configure anything to work over both ipv4 and ipv6. But I found that the holy grail was in the system for the whole time.

The rest of this guide is written with .localhost as your TLD and foo.localhost as your domains.

Using systemd-resolve

Newer versions of systemd-resolve automatically resolve domains .localhost to localhost, so you can use whatever.localhost easily.

Try ping whatever.localhost and update systemd-resolve if there is no answer

Using dnsmasq for .test TLD

apt-get install dnsmasq

Append to /etc/dnsmasque.conf echo "listen-address=127.0.0.1" >> /etc/dnsmasque.conf

Create zone file /etc/dnsmasq.d/test echo "address=/test/127.0.0.1" > /etc/dnsmasq.d/test

Restart dnsmasque. service dnsmasq restart

Add 127.0.0.1 to DHCP client as a DNS server echo "prepend domain-name-servers=127.0.0.1" >> /etc/dhcp/dhcient.conf

Restart dhclient service dhclient restart

Test ping foo.test

Apache

apt-get install apache2

Enable mod_rewrite a2enmod rewrite

Enable vhost aliasing a2enmod vhost_alias

Make wildcard vhosts file in /etc/apache2/sites-available/zzz-wildcard.conf.

Edit the file zzz-wildcard.conf in this gist to yor liking.

Setup a fallback error page when you inevitably enter a wrong domain name in the addressbar. echo "<?php echo('Be original with your error message.');" > /var/www/notfound.php

Enable HTTPS (optional)

Eanble ssl module in apache

a2enmod ssl

Generate a key

This guide aims to provide a simple method to get the green bar in most browsers. Simply generating a random key might be sufficient in some cases, but for example chrome will not allow you to run service workers on 'unverified' domains.

As a certificate authority cannot sign certificates for TLDs (e.g. *.localhost) but can sign wildcards for subdomins, our https server will be resolving the virtual hosts on https://*.https.localhost. You could probably use mod_rewrite magic to rewrite your URLs to exclude the https part.

This is an abrigded version of this gist. If you wish to know what the hell you are doing, read it.

  1. Configure openssl - you can copy openssl-ca.conf from this gist and edit it to suit your needs

  2. Make a Certificate Authority openssl req -x509 -config openssl-ca.conf -newkey rsa:4096 -sha256 -nodes -out cacert.pem -outform PEM

  3. Make a config for your server - you can copy openssl-server.conf from this gist and edit it to suit your needs

  • Take note of the alternate_names section, as that is where your domains go
  1. Generate a certificate request for your server openssl req -config openssl-server.conf -newkey rsa:2048 -sha256 -nodes -out servercert.csr -outform PEM

  2. Create a index.txt file for storing the database of certificates touch index.txt

  3. Create a serial file, so you can generate more certificates for your server(s) echo '01' > serial.txt

  4. Sign the certificate request with your certificate authority openssl ca -config openssl-ca.conf -policy signing_policy -extensions signing_req -out servercert.pem -infiles servercert.csr

  5. You will find servercert.pem and serverkey.key in the directory. Copy these files into /etc/apache2/ssl directory to keep them near the server configuration

mkdir /etc/apache2/ssl &&
cp servercert.pem /etc/apache2/ssl/zzz-wildcard.pem &&
cp serverkey.key /etc/apache2/ssl/zzz-wildcard.key
  1. Check zzz-wildcard.conf if your certificate path matches the certificates
  2. Restart apache service apache2 restart

Add your CA to browsers/keychains

As adding a CA into your browser/system is widely different for every piece of sotware, I will not describe every single option here. Just google how to import a certificate in firefox and follow the tutorials. By default the authority you generate is named with ZZZ in the beginning so it's always on the bottom of whatever list you put it in.

The certificate you want to import is the cacert.pem file.

Now your connection to https://test.https.localhost should be displayed as safe and trusted.

MySQL / MariaDB

Considerations

I found myself not needing a dedicated MySQL server that often. IMHO SQlite seems like a better fit for local development, as you can easily just delete the file if you want to start over or move/pass/share the whole databse without the need for dumps and renumbering of ids and whatnot.

Also keep in mind that transferring data between SQlite and MySQL can be... painful.

I would suggest using an ORM layer for database communication as most ORM implementations contain MySQL and SQlite connectors.

Installing

apt-get install mariadb-server
mysql_install_db
mysql_secure_installation

(Optional) Make MySQL root user can access with password, not only from user with elevated permissions

sudo mysql -u root
use mysql;
update user set plugin='' where User='root';
flush privileges;
exit;

PHP

Different versions

If you want a different version, just add 7.0, 7.1 or 7.2 after php.

apt-get install php libapache2-mod-php php-mcrypt php-mysql php-cli php-curl php-sqlite3 php-intl php-mbstring php-xml php-opcache + whatever extensions needed

Enable apache2 mod

a2enmod php

Restart apache

service apache2 restart

Recommended and tips

Symlink /var/www to your ~/Sites for nicer browsing.

ln -s /var/www ~/Sites/

Add yourself to www-data group to access files created by the server

usermod -a -G www-data $USER

Create a /var/www/info/index.php so you can quickly see your phpinfo on info.localhost

mkdir /var/www/info && echo "<?php phpinfo();" > /var/www/info/index.php

Create a php versions swticher script

Automatically set access on created files

You can make all files in /var/www/ accessible as the group www-data. If you don't do this, there can be problems when you're creating the files by yourself. An example can be if you install a package using composer and that package needs an accessible folder for storing cached data. As apache runs as www-data and you created the files as $USER, the default umask forbids the server to write in the directory.

This can be done by chmod|chgrping them every time your scripts fail, running apache as a different user or IMHO better using ACL.

Setup ACL

Enable ACL for your filesystem in /etc/fstab. ACL can be enabled on ext partitions only. Maybe on some others, I have no clue.

[/dev/sda1 | UUID=[whatever]] / ext4 errors=remount-ro,acl 0 1

Remount drive

mount -o remount,acl /

Set ACL for /var/www/

setfacl -d -m group:www-data:rwx /var/www setfacl -m group:www-data:rwx /var/www

You can test this using the test-acl.php script found in this gist. Download an copy to /var/www/test/ and set the owner and group of the directory to whoever

sudo chown root:root -R /var/www/test

Then test it by

curl -g http://test.localhost/test-acl.php

HOME = .
RANDFILE = $ENV::HOME/.rnd
####################################################################
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
base_dir = .
certificate = $base_dir/cacert.pem # The CA certifcate
private_key = $base_dir/cakey.key # The CA private key
new_certs_dir = $base_dir # Location for new certs after signing
database = $base_dir/index.txt # Database index file
serial = $base_dir/serial.txt # The current serial number
unique_subject = no # Set to 'no' to allow creation of
# several certificates with same subject.
default_days = 1825 # how long to certify for
default_crl_days = 30 # how long before next CRL
default_md = sha256 # use public key default MD
preserve = no # keep passed DN ordering
x509_extensions = ca_extensions # The extensions to add to the cert
email_in_dn = no # Don't concat the email in the DN
copy_extensions = copy # Required to copy SANs from CSR to cert
####################################################################
[ req ]
default_bits = 4096
default_keyfile = cakey.key
distinguished_name = ca_distinguished_name
x509_extensions = ca_extensions
string_mask = utf8only
####################################################################
[ ca_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Colorado
localityName = Locality Name (eg, city)
localityName_default = South Park
organizationName = Organization Name (eg, company)
organizationName_default = ZZZ Localhost CA, Limited
organizationalUnitName = Organizational Unit (eg, division)
organizationalUnitName_default = Server Research Department
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = ZZZ Localhost Test CA
emailAddress = Email Address
emailAddress_default = catch.all@https.localhost
####################################################################
[ ca_extensions ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
basicConstraints = critical, CA:true
keyUsage = keyCertSign, cRLSign
####################################################################
[ signing_policy ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ signing_req ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
HOME = .
RANDFILE = $ENV::HOME/.rnd
####################################################################
[ req ]
default_bits = 2048
default_keyfile = serverkey.key
distinguished_name = server_distinguished_name
req_extensions = server_req_extensions
string_mask = utf8only
####################################################################
[ server_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Colorado
localityName = Locality Name (eg, city)
localityName_default = South Park
organizationName = Organization Name (eg, company)
organizationName_default = ZZZ Localhost server
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = ZZZ Localhost server
emailAddress = Email Address
emailAddress_default = catch-all@https.localhost
####################################################################
[ server_req_extensions ]
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = @alternate_names
nsComment = "OpenSSL Generated Certificate"
####################################################################
[ alternate_names ]
DNS.1 = *.https.localhost
DNS.2 = https.localhost
DNS.3 = *.localhost # Added this line just so you can see it doesn't work
IP.1 = 127.0.0.1
IP.2 = ::1
#!/usr/bin/env python
import os;
import sys;
if not os.geteuid() == 0:
sys.exit("This must be ran as root")
# First one is the name of the program
if len(sys.argv) != 2:
sys.exit("This program expects only one parameter")
versions = ["5.6", "7.0", "7.1", "7.2"]
requestedVersion = sys.argv[1]
if requestedVersion not in versions:
sys.exit("Your requested version " + requestedVersion + " is not in list of supported versions.")
print "Requested version: " + requestedVersion
for version in versions:
command = "a2dismod php" + version
print command
os.system(command)
command = "a2enmod php" + requestedVersion
print command
os.system(command)
command = "service apache2 restart"
print command
os.system(command)
<?php
error_reporting(-1);
header('Content-Type', 'text/plain');
$result = file_put_contents('testing.txt', 'The file was written to.');
if ($result === false) {
die('Cannot access a file in current root for writing.');
}
echo file_get_contents('testing.txt');
unlink('testing.txt');
$result = mkdir('testDir');
if ($result === false) {
die('Cannot make a new directory.');
}
$result = file_put_contents('testDir/testing.txt', 'The file in newly created directory was written to');
if ($result === false) {
die('Cannot access a file in created directory for writing.');
}
echo file_get_contents('testDir/testing.txt');
unlink('testDir/testing.txt');
rmdir('testDir');
<Virtualhost *:80>
VirtualDocumentRoot "/var/www/%1"
ServerName vhosts.localhost
ServerAlias *.localhost
UseCanonicalName Off
LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon
ErrorLog "/var/www/vhosts-error_log"
Alias /errors /var/www/
ErrorDocument 404 /errors/notfound.php
<Directory "/var/www/*">
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
Allow from all
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
</Directory>
</Virtualhost>
<Virtualhost *:443>
VirtualDocumentRoot "/var/www/%1"
ServerName vhosts.https.localhost
ServerAlias *.https.localhost
UseCanonicalName Off
LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon
ErrorLog "/var/www/vhosts-error_log"
Alias /errors /var/www/
ErrorDocument 404 /errors/notfound.php
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/zzz-wildcard.pem
SSLCertificateKeyFile /etc/apache2/ssl/zzz-wildcard.key
<Directory "/var/www/*">
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
Allow from all
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
</Directory>
</Virtualhost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment