I have been using Dokku for years to host personal sites and pet projects on a cheap web-server. Dokku allows you to deploy your projects through git-ops. The experience is very similar to deploying to other git-ops platforms like Heroku. This had been working smoothly for my previous personal blog which was a Wordpress app. When I decided to switch my personal site to Ghost I wasn't willing to sacrifice my Dokku setup. This made me figure out how to deploy a Ghost application to Dokku.

The main difference between my Wordpress setup and Ghost, is that there is no code repository managed by Git for my Ghost setup. Since Dokku is based on git-ops there is no clear way to deploy Ghost to a Dokku environment.

Fortunately Dokku allows you to directly deploy Docker images directly, be it a bit of an obscure feature, it works remarkably well. Directly deploying a Docker image gives us the advantage of not having to create a custom code repository for our Ghost deployment, saving us time spent on maintenance.

  1. Basic setup
  2. Mysql
  3. NGINX cache

Basic Setup

Prerequisites: I assume you have a server running Dokku, if you don't it is quite easy to spin one up on Digitalocean. This guide is tested against Dokku v0.14.6

Start by creating a new Dokku application

$ dokku apps:create my-ghost-app

Once you have an application setup you can pull in the latest Ghost docker image

$ docker pull ghost:latest

For Dokku to recognize a docker image from your local repository it should be name dokku/name-of-app:tag. So let's tag our Docker image with the correct name:

$ docker tag ghost:latest dokku/my-ghost-app:latest

Now that we have tagged the Docker image we can deploy it:

dokku tags:deploy my-ghost-app latest

We should now have a docker container that is running Ghost (you can check by issuing the command docker ps). But before we can reach our Ghost application from the outside world we need to expose it's port:

$ dokku proxy:ports-add my-ghost-app http:80:2368

This command tells Dokku to route all incoming traffic on port 80 to port 2368 inside our Ghost Docker container.

Since Docker containers are ephemeral and do not persist across restarts we should mount a storage volume to make sure we do not lose any content when that happens.

$ dokku storage:mount my-ghost-app /opt/my-ghost-app/ghost/content:/var/lib/ghost/content

And create the directory.

$ mkdir -p /opt/my-ghost-app/ghost/content

Now configure one of Ghost's environment variables to instruct it for which Host it can receive traffic:

$ dokku config:set my-ghost-app url=http://my-ghost-app.com

Since this domain is not known by the Nginx server operated by Dokku we need to add it through the Dokku command-line:

$ dokku domains:add my-ghost-app my-ghost-app.com

This should be enough for your blog to be running. Now you can start using you Ghost deployment. Or you could leverage any of the other Dokku features to for instance improve your deployment's security.

Bonus: uploading large files

By the default the Nginx proxy created by Dokku will not allow you to upload 'large files'. We can update the Nginx configuration to support these files. To do this we need to access the server running Dokku with ssh, and create the following file:

$ echo 'client_max_body_size 50M;' > /home/dokku/APP_NAME/nginx.conf.d/upload.conf

Where you replace APP_NAME with the name of your Ghost Dokku app. After this we need to reload the Nginx service and we should be good to go:

$ service nginx reload

Troubleshooting In the case something is wrong you can troubleshoot by looking at the Docker container logs, through $ dokku logs my-ghost-app. If this gives you zero output please check that the Ghost docker image is actually running $ docker ps.

Mysql

By default Ghost will persist all content to a sqllite database. This setup requires very little effort to get up and running but will not outperform a persistence layer backed by Mysql.

Start by creating a new database:

$ dokku mysql:create my-ghost-db
Create a new Mysql database

Now link this database to your app:

$ dokku mysql:link my-ghost-db my-ghost-app
Link the database to your app, this will populate the DATABASE_URL env variable.

Finally you need to pass the database__connection environment variable to Ghost. (replace mysql://... with the url printed out by the previous command)

$ dokku config:set my-ghost-app\ 
   database__client=mysql\
   database__connection=mysql://...

That's it. After restarting, your Ghost is running on a Mysql datastore instead of Sqlite.

NGINX cache

If you are running Ghost for your personal blog, chances are most posts wont update that frequently. So it is probably a good idea to let nginx cache the output generated by Ghost, this will reduce load on the NodeJS instance(s) in charge of rendering. Unfortunately Ghost sends the following Cache-Control header public, max-age: 0 which disallows Nginx to cache html output (by default).

To solve this we will need to deploy Ghost with a custom nginx configuration, instructing Ghost to cache any public content.

$ git init
Create a new repository

Add the following Dockerfile to this new repository:

FROM ghost:3.1.1

ADD nginx.conf.sigil .
Dockerfile

Now get the base nginx config template from the Dokku repository. Be sure to pick the configuration template for the version of Dokku that is running on your server.

We first need to instruct NGINX where it can store cached content on the file system. To do this add the following line above the first server block:

proxy_cache_path /tmp/ghost_cache levels=1:2 keys_zone=ghost_cache:10m max_size=500m inactive=24;
This line should appear once, top-level in the nginx config.

Now we can activate the cache for the location block /:

# Ignore the cache-control header sent by Ghost
proxy_ignore_headers Cache-Control;
# Add header for cache status (miss or hit). This allows you to debug whether the cache is working.
add_header X-Cache-Status $upstream_cache_status;

proxy_cache ghost_cache;
# Default TTL: 1 day
proxy_cache_valid 1d;
# Cache 404 pages for 1h
proxy_cache_valid 404 1h;
# use conditional GET requests to refresh the content from origin servers
proxy_cache_revalidate on;
proxy_buffering on;
# Allows starting a background subrequest to update an expired cache item,
# while a stale cached response is returned to the client.
proxy_cache_background_update on;
# Bypass cache for errors
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
These options enable caching for a Nginx location.

This will enable caching for all locations, we probably do not want to cache the admin pages, these can be disabled by adding the following block nested inside the root location block:

location /ghost {
  proxy_pass  http://{{ $.APP }}-{{ $upstream_port }};
  proxy_cache off;
}
Disable caching for the Ghost admin pages.

If you support both HTTP and HTTPS (which you should), be sure to duplicate the process for the HTTPS part of the configuration.

Add all the files to your git repository and create a commit. Now add my-ghost-app as a remote to your git repository:

$ git remote add dokku dokku@yoursite.com:my-ghost-app

Finally push up your changes to the remote. Dokku's git-ops should take care of rolling out your application.

As a reference I have uploaded my setup to Github (notice that this NGINX config template is written for Dokku 0.14): https://github.com/matthisk/matthisk.com

References