Finding the perfect SSH host

I'm trying to find a company providing hosting capabilities. I have a strong set of pre-requisites.

Connected through SSH

I want the server to be accessible through SSH. I don't need http nor ftp acces on it.

I'll host git, mercurial and subversion repositories on it, and will push(/pull) from(/to) them using ssh.

So, I will need those softs installed, or at least enough power to install them (even locally).

Serving as an SSL tunnel

I also want to be able to create an SSH tunnel through this server. That way I could be securely connected from any wifi connection using SSL.

The server should have a pretty reasonable bandwidth. I'd also wish to avoid having it hosted in France. Peering agreement between providers in France are getting uglier and uglier.

Secondary accounts for holding websites

If the same company also provides more conventional hosting, that also could be an important part of my choice.

What I would like is something as flexible as allowing some websites to use Apache, and others Lighttpd. Even being able to configure it per subdomain.

Some sites would be running PHP, others Rails, even node.js. I want enough freedom to configure it and the associated inner config (lighttpd.conf, php.ini).

I also want to be able to install secondary parts like mongoDB or memcache and having a full access to their configuration.

Pricing

Of course, the cheaper the better, but I know that such an amount of freedom and features comes with its own price.

Now, let's review some of the contestants I've picked

Alwaysdata

They were the first I checked. I heard quite a good thing about their service and reliability. They really seem to know what they're doing.

I've tried their free 10Mo plan and am quite satisfied with it. Mercurial, git, subversion and quite a batch of others softs are already installed. They also have a pretty 10Mo/s awesome bandwith.

I haven't tried their hosting plans, but they also seems nice. All default (shared) hosting provides php, perl, ruby and python.

Their custom (managed dedicated server) provides almost anything you could want from lightty to memcache, but the price goes high with it to. 350€/month that is way too much I can afford for what I have in mind.

On the other hand, they not only provide hosting but the whole support package along with hotline and custom installation.

Dreamhost

I'm fond of Dreamhost. I've used them for years. They do provide a very nice shared hosting, with a lot of options to configure. They even allow cronjob for their admin panel.

And they are cheap, with a customer support very professional, I highly recommend them for your simple hosting needs.

I haven't tested their dedicated server version, so can't really talk about it.

However, the SSH capabilities of the shared hosting are quite small. They have git, svn and mercurial installed, but the version are quite outdated. Even the python version running mercurial was out of date.

You can however download and install them yourself on your account (which I did), but it is not as full featured as alwaysdata.

Their bandwith is also capped to 1Mo/s.

OVH

Haven't tested yet, but their VPS offer seems nice for as low as 60€/year.

I have tried the shared hosting offer of OVH before and am mitigated. On one hand I like some of their features like the automatic backup of your ftp files, on the other hand their administration panel is a total mess and accessing the server through SSH is not that simple.

I've heard they have a 10Mo/s bandwith. I'll try to get more information on this.

Gandi

Gandi is an amazing registrar (both technically and ethically). I've never had any issues with their service and can only praise their technical support.

Some years ago, they started doing hosting too. I tested it back then and was quite disapointed. This was horribly slow and crashed.

I never tried it again since then. I think they improved their service, and I just asked a test account. Their VPS offer is interesting, the server is pretty decent from what I can read and the price is still in my range (12€/month).

I'll continue this review later, with more information. If you, reader, have any suggestion on a good host filling my need, feel free to post it in the comments, I'll review it.

Another PHP casting weirdness

Second weird PHP behavior behavior of the day :

$foo = false;
echo $foo['bar']; // Ouptuts nothing, but doesn't throw an error either

Strange. I try to read an undefined index (falseis not an array), but PHP doesn't complain. That's weird.

$foo = true;
echo $foo['bar']; // Throws an error

This time, PHP tells me that the index is not defined and throws an error. Wait, what ?

Apparently, this is the intended behavior, but it does seem a bit strange...

json_decode casts strings to floats

The PHP function json_decode takes a JSON string as argument and return a decoded array/object .

However, when passing an argument that has nothing to do with a JSON string, the function was supposed to return null. But, in practice, this didn't go so well.

Passing a string

Here's an example :

$foo = 'foo';
$bar = json_decode($foo);
// $bar is nothing,
echo (is_null($bar)) ? "This is null" : "This is not null";

Passing a float

Here, we try to decode a string. The function rejects it and returns null.

$foo = 0.4;
$bar = json_decode($foo);
// $bar is a float
echo (is_float($bar)) ? "This is a float" : "This is not a float";

Here, we pass a floating number. And the function returns... a floating number. Wait, what ?

This is weird, I expected it to return null, once again. But maybe it's correct and the JSON specs says so. I didn't check, actually.

Passing a string thats looks like a float

But the next example is even better :

$foo = '0.4';
$bar = json_decode($foo);
// $bar is also a float, even if $foo was a string
echo (is_float($bar)) ? "This is a float" : "This is not a float";

This time I pass a string as a parameter (like example 1) but got a float as a result (like example 2).

Well, this time I'm sure that can't be the correct behavior.

How to deal with it

I finally wrote a little wrapper for json_decode to handle those strange cases :

function my_json_decode($var) {
  $decodedValue = json_decode($var, true);
  return is_array($decodedValue) ? $decodedValue : $var;
}

This will check that the result of the JSON decoding is an array, and if not (meaning the original string was not a JSON string), it will return the original string.

Custom logging system for cakePHP

I do love cakePHP, but sometimes it can get tricky to get it to do exactly what you want.

For our big app, we needed to stop using the builtin logging system to take advantage of the syslog and log analyzer tools we had.

At first I got confused by the various log files, but after reading the cake core code, it (kinda) makes sense. Let me explain it to you.

CakeLog and FileLog

CakeLogis the cake static class that handles all the log actions. You can call it yourself statically using CakeLog::write(), but cake also calls it itself when an error is reported.

CakeLoginternally write its content using the FileLog. This FileLogwrites its content to files located in app/tmp/logs.

Writing errors to the syslog instead

We didn't want our logs to be saved in app/tmp/logs for three mains reasons :

  1. Those files gets deleted on every deploy we did (this was how our deployment system works)
  2. Those files can get very big very fast
  3. Our app was distributed accross several servers, meaning that each server had its own set of log files

Instead, we wanted them to be written to the syslog where they would be intercepted and stored in our main log analyzer.

To do so, we wrote a simple SysLogclass to use instead of the FileLog. Here it is :

class SysLog {
  /**
  *    Writes a log to the syslog
  *    \param    $type    Either a numerical constant or a string
  *    \param    $message    Message to log
  **/
  public function write($type, $message) {
    // We "fix" CakeLog that passes severity as a string
    if (is_string($type)) {
      // Mapping string to syslog priorities
      $priorities = array(
        'debug'    => LOG_DEBUG,
        'info'        => LOG_INFO,
        'notice'    => LOG_NOTICE,
        'warning'    => LOG_WARNING,
        'error'    => LOG_ERR,
        'default'    => LOG_NOTICE
      );
      $type = (array_key_exists($type, $priorities)) ? $priorities[$type] : $priorities['default'];
    }
    // Writing to syslog
    openlog(false, 0, LOG_LOCAL7);
    syslog($type, trim($message));
    closelog();
  }
}

Place this file in app/lib/log/sys_log.php. Then, in app/config/bootstrap.php, place the following code :

CakeLog::config('default', array('engine' => 'SysLog'));

It is important that you place the CakeLog::config() call inbootstrap.php and not in core.php because of the way cake actually loads its internal. I also manually defined the syslog facility as LOCAL7, but you can change it to whatever you want or even update the code so the actual facility can be passed as a parameter of the CakeLog::config() call (actually, that's how it's done in my real code, but I didn't want to overcomplicate the example).

Also, note that I added a special code to handle the way CakeLog passes parameters to the SysLog. CakeLog passes the severity as a string, so we convert it back to the PHP constants.

With this code, all your logs will now be routed to the syslog, and your app/tmp/logs directory will no longer grow in size (instead, that will be your /var/logs/syslog :) )

Where are my errors loggued now ?

Well, your errors are loggued to the syslog. But cake defines its own error handler that will reformat the error thrown by PHP, and reroute it to the CakeLog. In effect, it means that all errors with similar severity will be grouped (E_PARSE, E_ERROR, E_CORE_ERROR, etc will be loggued asLOG_ERROR). Also, cake will parse and reformat the message to log. It can be problematic if you rely on your PHP config to correctly parse your logs because you won't have the expected output.

Hopefully, cake provides a way to disable this error handler, but it was very tricky to find and even more tricky to correctly use. I won't spend too much time on all the details, but what you have to know is :

  1. You have to define a define('DISABLE_DEFAULT_ERROR_HANDLING', true); in order to disable the cake error handler and use the default PHP one
  2. This call MUST be done before your Configure::write('debug') call otherwise it won't work
  3. You also have to define a Configure::write('log', E_ALL & ~E_DEPRECATED); for this to work but...
  4. You can't define both the debug and the log value in the same call using an array, you have to define them in two different calls debugthen log

So, finally, here is the final working configuration :

In app/config/core.php :

Configure::write('log', E_ALL & ~E_DEPRECATED);
define('DISABLE_DEFAULT_ERROR_HANDLING', true);

And inapp/config/bootstrap.php :

CakeLog::config('default', array('engine' => 'SysLog'));

Final words

I wrote this blog post because I got hit by this problem. Twice. I didn't bloggued about it the first time, so a few weeks later when I had to change some config values in my bootstrap.php and core.php I forgot about the specific order of loading things. It took me a couple hours to figure it out again.

So, to avoid running into the same issue some months from now, I took some time to write it down, and hopefully I'll help some of you too.

Parsing error in cakePHP view for empty file

As a convention, I never write the closing PHP tag at the end of my php files. It helps in avoiding the "header already sent" error when dealing with cookie/sessions.

However, today cake complains about

Parse error: syntax error, unexpected $end in /var/www/website/app/views/players/default.ctp on line 1

My default.ctp file only contain the following content :

<?php

It took me a couple of minutes to fix it. I only added an empty line after the opening tag, so it now read :

<?php


The first statement was a perfectly valid php file, but somehow it makes cakePHP fail.

I didn't have time to investigate on it further, nor submit a bug report (because it may well be coming from my custom app, not cake itself) but I plan to.