Statamic meets Hetzner Cloud, Ploi and Deployer
Tags: statamic, devops, deployer, laravel
Published: 2022-07-02 16:36:22


Enough with the adjustments, let's deploy this thing! 🚀

🚧 This article is work in progress. 🚧

I'll use the following combination for this:

- (private) GitHub repository
- [DDEV](https://ddev.com/) for my local dev environment, see previous articles
- [Deployer](https://deployer.org/) for the actual deployment workflow
- [Hetzner Cloud Hosting](https://www.hetzner.com/cloud)
- [Ploi.io](https://ploi.io/) for help with setting up the cloud server and adding PHP projects

## Register a Hetzner Cloud Account

Hetzner launched a flexible [cloud service](https://www.hetzner.com/cloud) back in 2018, but I wasn't really aware of it. Why Hetzner? They offer servers in Germany (and other EU-locations) which is important to avoid GDPR-struggles. They are also listed in the [Green Web Directory](https://www.thegreenwebfoundation.org/directory/). 🌱

The CX11 entry plan starts with a monthly price cap of 4,15€ / month. If you use it less than a month and delete it before, there will be just a hourly rate billed based on your usage. 

That is great for trying out stuff of course. 

> _How do you bill your servers?_
Servers have both a monthly price cap and a price per hour. Your server's bill will never exceed its monthly price cap. If you delete your Cloud Server before the end of the billing month, you will only be billed the hourly rate. We will bill you for each cloud server until you choose to delete them. Even if you aren't actively using your server, we will bill you for it.

Register an account at [hetzner.com/cloud](https://www.hetzner.com/cloud) and create a API token for Ploi (see next step):

![Screenshot API token Hetzner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubp5rvkb0r24pqyoxoqv.png)

## Connect Ploi with Hetzner via API token

[Ploi.io](https://ploi.io/) is like your personal IT server administrator which helps you setting up and configuring the cloud servers. You could do this yourself as well if you're experienced in these (linux) things. I'm not really, so I'll use a service like Ploi, Laravel Forge, Cleavr, etc. ;-)

![Ploi Screenshot add service provider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ywgtupii1rstm4t3rtb.png)

## Create (provision) the first server

Now let's create our first server, I'll go with PHP 8.0 by now:

![Ploi create a server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/njyjpu76srdlqn7gfqz6.png)

![Ploi create a server settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtml2gbko804l9k3mbze.png)

Ploi will now install and configure nginx webserver, database, Redis, Supervisor, PHP, Composer, Memcached, NPM (NodeJS), UFW Firewall, Fail2ban and basic packages on the new server. (See: [ploi.io - Server Installation](https://ploi.io/server-installation). 

## Create a new site

We can add multiple sites to a server. Let's create one and select Statamic as project type:

![Add site on ploi to server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1ybyk2uxssogwr0m5q0.png)

Ploi offers test domains, so no need to directly connect your real URL:

![Screenshot test domain button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jd4ts5w83dnoswcsu3jc.png)

## Don't use 1-click and don't add a repo on Ploi!
 
We **don't** connect a Git repository on Ploi directly.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxa3kmw7q6rjtao8qiwh.png)

While this is also a very handy and convenient feature, we want to use a standardized and portable deployment solution via a Deployer script (which can be used on any host and it can also run in Github Actions).  

## Add Deployer script

[Deployer](https://deployer.org/) is a nice option if your target production server supports composer, NodeJS and SSH. You can write your deploy routine in PHP (instead of fiddling around with Github Action pipeline commands) and test it beforehand locally.

The docs of Deployer are a little bit confusing, because simple and concrete examples are missing. But we will struggle through this together. First install Deployer to your Statamic project:     

```bash
ddev composer require --dev deployer/deployer:^7
```

Now we need to create our `deploy.php` file (you could also write a `deploy.yaml` file). Deployer v7 has already a [Statamic](https://deployer.org/docs/7.x/recipe/statamic) recipe which can be imported and modified. 

This is my current state for the `deploy.php` file:

```php

<?php

namespace Deployer;

require 'contrib/npm.php';
// https://deployer.org/docs/7.x/contrib/npm

import('recipe/statamic.php');
// https://deployer.org/docs/7.x/recipe/statamic

set('application', 'My Statamic Site');

// The git repository which should be used
set('repository', 'git@github.com:mandrasch/my-statamic-site.git');

// Targets for deployment (SSH),
// this will be called via 'ddev dep deploy production'
host('production')
    ->set('remote_user', 'ploi')
    ->set('hostname', '123.456.779.1')
    ->set('deploy_path', '~/my-statamic-site.mandrasch.dev');

// Sharead and writable directories
// (These will be kept on the server and will not be replaced on deployments)
add('shared_dirs', [
    'content',
    'storage'
]);
add('writable_dirs', [
    'content',
    'storage'
]);

// Npm production build
desc('Compile JS/SCSS via npm');
task('npm:production', function(){
    run('cd {{release_path}} && npm ci && npm run production');
});


// We copied the following from Statamic recipe to know what's going on
// (https://deployer.org/docs/7.x/recipe/statamic)
/*
 * Main Deploy Script for Statamic, which
 * will overwrite the Laravel default.
 */
desc('Deploys your project');
task('deploy', [
    'deploy:prepare',
    'deploy:vendors',
    // TODO: good spot here?
    'npm:production',
    'artisan:storage:link',
    'artisan:cache:clear',
    'statamic:stache:clear',
    'statamic:stache:warm',
    'deploy:publish',
]);

after('deploy:failed', 'deploy:unlock');

```

To use the `dep` cli command of Deployer in DDEV, we need a little custom command add-on: 

```
ddev get mandrasch/ddev-deployer-dep
```

## Try Deployer workflow locally

Now we need to set the connection settings in `deploy.php` accordingly to the information shown in ploi, the user is always "ploi" and the IP address is shown in the dashboard:

```php
host('production')
    ->set('remote_user', 'ploi')
    ->set('hostname', '123.456.789.123')
    ->set('deploy_path', '~/my-statamic-site.mandrasch.dev');
```

To connect to the server via SSH, you need to add your **local** public SSH key to your ploi profile:

![Add SSH key to ploi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66nmsw014b5ukurhb63z.png)

On Mac you'll get your public key via 

```bash
cat ~/.ssh/id_rsa.pub
```

Paste it in and send it to your servers:

![Screenshot add ssh key to server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v7nokiw1iuwpoc3d40lq.png)
 
The important part now is to add your local SSH keys to your DDEV project containers as well. This is just a quick command:

```bash
ddev auth ssh
```

Afterwards we can try our first Deployer run locally via DDEV:

```bash
ddev dep deploy production
```

Deployer will connect to your target server via SSH, clone the repository from Github and run the rest of the routine described in `deploy.php`. For example Deployer will run the npm production build and setup the folder structure (all via SSH on the target server).

## Add servers SSH key to GitHub

The SSH connection from DDEV to the server should work by now, but the private git repository can't be accessed (of course): 

```
[production] git@github.com: Permission denied (publickey).
[production] fatal: Could not read from remote repository.
[production] Please make sure you have the correct access rights
```

Therefore we need to add the SSH key of the **target** server (the ploi/hetzner server!) to Github. 

We need to connect to the ploi server via `ssh ploi@XXX.XXX.XXX` and use the command `cat ~/.ssh/id_rsa.pub` again. Add this key to the Deploy Keys of your private repository on github.com:

![Screenshot Deploy Keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8tggc0yd3u4j4xtty7l.png)
 
You should now have a success moment after running `ddev dep deploy production`:

![Screenshot command line output successful run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikguy1r3qf9vemhwkbaq.png)

Now only the `.env` file must be configured. We can use SSH (or you could use an FTP program), the file is located in `shared/.env`. 

Unfortunately - while I was testing the "Edit environment" feature of [Plois Laravel Tab](https://ploi.io/laravel-optimized) did not work with this Deployer setup. Make sure to set the project directory to `current/` anyhow to use the artisan commands:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/elulavop82e12guryxs8.png)
 
## Show the live site!

One last thing to do, change the web directory to `current/public`, this is where the final site get's served from: 

![Screenshot change web directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yt8mnnnt57pb41qdnuhj.png)
 
Find the "View test" link and open your site in the browser. Visit `/cp` and add a home page:

![Screenshot add home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9js51kv33u63d2fobaru.png)

Now you should have a functioning home page:

![Screenshot Hello World place holder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43r8ec3a9ccqa8sl62si.png)

## Deployer folder structure

Deployer uses symlinks and configures the following structure for us, it's called zero downtime deployment because the new versions are switched in via symlink after they are ready:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/28lfgihaggh7f2l5ou1m.png)

The `current/`-folder:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrr7v48cb6lmglrkmcpl.png)

The `shared/`-folder, this will be kept during deployment changes:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdpeuliuxd1vism9a421.png)

## GitHub Action for Deployer

You might be wondering: Yeah, nice. But do I have to run the deploy every time locally? No, this is just for testing. Later in this series we can add the GitHub deploy action which runs on github (automatically for every commit in a certain branch)

https://github.com/deployphp/action

## Monitoring

Ploi has monitoring in the higher-priced subscription, but you can view the server load as well as server traffic in the Hetzner Cloud dashboard:

![Screenshot hetzner cpu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1denckrfrbcyxngs9e94.png)

## Build everything in the pipeline instead?

There is a great tutorial for typo3: [TYPO3 Gitlab CI/CD Auto Deployment (2022)](https://t3terminal.com/blog/typo3-gitlab-deployment/), the corresponding `deploy.php` can be found [here](https://git.typo3.org/services/t3o-sites/typo3.org/typo3.org/-/tree/develop/.gitlab-ci/deployer). What is the advantage? Instead on running `composer install` and `npm run production` on the target server, everything is build beforehand in the Github/Gitlab/Bitbucket action pipeline and - only if successful - transferred via rsync to the server. I'm not completely sure yet but it seems to be a safer approach since the target server will be only 'touched' if the build steps before were 100% successful.

(There is also no `git checkout` anymore on the server itself and there is no need to add the Github SSH key to the target server.)

Hope I have time to integrate this approach for Statamic/Laravel as well.

## More resources:

- See [How do I connect with SSH? (Mac & Linux)](https://ploi.io/documentation/ssh/how-do-i-connect-with-ssh-mac-linux) and [How do I connect with SSH? (Windows)](https://ploi.io/documentation/ssh/how-do-i-connect-with-ssh-windows) for more information on SSH
- Good series as well, but things are done there slightly different https://lorisleiva.com/deploy-your-laravel-app-from-scratch
- [Statameet 2021 | Zero-Downtime deployment (Jonas Siewertsen)](https://www.youtube.com/watch?v=Dsld-2iHv2k), slightly different as this workflow as well
- My notes for DDEV & Statamic: https://my-ddev-lab.mandrasch.eu/tutorials/cms-and-frameworks/statamic.html

## Coming soon - next articles: 

- Article about DDEV pull via rsync to get the production site data for local development
- Article about BrowserSync support in DDEV (https://github.com/tyler36/ddev-browsersync#laravel-mix-example) for LaravelMix or Vite (https://stackoverflow.com/questions/72130137/ddev-laravel-vite-websockets-with-soketi-trouble-with-port-configuration/72405747)