Introduction

These are the steps I followed to create a production web server on Ubuntu 7.10 Gutsy. If you use exactly the same versions of everything you should be able to follow these notes exactly. I expect they will work for at least Ubuntu 6 upwards, and for the versions of the components upwards. Each of Apache 2, MySQL 5 and PHP 5 included major changes from their previous major versions, so it’s unlikely that these notes will work for older releases.

If you just want to play with PHP/MySQL I strongly suggest installing XAMPP, as this is much easier to get up and running. However, if you are running a live web site with thousands of real users, this document might be helpful. It covers installing from source code, which involves compiling and linking with various options to optimise for speed. At the end of it you should come out with a web server and database configured with performance and security in mind.

When I set out to do this write up, I thought I would encourage good practice and use sudo for all the commands that need it. In practice typing those 5 extra characters all the time was just too much, so I was logged in as root as well as my normal user. Luckily I never make msitakes. So in the interests of sticking to the facts, these notes will occasionally assume you are the root user, indicated by the prompt in front of the command. The ones starting with the hash (pound) sign are to be done as root. The ones with a dollar are to be done as your normal user. If you find yourself typing or pasting this prompt into your own command line I’m afraid you’re in way over your head!

Tidy up

Before you start installing these applications, you should check if any are already installed, as will be the case if the “LAMP server” option was selected when Ubuntu was originally installed. So grep for the corresponding package names in the list of installed packages:

dpkg --list|grep -e httpd -e apache -e mysql -e php

Look for “ii” beside the package name, indicating it’s installed. If LAMP was already installed from binaries, you will probably see a list similar to this:

ii  apache2                               2.2.4-3build1           Next generation, scalable, extendable web se
ii  apache2-mpm-prefork                   2.2.4-3build1           Traditional model for Apache HTTPD
ii  apache2-utils                         2.2.4-3build1           utility programs for webservers
ii  apache2.2-common                      2.2.4-3build1           Next generation, scalable, extendable web se
ii  libapache2-mod-php5                   5.2.3-1ubuntu6          server-side, HTML-embedded scripting languag
ii  libdbd-mysql-perl                     4.004-2                 A Perl5 database interface to the MySQL data
ii  libmysqlclient15off                   5.0.45-1ubuntu3         MySQL database client library
ii  mysql-client-5.0                      5.0.45-1ubuntu3         MySQL database client binaries
ii  mysql-common                          5.0.45-1ubuntu3         MySQL database common files
ii  mysql-server                          5.0.45-1ubuntu3         MySQL database server (meta package dependin
ii  mysql-server-5.0                      5.0.45-1ubuntu3         MySQL database server binaries
ii  php5-common                           5.2.3-1ubuntu6          Common files for packages built from the php
ii  php5-mysql                            5.2.3-1ubuntu6          MySQL module for php5

If you see any of these, you should uninstall them before continuing. My hosting company had installed Ubuntu for me, and when I first logged in Apache2 and mysql were running. If this is the case you can stop them as root:

# ps -wef|grep apache2
root      4006     1  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4025  4006  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4026  4006  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4027  4006  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4028  4006  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4029  4006  0 09:41 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4140  4006  0 14:24 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4141  4006  0 14:24 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  4142  4006  0 14:24 ?        00:00:00 /usr/sbin/apache2 -k start
root      4157  4125  0 14:36 pts/0    00:00:00 grep apache2
# apache2ctl stop
# ps -wef|grep apache2
root      4162  4125  0 14:36 pts/0    00:00:00 grep apache2
# ps -wef|grep mysql
root      3857     1  0 09:41 ?        00:00:00 /bin/sh /usr/bin/mysqld_safe
mysql     3897  3857  0 09:41 ?        00:00:00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --user=mysql --pid-file=/var/run/mysqld/mysqld.pid --skip-external-locking --port=3306 --socket=/var/run/mysqld/mysqld.sock
root      3898  3857  0 09:41 ?        00:00:00 logger -p daemon.err -t mysqld_safe -i -t mysqld
root      4355  4310  0 18:54 pts/1    00:00:00 grep mysql
# /etc/init.d/mysql stop
 * Stopping MySQL database server mysqld      [ OK ]
# ps -wef|grep mysql
root      4395  4310  0 18:55 pts/1    00:00:00 grep mysql

Now remove the offending packages:

# 3 remove php5-mysql libapache2-mod-php5 php5-common
# apt-get remove libdbd-mysql-perl mysql-server mysql-server-5.0 mysql-client-5.0
# apt-get remove libmysqlclient15off mysql-common
# apt-get remove apache2 apache2-mpm-prefork apache2.2-common apache2-utils

Apache2 was also configured to start at reboot. To remove this, as root:

# rm /etc/init.d/apache2
# update-rc.d apache2 remove

Prerequisites

In order to build we’ll need make, GNU C, etc. These are bundled in the package build-essential. We also need ncurses development for MySQL to compile:

# aptitude install build-essential
# aptitude install libncurses5-dev

TIP: If like me, your CD is corrupt, or if putting the CD in is just too much hassle anyway, you can tell aptitude to look on the internet for packages. Edit /etc/apt/sources.list and comment out the line that starts “deb cdrom”

I saw this error after trying to install build-essential:

Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/l/linux-source-2.6.20/linux-libc-dev_2.6.20-16.29_i386.deb  404 Not Found [IP: 91.189.88.31 80]
E: Unable to fetch some archives, maybe run aptitude update or try with --fix-missing?

So I did “aptitude update” then reran the install command and it worked. How easy was that? I can’t believe I persevered with RPM all these years!

Get the sources

  • Apache from http://httpd.apache.org/. Go to the download page and choose a mirror close to you. I used version 2.2.6.
  • MySQL from http://dev.mysql.com/. The community server is the free one! Go to the bottom of this page where the source is, and pick a mirror for the tar.gz. Don’t get the download from the section marked Ubuntu because these are binaries. I used version 5.0.45.
  • PHP from http://www.php.net/downloads.php. You want the complete source code, and bzip is a much smaller file than gzip. I used version 5.2.4.

Download them to your home directory, then unzip them in /usr/local/src:

cd /usr/local/src
# tar xzf ~/httpd-2.2.6.tar.gz
# tar xzf ~/mysql-5.0.45.tar.gz
# tar xjf ~/php-5.2.4.tar.bz2


Warning: Lots of things can go wrong at this point. It is considered best practice to wear your lucky Ubuntu thong.

Compile MySQL

Add a group and a user to run MySQL as. These might already exist on your system, which is fine.

# groupadd mysql
# useradd -g mysql -c "MySQL Server" mysql

Give ownership of the source to your current user so you can make. Let’s say my normal user is mjhinds, so as root:

# cd /usr/local/src/mysql-5.0.45
# chown -R mjhinds .

The Ubuntu help page on compiling MySQL suggests that if you’re on an Intel P4 you could optimise for this architecture by setting the following environment variables before compiling:

$ export CHOST="i686-pc-linux-gnu"
$ export CFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer -msse -mmmx  -mfpmath=sse"
$ export CXXFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer -msse -mmmx  -mfpmath=sse -felide-constructors -fno-exceptions -fno-rtti"

There are also settings for AMD and a generic Linux one. You should use one of these, if only to tell it to omit frame pointers. The MySQL documentation says compiling on x86 without frame pointers makes mysqld 1-4% faster. So export those variables as mjhinds, then generate your Makefile by running configure:

$ cd /usr/local/src/mysql-5.0.45/
$ ./configure \
  --prefix=/usr/local/mysql \
  --localstatedir=/usr/local/mysql/data \
  --with-mysqld-user=mysql \
  --with-unix-socket-path=/tmp/mysql.sock \
  --without-debug \
  --without-bench \
  --with-mysqld-ldflags=-all-static \
  --disable-shared \
  --with-charset=utf8

To briefly explain those options:

  • prefix: the default is /usr/local. /usr/local/mysql is much tidier!
  • localstatedir: similarly, your data goes into /usr/local/var by default
  • with-mysqld-user: tell it the user we created which we’ll run as
  • with-unix-socket-path: hmm, this might be the default anyway. No harm specifying it. This is for local connections to the database.
  • without-debug: turn off debugging for a decent speed improvement
  • without-bench: turn off benchmarking
  • with-mysqld-ldflags: static linking for the server is much faster
  • disable-shared: don’t build shared library
  • with-charset: utf8 is the most flexible while not using as much space as utf16. I couldn’t find a definitive answer, but I believe latin1 doesn’t include the euro symbol.

Check out this page which talks about compile time optimisations.

IMPORTANT! If you make changes to your configure option, you must do a “make distclean” before re-running configure. Otherwise it might pick up some cached settings, and you could end up with a broken installation.

$ make

Took 21 minutes on my 1GHz Pentium 3 (,) with half a gig of RAM. No, that’s not my production server 🙂 Now install the binaries as root:

# cd /usr/local/src/mysql-5.0.45/
# make install

And create MySQL’s data dictionary:

# ./scripts/mysql_install_db

Set the permissions on the installed files:

# chown -R root:mysql /usr/local/mysql
# chown -R mysql:mysql /usr/local/mysql/data

Strip the symbols from the object file. Think this is a minor performance increase?

# strip /usr/local/mysql/libexec/mysqld

Copy the configuration file (this assumes a decent amount of memory):

# cp support-files/my-large.cnf /etc/my.cnf
# chown root:sys /etc/my.cnf
# chmod 644 /etc/my.cnf

There are a few lines we need to change in /etc/my.cnf:

  • Add the line “innodb_file_per_table” under [mysqld]. This will make your life as a DBA easier.
  • If you are running MySQL on the same machine as the web server, uncomment the line “skip-networking”. This means we can only connect via Unix sockets or named pipes.
  • Uncomment all the lines that start with “innodb”.

Configure MySQL to start automatically. I have a feeling this isn’t necessary any more, but no harm trying to do it again:

# cp ./support-files/mysql.server /etc/init.d/mysql
# chmod +x /etc/init.d/mysql
# update-rc.d mysql defaults

Test this. The following command should display a few dots then a star. Then hopefully MySQL is running!

# /etc/init.d/mysql start
    

I prefer to add a link to the mysql binaries I use in /usr/bin than add MySQL’s bin directory to the path:

# ln -s /usr/local/mysql/bin/mysql /usr/bin/mysql
# ln -s /usr/local/mysql/bin/mysqldump /usr/bin/mysqldump

Now we can connect:

mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.0.45-log Source distribution

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> show tables;
ERROR 1046 (3D000): No database selected
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| test               |
+--------------------+
3 rows in set (0.00 sec)

mysql> exit
Bye

Note the lack of security here. Let’s fix that:

/usr/local/mysql/bin/mysqladmin -u root password your-password

Test the new password:

mysql -u root -p

Now drop the test database and remove a couple of extra users that aren’t needed:

drop database test;
use mysql;
delete from db;
delete from user where not (host="localhost" and user="root");
flush privileges;
# /etc/init.d/mysql stop

Compile Apache

Add a group and a user to run Apache as. These might already exist on your system, which is fine.

# groupadd httpd
# useradd httpd -g httpd -c "Apache" -d /dev/null -s /sbin/nologin

Give yourself ownership of the source so you can make:

# cd /usr/local/src/httpd-2.2.6/
# chown -R mjhinds .

Set the environment vars appropriate for Apache and your architecture. A safe default would be “-O2”. For Pentium 4 we can try:

$ export CFLAGS="-O3 -march=pentium4 -msse -mmmx -funroll-loops -mfpmath=sse"

With Apache 2 a lot of modules are enabled by default. Check this page to see which ones you might be able to exclude. Now configure accordingly:

$ ./configure \
  --prefix=/usr/local/apache \
  --disable-cgi \
  --disable-imap \
  --disable-userdir \
  --enable-rewrite=static \
  --enable-so=static

IMPORTANT! If you make changes to your configure option, you must do a “make distclean” before re-running configure. Otherwise it might pick up some cached settings, and you could end up with a broken installation.

$ make

Took just 6 minutes on my old-timey Pentium 3. Now install the binaries as root:

# cd /usr/local/src/httpd-2.2.6/
# make install

make apache auto start Process running: /bin/sh /usr/bin/mysqld_safe /usr/sbin/mysqld –basedir=/usr –datadir=/var/lib/mysql –user=mysql –pid-file=/var/run/mysql/mysql.pid –skip-external-locking –port=3306 –socket=/var/run/mysqld/mysqld.sock

PHP required packages

There are a few libraries we need to compile PHP with useful options. As root:

# aptitude install libxml2-dev libpng3 libpng12-dev mcrypt libncurses5 libjpeg62-dev libmcrypt-dev libmhash-dev libcurl3-dev

For JPEG support I had to hack in a link to get configure to find the JPEG library:

# ln -s /usr/lib/libjpeg.so.62.0.0 /usr/lib/libjpeg.so

Compile PHP

Give yourself ownership of the source so you can make:

# cd /usr/local/src/php-5.2.4
# chown -R mjhinds .

Set the environment vars appropriate for PHP and your architecture. A safe default would be “-O2”. For Pentium 4 we can try:

$ export CFLAGS="-O3 -march=pentium4 -msse -mmmx -funroll-loops -mfpmath=sse"
$ cd /usr/local/src/php-5.2.4
$ ./configure \
  --prefix=/usr/local/php5 \
  --with-apxs2=/usr/local/apache/bin/apxs \
  --with-mysql=/usr/local/mysql \
  --with-mysql-sock=/tmp/mysql.sock \
  --with-mcrypt \
  --with-mhash \
  --with-curl \
  --with-curlwrappers  \
  --disable-cgi \
  --enable-mbstring=all  \
  --with-gd  \
  --enable-gd-native-ttf  \
  --with-ttf  \
  --with-jpeg-dir=/usr/lib \
  --enable-magic-quotes
$ make

The trusty P3 knocked this one out in 12 minutes. Install the binaries as root:

# cd /usr/local/src/php-5.2.4
# make install

Now copy the ini file “warmly recommended” for production:

# cp php.ini-recommended /usr/local/php5/lib/php.ini

Now edit /usr/local/php5/lib/php.ini and find the following line:

error_reporting = E_ALL

This is a good, finicky setting for dev, but with my sloppy legacy code I’d rather not have errors popping up all over production, so best to change to:

error_reporting = E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR

By default you also get the command line interface, which can be useful for scheduling tasks relating to your web application. Rather than add PHP’s bin directory to your path you can link to it from /usr/bin:

# ln -s /usr/local/php5/bin/php /usr/bin/php

Configure Apache

Edit /usr/local/apache/conf/httpd.conf and change the lines:

User daemon
Group daemon

to

User httpd
Group httpd

Find the following line and change to suit your domain:

ServerAdmin you@example.com

Uncomment the following line and change to suit your domain:

#ServerName www.example.com:80

Assuming you put your web pages in /web/domain/www, find following line and change to suit your domain:

DocumentRoot "/usr/local/apache/htdocs"

Under where it says “This should be changed to whatever you set DocumentRoot to.” you should do so!

Change

AllowOverride None

to

AllowOverride All

In the <Directory%gt; entry for your domain, change

Options Indexes FollowSymLinks

to

Options FollowSymLinks

(this stops directory listings)

Inside the <IfModule mime_module> add:

# Parse these types as PHP
AddType application/x-httpd-php .html .php .xml .js

Inside the <IfModule dir_module> change to:

DirectoryIndex index.html index.php

Find the lines starting:

#ErrorDocument

Uncomment and point them to your own custom error pages.

Test apache startup:

# /usr/local/apache/bin/apachectl start

If all goes well you should be able to point your browser at the server on port 80. If it’s not working, have a look at /usr/local/apache/logs/error_log

When it works, make a link to apachectl for command line use:

# ln -s /usr/local/apache/bin/apachectl /usr/bin

And make it startup automatically when the server starts:

# cd /etc/init.d
# rm apache2
# update-rc.d apache2 remove
# ln -s /usr/local/apache/bin/apachectl apache
# update-rc.d apache start 91 2 3 4 5 . stop 09 0 1 6 .

So now you can create your page root…

# mkdir -p /web/domain/www
# chown -R httpd:httpd /web

…put a “hello world” in there, and fingers crossed you should be able to browse to it.

Quick note on rebuilding:

For any of these compile steps, if you need to make changes to the configuration and run ./configure again be sure to do a “make clean” before “make”.

If you recompile PHP, make sure Apache isn’t running. This is the polite way to bring down Apache:

$ sudo apachectl stop

…and of course start to start it again.

Optional stuff

PDFLib

You must pay for PDFLib, but it is pretty darn useful. You can trial it for free with full functionality, but every PDF has a watermark added. Well, I say watermark. More like a bloody great tea stain to be honest. Download the PHP version from www.pdflib.com and untar as root:

# cd /usr/local/src
# tar xzf ~/PDFlib-7.0.2-Linux-php.tar.gz
# mkdir /usr/local/php5/lib/php/extensions
# cp PDFlib-7.0.2-Linux-php/bind/php5/php-520/libpdf_php.so /usr/local/php5/lib/php/extensions

Now edit /usr/local/php5/lib/php.ini and change the line that starts “extension_dir” to this:

extension_dir = "/usr/local/php5/lib/php/extensions/"

Also add this line with all the other extensions:

extension=libpdf_php.so

Cronolog

Cronolog is a very useful utility for rolling an application’s log files. There is an Ubuntu package of version 1.6.2, but I prefer the 1.7.0 beta version because it allows you to set ownership on the files. Download it from http://cronolog.org/patches/.

# cd /usr/local/src
# tar xzf ~/cronolog-1.7.0-beta.tar.gz
# cd cronolog-1.7.0
# CFLAGS="-O3 -DFILE_MODE=0640" ./configure
# make
# make install

I like to keep my log files in my /web directory beside the server’s page root. So:

$ cd /web/servername
$ mkdir logs

Now configure Apache to use cronolog by editing /usr/local/apache/conf/httpd.conf. We need to change 2 lines, one for the access log and one for the error log. Find the lines that starts with ErrorLog and CustomLog and change them as follows (they’re not next to each other in the file):

ErrorLog "|/usr/local/sbin/cronolog --set-gid=httpd --symlink=/web/servername/logs/error.log --prev-symlink=/web/servername/logs/error.log.prev /web/servername/logs/%Y/%m/error_%Y%m%d.log"
CustomLog "|/usr/local/sbin/cronolog --set-gid=httpd --symlink=/web/servername/logs/access.log --prev-symlink=/web/servername/logs/access.log.prev /web/servername/logs/%Y/%m/access_%Y%m%d.log" combined

memcached

This seems to be the caching daemon of choice for highly loaded websites. There are 3 things to install: the memcache daemon, libevent which it requires, and the PHP client library. The recommended way of building the PHP client is with PECL, but since I have no need or desire to install this on the production server I built the client on a separate machine and just copied over the .so file it created.

First, install libevent:

$ sudo aptitude install libevent-dev

Now download, build and install memcached:

$ wget http://www.danga.com/memcached/dist/memcached-1.2.6.tar.gz
$ cd /usr/local/src
$ sudo tar xzf ~/memcached-1.2.6.tar.gz
$ cd memcached-1.2.6
$ ./configure
$ make
$ sudo make install

Now you can test it with the following command which uses just 10Mb of memory, listens on localhost port 11211 and runs as the nobody user:

$ memcached -m 10 -l 127.0.0.1 -u nobody -p 11211

We’ve left out the -d option so it runs in the foreground (or shows you errors if there are any). Use CTRL-C to stop it.

This is how I installed PEAR and built the PHP client (on a test server). It assumes you’ve already installed PHP to /usr/local/php5.

# aptitude install php-pear
# aptitude install autoconf
# cd /usr/bin
# ln -s /usr/local/php5/bin/phpize
# ln -s /usr/local/php5/bin/php-config
# pecl install memcache

It will tell you where it has installed the library to, probably “/usr/local/php5/lib/php/extensions/no-debug-non-zts-20060613/memcache.so”.

Copy this file to the production server into your PHP extension directory as specified in php.ini.

$ cp ~/memcache.so /usr/local/php5/lib/php/extensions/

Edit /usr/local/php5/lib/php.ini to load it by adding this line with the other extensions:

extension=memcache.so

Now you should be able to call memcache from PHP. Start memcache as above and create the following test page:

<?php

$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");

$version = $memcache->getVersion();
echo "Server's version: ".$version."
\n";

$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;

$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");
echo "Store data in the cache (data will expire in 10 seconds)
\n";

$get_result = $memcache->get('key');
echo "Data from the cache:
\n";

var_dump($get_result);

?>

There should also be a PHP admin page installed as part of the PHP client at /usr/share/php/docs/memcache/memcache.php. Copy this page from the test server to somewhere under your production page root. Note that you’ll have to change the $MEMCACHE_SERVERS array to suit your setup:

$MEMCACHE_SERVERS[] = '127.0.0.1:11211'; // add more as an array
#$MEMCACHE_SERVERS[] = 'mymemcache-server2:11211'; // add more as an array

If you’re relying on the built in authentication you should change the user/password in the same file.

To get memcached to automatically start, create the file /etc/init.d/memcached as root as follows:

#!/bin/sh -e
/usr/local/bin/memcached -d -m 10 -p 11211 -u nobody

Make it executable, and add it to the start up scripts:

# chmod u+x /etc/init.d/memcached
# update-rc.d memcached defaults

More than one daemon

Once you’re comfortable running memcached you might want to increase the number of daemons in order to split out the various logical objects that you’re caching. The advantage of this is that you can flush one cache without affecting the others. To run another daemon, just add another line to /etc/init.d/memcached and use the next available port number. Of course you’ll have to update your application so it knows which ports map to which objects.

References

Most of this document was compiled by taking the best from the following sources:

http://www.michaelhinds.com/tech/linux/ubuntu-server.html#mysql