Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active December 15, 2023 07:37
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mindplay-dk/7b09aeebfb82793d2d8b170123588d11 to your computer and use it in GitHub Desktop.
Save mindplay-dk/7b09aeebfb82793d2d8b170123588d11 to your computer and use it in GitHub Desktop.
Ubuntu on Windows

Introduction

⚠️ I am no longer actively maintaining this. ⚠️

With the Windows 10 Creators Update comes an awesome new opportunity to run a lighweight Ubuntu Linux environment as a subsystem of Windows. This is officially known as Windows Subsystem for Linux (WSL) or Bash on Windows.

This is great news for PHP developers using Windows - we can now get access to native Linux builds of PHP, PECL extensions, ImageMagick, NGINX and other server components and tools, without having to migrate to a Linux desktop environment!

In this guide, we'll get you set up with WSL itself, a working PHP 8.2 environment with OpCache, XDebug and task/terminal integration with Php Storm, and working NGINX configuration - as well as various tools, including PEAR/PECL, Composer and Node.JS.

Before you begin, skim over the official FAQ, which might answer some of your initial questions: what exactly is Ubuntu on Windows, how does it work, how do the Linux and Windows file-systems map to each other, and so on. Skip this basic orientation step at your own peril.

For a more general guide to using WSL and maintaining the Ubuntu installation, try The WSL Guide.

Note that WSL technology is unstable (and still changing rapidly) at the time of writing. You should be prepared for some learning curve, a few surprises, and some bumps along the road!

Install Ubuntu on Windows

Before you proceed with the installation of Ubuntu on Windows, first upgrade your copy of Windows to the latest version:

https://www.microsoft.com/en-us/software-download/windows10

If you have a previous, older installation of Ubuntu on Windows, or one that you've already tampered with while trying to install PHP/NGINX, I recommend you start with a full uninstall, as described in this article.

If you have a broken installation of Ubuntu on Windows that won't start, I recommend you start by removing the feature via the "Turn Windows features on or off" dialog, reboot, add the feature again, and reboot again.

If it still won't start, try the PowerShell approach (as prescribed in the installation guide) and make sure you're launching the x64 version of PowerShell on 64-bit Windows, using "Run as Administrator", or you will receive an error-message incorrectly explaining that the LxRun command doesn't exist.

Follow the installation guide here:

https://msdn.microsoft.com/en-us/commandline/wsl/install_guide

Configure WSL

Your WSL installation can be configured via the /etc/wsl.conf file - you can learn more about in this article.

If your WSL installation was upgraded from an earlier version of Windows 10, your system may not have the /etc/wsl.conf file yet, and you should create it first - run e.g. sudo nano /etc/wsl.conf and enter the following minimal configuration:

[automount]
options = "metadata"

Recent improvements to file-system integration made this necessary - you can read more about it in this article.

Upgrades

Get available OS updates:

sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get upgrade

Install PHP and NGINX

Before you proceed with the installation, if you have a existing/older installations of Apache and/or PHP, you may wish to (optionally) scrap those packages first:

sudo apt-get remove apache
sudo apt-get remove ^php.*
sudo apt-get autoremove

First, add the main package repository for PHP to APT:

sudo add-apt-repository ppa:ondrej/php
sudo apt-get update

Install PHP 8.2 and various PHP modules:

sudo apt-get install php8.2 php8.2-fpm php8.2-cli php8.2-cgi php8.2-curl php8.2-gd php8.2-intl php8.2-opcache php8.2-mbstring php8.2-xml php8.2-zip php8.2-mysql php8.2-pgsql
php --version

Note that, if you install multiple versions of PHP, you can switch between them using this command:

sudo update-alternatives --config php

Switching between different versions of FPM services is a matter of starting/stopping the right service, e.g. sudo service php8.2-fpm start.

Install NGINX

To install NGINX:

sudo apt-get install nginx

There's a longer section on configuring NGINX below, but you'll most likely want to go through the PHP configuration first.

Install Caddy v2

To install the stand-alone binary (with no service) pick a .tar.gz release and download it.

For example:

curl -Ls https://github.com/caddyserver/caddy/releases/download/v2.3.0/caddy_2.3.0_linux_amd64.tar.gz | tar xvzf - caddy
sudo mv caddy /usr/local/bin/

Refer to the Caddy Manual for service installation instructions.

A basic Caddyfile with PHP support looks like this:

http://domain.test {
  root webroot
  errors stdout # if you want PHP errors visible in console
  log stdout    # if you want HTTP requests visible in console
  php_fastcgi 127.0.0.1:9000
  file_server
}

This will serve static assets from a webroot folder and route all incoming requests through an index.php file.

Install Composer

Go to home dir with cd ~, then follow command-line installation instructions for Composer, then move it into place:

sudo mv composer.phar /usr/bin/composer
composer --version

To avoid permission issues with some packages, also install unzip:

sudo apt-get install unzip

To enable running tools you install via composer global, add the global bin folder to your search path - use e.g. nano ~/.profile to edit your login script, and add this line:

export PATH="$HOME/.composer/vendor/bin:$PATH"

Configure PHP

To edit php.ini as used by php under bash:

sudo nano /etc/php/8.2/cli/php.ini

To edit php.ini as used by NGINX:

sudo nano /etc/php/8.2/fpm/php.ini

Rather than editing those directly, I prefer to create a single shared configuration file with my personal settings for the cli/fpm versions of PHP - I name it 99-user.ini to make sure that it loads last, so I can override everything:

sudo nano /etc/php/99-user.ini

Then symlink that configuration file so that it applies to both CGI, CLI and FPM:

sudo ln -s /etc/php/99-user.ini /etc/php/8.2/cgi/conf.d/
sudo ln -s /etc/php/99-user.ini /etc/php/8.2/cli/conf.d/
sudo ln -s /etc/php/99-user.ini /etc/php/8.2/fpm/conf.d/

You will most likely want to set at least error_reporting = E_ALL and display_errors = On.

Here are the development settings I use on my own installation:

;;; extensions:

;UNCOMMENT THESE AND/OR ADD MORE AFTER INSTALLING (SEE BELOW)
;zend_extension = xdebug.so
;extension = imagick.so

opcache_enable = true

;;; development settings:

error_reporting = E_ALL
display_errors = true
display_startup_errors = true

assert.active = true

xdebug.var_display_max_depth = 100
xdebug.mode = develop

;;; resource limits:

post_max_size = 1024M
upload_max_filesize = 2024M
max_execution_time = 1800

You can use php --ini to verify that your custom INI file is being loaded.

To restart PHP after making changes:

sudo service php8.2-fpm restart

Install PEAR, PECL and Xdebug

PECL depends on PEAR for package management, so first install PEAR:

sudo apt-get install php-pear
pear version

PECL modules (such as xdebug) are built at installation time, so we'll need the PHP sources and development tools - install those:

sudo apt-get install php8.2-dev
phpize --version
pecl help version

It's always a good idea to update the official PECL-channel for PEAR straight away, to make sure you're getting the latest packages:

sudo pecl channel-update pecl.php.net

If all that works out, you should now have access to the full PECL library of PHP extensions!

If you've never installed a PECL extension before, the following section will walk you through the installation of the Xdebug and ImageMagick PECL extension next.

Install Xdebug

Install Xdebug via PECL:

sudo pecl install xdebug

To enable the extension, add this line to your 99-user.ini file:

zend_extension = xdebug.so

Install ImageMagick + imagick for PHP

First, install ImageMagick itself:

sudo apt-get install imagemagick
convert --version

The imagick PECL package is going to get built from sources, and it depends on libmagickwand or something, I don't know. Apparently, we'll need the full stack of tools for building that as well:

sudo apt-get install libmagickwand-dev

You should now be able to install the imagick extension:

sudo pecl install imagick

Unless your screen is full of error messages, the compiled binary should now be in place, but PECL extensions apparrently don't get up and walk around on their own, so you'll have to add the extension=imagick.so directive to your php.ini files manually - edit each of your /etc/php/8.2/{cli|cgi|fpm}/php.ini files (or your 99-user.ini file as described earlier) and add the directive.

Try e.g. php -m | grep 'imagick' to see if the extension loads under CLI, and phpinfo() from a test-script to see if it's loading under php-fpm.

Configure FPM

It appears there's an issue with the socket-based default daemon configuration, so we need to switch the daemon to TCP mode.

TODO: is this issue resolved? (even if it is, we probably still want to use a TCP port, so a Windows-based debugger can connect from PHP Storm?)

Create your FPM daemon configuration file for overrides:

sudo nano /etc/php/8.2/fpm/pool.d/x-www.conf

Add the listen directive:

[www]
listen = 127.0.0.1:9000

Then restart FPM:

sudo service php8.2-fpm restart

Configure NGINX

Your NGINX configuration files are located in /etc/nginx/sites-enabled.

To set up a virtual host, create files like e.g. /etc/nginx/sites-enabled/test.conf - here's a sample file:

server {
    listen       80;
    server_name  test;
    root         /mnt/c/workspace/test;
    index        index.php;

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

    location ~ \.php$ {
        try_files $uri /index.php =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

The important thing here is the fastcgi_pass 127.0.0.1:9000 directive, which forwards CGI requests to php-fpm on the TCP port number we configured in the section above.

Adjust the listen, server_name and root settings as needed. (Note that opening local domain names can be a bit tricky in Chrome)

Note that this example uses a mounted Windows root folder, which doesn't allow changing folder/file permissions from Linux - see the Windows File-system Integration section below for more information.

To restart NGINX after making changes:

sudo service nginx restart

Managing PHP and NGINX Services

To start the services:

sudo service php8.2-fpm start
sudo service nginx start

To check if they're running:

sudo service php8.2-fpm status
sudo service nginx status

Troubleshooting

If the NGINX service won't start, check the log-file for errors:

tail /var/log/nginx/error.log

If PHP won't budge, check the log-file for php-fpm for errors:

sudo tail /var/log/php8.2-fpm.log

Install Node.JS

To install Node.JS, first pick the distribution you want and copy the URL.

For example, to download and unpack version 14.4.0:

sudo mkdir -p /usr/local/lib/nodejs
curl -sL https://nodejs.org/dist/v14.4.0/node-v14.4.0-linux-x64.tar.xz | sudo tar -xJvC /usr/local/lib/nodejs

Add the path to your ~./profile:

export PATH=/usr/local/lib/nodejs/node-v14.4.0-linux-x64/bin:$PATH
. ~/.profile

Test your installation:

node --version
npm --version

To avoid permission issues, change npm's default directory.

Some npm packages (such as node-sass) depend on native add-on modules to node - to build these dependencies during installations, you need node-gyp, and it's a good idea to install this globally:

sudo npm install -g node-gyp

Note that after certain major system changes, it's sometimes necessary to reinstall this.

To avoid port restrictions, for example, if a node process needs to listen on port 80 or 443:

sudo sysctl -w net.ipv4.ip_unprivileged_port_start=0

Or, to make this setting permanent:

echo 'net.ipv4.ip_unprivileged_port_start=0' | sudo tee /etc/sysctl.d/50-unprivileged-ports.conf

⚠️ Note that launching node processes with sudo is a really bad idea, since this gives unrestricted access to any npm package to do literally anything it wants on your system.

Install PostgreSQL + pgAdmin

To install PostgreSQL 12 and pgAdmin 4:

sudo apt-get install curl ca-certificates gnupg
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
sudo apt-get update
sudo apt-get install postgresql-12 pgadmin4

A new PostgreSQL installation doesn't have any roles (user accounts) yet - create your first role with this command:

sudo -u postgres createuser --interactive --pwprompt

You will be prompted for a username and password.

Windows File-system Integration

Two-way mappings between the Linux and Windows file-systems are available, with some major caveats - please read carefully before attempting to create or modify any files on either!

Accessing the Linux File-system from Windows

WARNING: do NOT attempt to manage the contents of the Lxss folder from Windows!

If, for example, you need to move a project from an existing workspace on your Windows drive, do so with e.g. mv /mnt/c/workspace/my_project ~/workspace rather than e.g. dragging and dropping from Windows.

For purposes such as backing up files only, the Ubuntu root-folder is accessible from Windows under:

%LOCALAPPDATA%\Lxss

Your home folder is accessible (via an explorer window) from Windows under:

%LOCALAPPDATA%\Lxss\home\{username}

Accessing the Windows File-system from Linux

Your Windows drives (C:\, etc.) are accessible from Linux under e.g.:

/mnt/c

WARNING: a mounted Windows folder does not support changing folder/file permissions (with chmod or any program/script that attempts to do so) and this is by design.

If your programs/scripts/tools rely on permissions for anything, do not use a mounted Windows file-system; instead, create a workspace inside the Linux file-system and access it from Windows via SSH or SFTP, etc.

Configure SSH on Ubuntu

Ubuntu and Windows subsystems share the same IP address and port-numbers - it's not like an emulator or Docker etc., your Windows OS is actually running Bash and Linux binaries on a Linux kernel running side-by-side with the Windows kernel.

Note that the SSH daemon appears to start and stop with the bash terminal. That means you need to open the bash console and leave it open while you're using the SSH service.

For some reason, Ubuntu on Windows ships by default with no host-keys in place. The easiest way to fix this, is to just reinstall OpenSSH:

sudo apt-get purge openssh-server
sudo apt-get install openssh-server

Now edit the SSH configuration file:

sudo nano /etc/ssh/sshd_config

The SSH Broker and Proxy services (which are built into Windows and enabled by default) are most likely occupying port 22, so first change change the Port directive to e.g. 2200. (Try netstat -A to get a list of port-numbers currently in use to help select an available port.)

Now restart the SSH daemon:

sudo service ssh --full-restart

Next, create an .ssh folder in your home folder and generate a personal key-pair:

cd ~
mkdir .ssh
chmod 700 .ssh
ssh-keygen -f .ssh/id_rsa

Then import the new key into the SSH daemon's authorized keys:

cat .ssh/id_rsa.pub >> .ssh/authorized_keys
chmod 600 .ssh/authorized_keys

You should now be able to SSH from your local system into Ubuntu - to retrieve your newly generated private key (id_rsa) from the .ssh folder, see the Windows File-system Integration section above.

Configure PHP Storm

Integration with PHP Storm is via SSH, so bash needs to be open while PHP Storm is running. Note that the SSH daemon starts and stops with bash, so you need to leave the bash window open.

Open the Settings dialog via the File menu.

To integrate the console, navigate to Tools -> Terminal and enter e.g. C:\Windows\System32\bash.exe into the Shell Path input - you should now be see the Ubuntu prompt when you select the Terminal tab. (if it fails to open, check the properties of the shortcut you use to launch Php Storm - if it points to phpstorm.exe, that's the 32-bit version, which can't host a 64-bit process; change it to phpstorm64.exe and restart Php Storm.)

To integrate PHP, navigate to Languages and Frameworks -> PHP and open the CLI Interpreters dialog by clicking on the ... button, then add a new Remote interpreter via the + button, then:

  • Check the SSH Credentials option
  • Enter localhost for Host, and enter the port-number you selected
  • Fill in your username
  • Select Key Pair from the Auth Type drop-down and select your private key file (your home-folder is accessible from Windows via e.g. %LOCALAPPDATA%\Lxss\home\{username}\.ssh\id_rsa)
  • Enter the passphrase for your private key (if any) and optionally check the Save option.
  • For PHP Executable, enter /usr/bin/php, then click the refresh-button.

If successful, you should now see the PHP version displayed below.

OK to close the Interpreters dialog.

Your new interpreter will be available for use in other projects, but the Path Mappings needs to be configured per-project - click the ... button and select e.g. C:\ as your Local Path, and /mnt/c as your Remote Path - this enables PHP Storm to correctly launch PHP and map between Linux and Windows paths.

You should now be able to launch your Run/Debug Configurations e.g. to run your tests, etc.

Note that the console output will of course display Linux paths (in stack traces, etc.) but clicking on a Linux path, PHP Storm should now be able to map and navigate to the file in the Windows file-system where your PHP Storm project resides.

Tips for the Linux Illiterate

This section may contain things that are totally obvious to the Linux literate, but I'm a humble Windows user.

Display Status of Services on Start-up

When you close the last open Bash terminal window, services (including PHP-FPM, NGINX and the SSH daemon) all shut down.

If you're like me, you open and close terminal windows all day, which can make it a bit difficult to keep track of whether you closed the last window and if the services are still running.

When an interactive shell that is not a login shell is started, Bash reads and executes commands from ~/.bashrc.

If you'd like to display the status of various services when you open a Bash terminal window, edit your ~/.bashrc files, and add the following lines to the end of it:

echo Service status:
service nginx status
service php8.2-fpm status
service ssh status

When you open a Bash window now, you'll be reminded of whether you need to start those services.

Authenticating with SSH Agent (Git and Composer)

When you run e.g. composer install, it runs git commands while cloning repositories, etc.

If you added a password to your personal key, you may get prompted repeatedly for your password, e.g. when cloning from private repositories.

To work around this, you can start the SSH agent in advance:

eval $(ssh-agent)

Once this is running, type ssh-add to authorize the agent to authenticate on your behalf - you'll need to enter your password once, and then, for the duration of the current session, the agent with authenticate for you.

If you prefer, you could also add these two commands to the end of your ~/.bashrc script, as explained in the section above - you'll get prompted for your password every time you open a Bash window, but you won't get interrupted by authentication prompts while you're working in the terminal anymore.

Pitfalls

One major pitfall of running two operating systems on the same native file-system, is that the Windows and Linux versions of the same programs may use slightly different file-formats for persistence.

For example, programs such as npm, git and composer may store absolute file-system paths - in the .git folder, for example, a given file may be indexed as /mnt/c/foo/bar.txt by the Linux version, whereas the Windows version would index it as C:\foo\bar.txt, and these paths can only be resolved on their home file-system.

This can be a problem if you continue to use both Windows and Linux versions of these tools, and use the same workspace folder for your Linux and Windows projects.

There are two ways you can deal with this issue: either keep your Linux and Windows workspace root project folders completely separate - or commit yourself to the Linux console completely and uninstall the Windows versions of these tools to avoid any confusion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment