Jan 24, 2019

Set up Caddy and PHP-fpm on Ubuntu 18.04

Caddy is a new webserver with very interesting features, like automatic HTTPS or serving markdown files and turning them into HTML on the fly. It's written in Go, so you get a complete binary without any other system dependencies.

It will replace Nginx or Apache in your stack. I understand it's scary to use something else that Nginx, but automatic on-demand SSL certificate me switch! Only by turning on a feature, Caddy will get a certificate via Let's Encrypt automatically. All certificates are renewed automatically when necessary.

I'm working on an app where every user can bind a domain and have their own public site. I need to get SSL certificate for all domains easily, and Caddy offers just that. You can even plug it to your app so Caddy will check if a domain is allowed before turning on TLS.

This posts focus on how to install Caddy with PHP-fpm on Ubuntu 18.04. I'll keep this posts focused on the install process so I can reference it in other posts about Caddy. My app is built with Laravel so in another post, I'll write about how to handle:

  • On-demand SSL certificate with domain validation
  • Running a Laravel app with Caddy

Make sure you have a domain pointing there, ideally with another subdomain: example.com, www.example.comand php.example.com.

Setup server basics

I usually start by install some utilities I know I always need. When sudo asks what configuration file to use, I always take the maintainers's one.

1apt install -y vim tree sudo zip unzip sqlite members libcap2-bin

I don't like running things as root so I create a new julien user . This user is used to maintain the server, check logs and other things.

1useradd -m -G sudo -s /bin/bash julien
2passwd julien

By default, users can't login because they have no associated password. It's actually good because all the other users we'll create will be used to run software and shouldn't be used to log in with. For this one, we needed to create a password.

Install Caddy

Now we're going to install Caddy. It has 2 licenses available, I'm going to install the Personal one for this tutorial, but please look if you need the commercial license.

I'd also recommend to go on the website and have look at all the other plugin available, in case some are relevant for you, add them to the list.

1curl https://getcaddy.com | bash -s personal http.cache,http.cors,http.expires,http.geoip,http.git,http.ipfilter,http.locale,http.nobots,http.ratelimit,http.realip

service status

Caddy will run with its own user, we won't log in with it so we create it without shell.

1sudo useradd -M -s /bin/false caddy

Caddy will need some folder with the correct rights to store configuration and logs.

1sudo mkdir -p /etc/caddy
2sudo mkdir -p /var/log/caddy
3sudo chown -R caddy:root /etc/caddy /var/log/caddy

Caddy will need to listen on port 80 and 443, which are the standard port for HTTP and HTTPS. There is one easy way to give a program the rights to bind to port less than 1024 without being root. In the very first step we installed libcap2-bin, it's time to use it.

1sudo setcap cap_net_bind_service=+ep /usr/local/bin/caddy

Not that if you ran the previous steps directly as root, caddy might be located in /usr/bin/caddy instead.

Setup example website

Before going further, we want to make sure everything works. We'll make a little "It works!" Html page and serve it with caddy.

Create a /var/www/demo/index.html file

1<!doctype html>
2 
3<html lang="en">
4<head>
5 <meta charset="utf-8">
6 <title>Caddy</title>
7</head>
8 
9<body>
10 <h1>It works!</h1>
11</body>
12</html>

Let's create our first configuration! We need to create a /etc/caddy/CaddyFile with the following configuration.

1mkwp.org {
2 redir https://www.mkwp.org{uri}
3}
4 
5www.mkwp.org {
6 root /var/www/demo
7 log /var/log/caddy/mkwp.org.log
8 tls off
9 gzip
10}

The first part will redirect the base domain to the www subdomain. Inside the second blog, we define repectively: * where the site lives on the server * where to store the logs * to not use SSL (yet) * to use gzip to compress your responses

The site is ready to be server.

Run Caddy as a deamon

We'll use system.d to run caddy. It makes it easy to run with the correct user, and easy to restart.

Create a caddy deamon definition in /etc/systemd/system/caddy.service . If you decided to store your logs or configuration somewhere else, remember the change the different paths in this file.

1[Unit]
2Description=Caddy HTTP/2 web server
3
4[Service]
5User=caddy
6Group=caddy
7Environment=CADDYPATH=/etc/caddy
8ExecStart=/usr/local/bin/caddy -agree=true -log=/var/log/caddy/caddy.log -conf=/etc/caddy/Caddyfile -root=/dev/null
9ExecReload=/bin/kill -USR1 $MAINPID
10LimitNOFILE=1048576
11LimitNPROC=64
12
13[Install]
14WantedBy=multi-user.target

Now, reload the configuration, start the service and check its status.

1sudo systemctl daemon-reload
2sudo systemctl start caddy
3sudo systemctl status caddy

service status

Make sure the deamon is started with the server is rebooted:

1sudo systemctl enable caddy
1julien@scw-optimistic-leakey:/etc/caddy$ sudo systemctl status caddy
2● caddy.service - Caddy HTTP/2 web server
3 Loaded: loaded (/etc/systemd/system/caddy.service; enabled; vendor preset: enabled)
4 Active: active (running) since Mon 2019-01-21 22:18:20 CET; 19min ago
5 Main PID: 14812 (caddy)
6 Tasks: 12 (limit: 2295)
7 CGroup: /system.slice/caddy.service
8 └─14812 /usr/local/bin/caddy -agree=true -log=/var/log/caddy/caddy.log -conf=/etc/c
9
10Jan 21 22:18:31 scw-optimistic-leakey caddy[14812]: 2019/01/21 21:18:31 [INFO] [www.mkwp.org]
11Jan 21 22:18:31 scw-optimistic-leakey caddy[14812]: 2019/01/21 21:18:31 [INFO] [www.mkwp.org]
12Jan 21 22:18:37 scw-optimistic-leakey caddy[14812]: 2019/01/21 21:18:37 [INFO] [www.mkwp.org]
13Jan 21 22:18:37 scw-optimistic-leakey caddy[14812]: 2019/01/21 21:18:37 [INFO] [www.mkwp.org]
14Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: 2019/01/21 21:18:40 [INFO] [www.mkwp.org]
15Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: done.
16Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: https://mkwp.org
17Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: https://www.mkwp.org
18Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: http://mkwp.org
19Jan 21 22:18:40 scw-optimistic-leakey caddy[14812]: http://www.mkwp.org

It works!

Head to your website, you should see your "it works" message.

It works over HTTP

Set up SSL certificate

If we chose Caddy, it was to serve over HTTPS hassle-free, so let's get into it.

Open up your Caddyfileconfiguration, remove the http:// to only leave the domain and turn on TLS.

1mkwp.org {
2 redir https://www.mkwp.org{uri}
3}
4 
5www.mkwp.org {
6 root /var/www/demo
7 log /var/log/caddy/mkwp.org.log
8 tls on
9 gzip
10}

Restart your caddy deamon sudo systemctl restart caddy. Head back to your browser, refresh the page, and that's it. You're serving over HTTPS!

It works over SSL

I told you it was easy ? In a next post, I'll show you how to get certificates on-demand. Typically, if your app let the user bind an external domain to your service, you can get certificates for all of them.

Install PHP 7.2 FPM (optional)

If you don't use PHP, feel free to skip this step and go on with your life.

1sudo apt install -y curl php7.2-fpm php7.2-cli php7.2-mysql php7.2-zip php7.2-mbstring php7.2-dom php7.2-bcmath

Very often, you run many website on the same server. Like your app, your blog, the documentation website, some special marketing mini-site and so one. Some people create one user (typically www-data) to run all their site with it, but it's generally recommended to create one per site. It helps isolate your website, in case one get hacked, it won't affect the other site next to it.

Let's create a new phpdemo user to run simple phpinfo() website.

1sudo useradd -M -s /bin/false phpdemo

Create FPM configuration

Create a new configuration file for php-fpm inside the pool.dfolder. I call the file according to the unix user, so in /etc/php/7.2/fpm/pool.d/phpdemo.conf.

1[phpdemo]
2 
3user = phpdemo
4group = phpdemo
5 
6listen = /run/php/php7.2-fpm.phpdemo.sock
7 
8listen.owner = phpdemo
9listen.group = phpdemo
10listen.mode = 0660
11 
12pm = dynamic
13pm.max_children = 5
14pm.start_servers = 2
15pm.min_spare_servers = 1
16pm.max_spare_servers = 3

With listen.owner and listen.group we specified which unix user is going to own the socket (the php7.2-fpm.phpdemo.sock file). In order to ensure that caddy can access it, we'll add the caddy user to the phpdemo group.

1sudo gpasswd -d caddy phpdemo

Like we did for Caddy, the daemon needs to be reloaded, started and enabled. We restart caddy, just in case.

1sudo systemctl daemon-reload
2sudo systemctl restart php7.2-fpm
3sudo systemctl enable php7.2-fpm
4sudo systemctl restart caddy

php fpm status

Create Caddy configuration

Instead of simply modifying the original Caddyfile we created earlier, we'll rename it to mkwp.org and create a new Caddyfile that imports it and a new file for php.

We could simply put everything in the same file, but in the long run, I prefer splitting it.

Our new Caddyfile will look like

1import mkwp.org
2import php.mkwp.org

Create the php.mkwp.org file.

1php.mkwp.org {
2 root /var/www/phpdemo/
3 log /var/log/caddy/php.mkwp.org.log
4 errors /var/log/caddy/php.mkwp.org.error.log
5 tls on
6 gzip
7 
8 fastcgi / /run/php/php7.2-fpm.phpdemo.sock php
9}

Looking at the fastcgi line, you'll see the php keyword. It's a shorthand, if you want to be more explicit you can replace it by the following block. Feel free to edit any value if necessary.

1fastcgi / /run/php/php7.2-fpm.phpdemo.sock {
2 ext .php
3 split .php
4 index index.php
5 }

We also added a directive for error logs (since it's php and not simple HTML). Restart caddy sudo systemctl restart caddyto take this new config into account.

Create php demo site

Create a simple /var/www/phpdemo/index.php and change the ownership to use the new user we created earlier sudo chown -R phpdemo:phpdemo /var/www/phpdemo.

For this type of test, I typically display the content of phpinfo().

1echo '<?php phpinfo();' > /var/www/phpdemo/index.php

PHP Info for PHP 7.2

It's done! If you have issues, please let me know here or on twitter @julienbourdeau, if I can I'll update this post with a troubleshooting section or at least more details.