How to install Contenta headless Drupal on a Digital Ocean droplet

April 07, 2018

I’m working on a project that uses Drupal as a data source for a Gatsby site, and I needed a clean Drupal instance for testing. Contenta is a headless Drupal distribution that comes with sensible defaults and some useful demo content, and it seems to fit the bill. It’s even the sample used in the Gatsby “using-drupal” demo, so that’s a good start.

Installing it is generally a straightforward process, but the documentation does assume a level of Drupal knowledge that I don’t possess, so it took me a bit of time to work out. I can’t be the only person in that situation (one of the goals of Contenta is apparently to be friendly to non-Drupal peole), so I thought I’d share how I did this.

install Drupal normally Err, thanks

This is specific to Digital Ocean, but with a little tweaking it should work on other Ubuntu or Debian hosts.

Contenta offers a nice bootstrapping script that installs a local development server, but out of the box this doesn’t work with Gatsby, as there are some errors in some of the content types. This can be fixed by disabling those types in the JSON API and clearing the caches, but I need a proper install so we’ll do this this long way.

We’ll get a headstart by using the Digital Ocean one-click LEMP app. I’m going to start with the smallest available droplet, and then increase the memory. The composer install scripts choke on 1GB of RAM. 2GB might work (I haven’t tried), but I’m going with 4GB as I know that works, and will resize once it’s all installed. If you start with the 4GB droplet straight away (rather than resizing a 1GB one) then you’ll have an 80GB SSD, meaning you can’t downsize later, so start with 1GB, resize it to 4GB while you install, then resize back to 1GB after.

I won’t go into detail of setting-up the droplet, as that’s covered by others. Just add your ssh key, boot it up, ssh in and we’re ready to go.

You should have a running LEMP stack out of the box: try opening the IP address in your browser and you should see a shark. Most of the packages are already installed, but there are a few extras that Drupal needs, so let’s install them. I’m assuming you are running as root, because that’s what you’ll be when you first login. If you’re using this for anything serious, make sure to create a user account to work with. You’ll need to prefix most of these commands with sudo if you do.

apt update
apt install php-dom php-gd php-mbstring zip composer

Now we need to install Contenta using Composer.

composer create-project contentacms/contenta-jsonapi-project /var/www/contenta --stability dev --no-interaction

Go and make a coffee, as this will take some time. Once that is done, we need to make some config changes to NGINX. I’ve used the NGINX Drupal recipe. Paste the following into /etc/nginx/sites-available/contenta:

server {
    server_name xx.xx.xx.xx; ## Replace with your IP address or hostname
    root /var/www/contenta/web; ## <-- Your only path reference.

    location = /favicon.ico {
        log_not_found off;
        access_log off;

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        deny all;

    location ~ \..*/.*\.php$ {
        return 403;

    location ~ ^/sites/.*/private/ {
        return 403;

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;

    location / {
        # try_files $uri @rewrite; # For Drupal <= 6
        try_files $uri /index.php?$query_string; # For Drupal >= 7

    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;

    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;

    # In Drupal 8, we must also match new paths where the '.php' appears in
    # the middle, such as update.php/selection. The rule we use is strict,
    # and only allows this pattern with the update.php front controller.
    # This allows legacy path aliases in the form of
    # blog/index.php/legacy-path to continue to route to Drupal nodes. If
    # you do not have any paths like that, then you might prefer to use a
    # laxer rule, such as:
    #   location ~ \.php(/|$) {
    # The laxer rule will continue to work if Drupal uses this new URL
    # pattern with front controllers other than update.php in a future
    # release.
    location ~ '\.php$|^/update.php' {
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        # Security note: If you're running a version of PHP older than the
        # latest 5.3, you should have "cgi.fix_pathinfo = 0;" in php.ini.
        # See for details.
        include fastcgi_params;
        # Block httpoxy attacks. See
        fastcgi_param HTTP_PROXY "";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_intercept_errors on;
        # PHP 5 socket location.
        #fastcgi_pass unix:/var/run/php5-fpm.sock;
        # PHP 7 socket location.
        fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;

    # Fighting with Styles? This little gem is amazing.
    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6
    location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7
        try_files $uri @rewrite;

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
        try_files $uri /index.php?$query_string;

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;

Then enable your new site and reload NGINX:

rm /etc/nginx/sites-enabled/digitalocean
ln -s /etc/nginx/sites-available/contenta /etc/nginx/sites-enabled/
nginx -s reload

Let’s set up MySQL. You can find the default root password in /root/.digitalocean_password.

mysql -p -u root

mysql> create database contenta;
mysql> grant all on contenta.* to contenta@localhost identified by 'areallylongautogeneratedpasswordthatsnotthesameasroot';

Keep track of that password. We’ll need it in a minute.

It makes me cringe, but we’re going to use the web interface to configure Drupal, so we need to make the settings file writeable by the web server. Change it back afterwards!

chown www-data /var/www/contenta/web/sites/default/settings.php

Now open the site in your browser again and you should see something like this:

configure contenta

Enter contenta for the database name and username, and use the password you created in the previous step (not the root password).

Save, and wait for Drupal to set itself up. Maybe make another coffee.

When that is done it’ll let you create your admin account. Save that and if all has gone well it should take you to the Contenta homepage, logged-in and ready to create content! Try using gatsby-source-drupal to build a nice React-based static site with it. Don’t forget to resize your droplet if you don’t need the full 4GB of memory, and it’s best to chown your settings.php back to another user so it can’t be written by the server user.

I'm Matt Kane. I've made high-speed flashes and beekeeping software, but I mostly spend my time making web and mobile apps with React and TypeScript. Follow me on Twitter and Github.