Datadog collects and monitors your PHP app metrics and distributed traces in real-time with application performance monitoring. Decrease downtime and performance issues with Datadog APM by tracing requests across service boundaries and drilling into individual traces end-to-end with flame graphs. Start your 14-day trial for free today.

Manage Docker containers using PHP

Original – by Freek Van der Herten – 7 minute read

Last week, my colleague Ruben and I released a package called spatie/docker, that makes it easy to spin up docker containers and execute commands on them. In this blog post, I'd like to introduce what you can do with it and why we built this.

Using the package

Spinning up a docker container is pretty straightforward.

$containerInstance = DockerContainer::create($imageName)->start();

Do you want to map some ports? No problem!

$containerInstance = DockerContainer::create($imageName)
    ->mapPort($portOnHost, $portOnContainer)
    ->mapPort($anotherPortOnHost, $anotherPortOnContainer)
    ->start();

When using this package in a testing environment, it can be handy that the docker container is stopped after __destruct is called on it (mostly this will happen when the PHP script ends). You can enable this behavior with the stopOnDestruct method.

$containerInstance = DockerContainer::create($imageName)
    ->stopOnDestruct()
    ->start();

Executing a command on an instance is straightforward too.

$process = $instance->execute($command);

You can execute multiple commands in one go by passing an array.

$process = $instance->execute([$command, $anotherCommand]);

The execute method returns an instance of Symfony/Process.

You can check if your command ran successfully using the isSuccessful method.

$process->isSuccessful(); // returns a boolean

You can get to the output using getOutput(). If the command did not run successfully, you can use getErrorOutput(). For more information on how to work with a Process, head over to the Symfony docs.

In addition to executing a command, you can also add files to the instance.

Files can be added to an instance with addFiles.

$instance->addFiles($fileOrDirectoryOnHost, $pathInContainer);

I can imagine that users would wish that they could add methods of their own to a docker instance. Our package grants that wish. The Spatie\Docker\ContainerInstance class is macroable. That makes it possible to add methods to it in realtime.

Spatie\Docker\DockerContainerInstance::macro('whoAmI', function () {
    $process = $containerInstance->run('whoami');


    return $process->getOutput();
});

$containerInstance = DockerContainer::create($imageName)->start();

$containerInstace->whoAmI(); // returns of name of user in the docker container

Why we built this

I'm currently building a package called spatie/laravel-backup-server. It will be the spiritual successor of spatie/laravel-backup. Laravel Backup is usually installed into the Laravel app, and it will copy that app to some other storage. Laravel Backup Server will take a different approach: it will SSH into each server that needs to be backed up and will copy all the files on to itself.

To test Laravel Backup Server, I needed to have some local servers with SSH capabilities. Docker was the perfect solution for this.

Here's the content of the dockerfile that we use to create the spatie/laravel-backup-server-tests image. In short, this docker container will contain an SSH server.

FROM ubuntu:latest
MAINTAINER Freek Van der Herten <freek@spatie.be>

# add openssh and clean
RUN apt-get update && apt-get install -y openssh-server sed nano rsync

RUN mkdir /var/run/sshd

COPY baseSshKeys /etc/ssh
RUN chmod 600 /etc/ssh/ssh_host_dsa_key
RUN chmod 600 /etc/ssh/ssh_host_ecdsa_key
RUN chmod 600 /etc/ssh/ssh_host_ed25519_key
RUN chmod 600 /etc/ssh/ssh_host_rsa_key

RUN echo "root:root" | chpasswd

RUN sed -ri 's/^#?PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^#?PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
RUN sed -ri 's/^#?SyslogFacility AUTH/SyslogFacility AUTH/g' /etc/ssh/sshd_config
RUN sed -ri 's/^#?LogLevel INFO/LogLevel INFO/g' /etc/ssh/sshd_config

RUN mkdir /root/.ssh
RUN touch /root/.ssh/authorized_keys
RUN chmod 600 /root/.ssh/authorized_keys

EXPOSE 22
CMD    ["/usr/sbin/sshd", "-D"]

Let's take a look at the test that makes sure the backup procedure works. The test will do these things

  1. Spin up a docker container
  2. Copy some files directly into the container
  3. Start the backup procedure
  4. Assert that the files we put on the docker container are backed up.

Here's the setup method of the test.

 public function setUp(): void
{
    parent::setUp();

    Storage::fake('backups');
    
    $this->source = factory(Source::class)->create([
        'host' => '0.0.0.0',
        'ssh_port' => '4848',
        'ssh_user' => 'root',
        'ssh_private_key_file' => $this->privateKeyPath(),
        'includes' => ['/src']
    ]);

    $this->container = DockerContainer::create('spatie/laravel-backup-server-tests')
        ->name('laravel-backup-server-tests')
        ->mapPort(4848, 22)
        ->stopOnDestruct()
        ->start()
        ->addPublicKey($this->publicKeyPath());
}

In the setUp we create a Source model. In our package, a source represents something that needs to be backed up. We also spin up a docker container. That stopOnDestruct will make sure that the docker container will be stopped and destroyed as soon as our test is over. In our test suite, we also have stored a private/public key pair. The public key is copied to the docker container. On the Source model, we saved the path to the private part in the ssh_private_key_file attribute, so the backup procedure will use that one when logging in to the docker container (which will run on IP 0.0.0.0 aka localhost on port 4848).

With that out of the way, let's take a look at the test itself.

/** @test */
public function it_can_perform_a_backup()
{
    $this->container->addFiles(__DIR__ . '/stubs/serverContent/testServer', '/src');

    $this->artisan('backup-server:backup')->assertExitCode(0);

    $this->assertTrue($this->source->backups()->first()->has('src/1.txt'));

    $this->assertEquals(Backup::STATUS_COMPLETED, $this->source->backups()->first()->status);
}

The first thing that we do in the test is to copy a file directly onto the container. Next, we run the backup procedure (we assert it ran ok), and after that, we assert that the backup contains the file that we just copied onto the container.

I love these kinds of tests that cover a lot of functionality without testing the implementation details. The test verifies the result, and it does not care about how the result was achieved. I can still refactor all the internal of the package, and this test should still pass.

Of course, more tests are needed to cover the entire behavior of the backup procedure fully. Let's take a look at a couple more. In this one, we don't use a private key file, simulating a login problem. The backup command should run without errors, but the backup it produces should be marked as failed.

/** @test */
public function it_will_fail_if_it_cannot_login()
{
    $this->source->update(['ssh_private_key_file' => null]);

    $this->artisan('backup-server:backup')->assertExitCode(0);

    $this->assertEquals(Backup::STATUS_FAILED, $this->source->backups()->first()->status);
}

Laravel-backup-server can also perform commands via ssh before the actual backup runs. We test that out in this test.

/** @test */
public function it_can_perform_a_pre_backup_command()
{
    $this->container->addFiles(__DIR__ . '/stubs/serverContent/testServer', '/src');

    $this->source->update(['pre_backup_commands' => ['cd /src', 'touch newfile.txt']]);

    $this->artisan('backup-server:backup')->assertExitCode(0);

    $this->assertTrue($this->source->backups()->first()->has('src/newfile.txt'));

    $this->assertEquals(Backup::STATUS_COMPLETED, $this->source->backups()->first()->status);
}

Here we create a file using touch inside the directory we wish to backup, and we assert that that file exists in the created backup.

In closing

Controlling Docker containers sure is handy. We built to package to easily test the behavior of our upcoming laravel-backup-server package. I'll share more about that package soon.

I'm pretty sure you can come up with other scenarios in which our spatie/docker package is useful. It has some more features, not mentioned in this post. Head over to the readme of the package on GitHub to learn more. This isn't the first package our team has built. Here's a list with all the stuff we released previously.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

Webmentions

Adrian Brown 🕶 liked on 12th February 2020
Swetank Raj ⚡ #DevFestKol liked on 12th February 2020
chilic retweeted on 11th February 2020
Dibyajyoti Panda liked on 11th February 2020
Dibyajyoti Panda replied on 11th February 2020
Loved it.
Ian Rodrigues 🚴 🐘 liked on 11th February 2020
Roman Pronskiy liked on 11th February 2020
Mozammil liked on 11th February 2020
. liked on 11th February 2020
Flamur Mavraj replied on 11th February 2020
Try from your phone, same result with Chrome and Android built in browser.
Freek Van der Herten replied on 11th February 2020
Made a small fix, could you check if it’s working now for you?
Flamur Mavraj liked on 11th February 2020
Freek Van der Herten replied on 11th February 2020
Not seeing that, trying to reproduce…
Flamur Mavraj replied on 11th February 2020
#oioioi
GC retweeted on 11th February 2020
Vikash Pathak liked on 11th February 2020
Vikash Pathak retweeted on 11th February 2020
Tony Messias retweeted on 11th February 2020
Tony Messias liked on 11th February 2020
Ciberninjas liked on 11th February 2020
mjczz retweeted on 11th February 2020
Sam Wrigley liked on 10th February 2020
Miguel Manzano García retweeted on 10th February 2020
Neil Carlo Faisan Sucuangco liked on 10th February 2020
Salman Zafar liked on 10th February 2020
Runar Jørgensen liked on 10th February 2020
Spatie retweeted on 10th February 2020
Радослав Витанов liked on 10th February 2020
Niels liked on 10th February 2020
Jimmy Lipham liked on 10th February 2020
Ahmad Ismail retweeted on 10th February 2020
oluwajubelo loves VueJS 🚨 retweeted on 10th February 2020
Mateus Junges liked on 10th February 2020
BallerinaNews liked on 10th February 2020
Dhanushka madushan liked on 10th February 2020
Miguel Orellana liked on 10th February 2020
Jaime Sares liked on 10th February 2020
Nasirou Wagana liked on 10th February 2020