Deploy a Ghost Blog as Back-End Docker Service
About • Built With • Prerequisites • Testing • Deployment • Usage • Contributing • Credits • Donate • License
Ghost is a popular open source Content Management System (CMS) based on Node.js. It was founded in 2013 and has seen more than 2 million installations to date. The team behind Ghost offers a managed service that gets you started in minutes. However, in this guide, we will be looking into self-hosting Ghost as a back-end service on a Virtual Private Server (VPS). The final configuration aims to be both secure and scaleable.
The project uses the following core software components:
- Docker - Container platform (including Swarm and Compose)
- Ghost - Content Management System
- HTTPS Portal - Fully automated HTTPS server
- MariaDB - Community-developed fork of MySQL relational database
- Mysqldump - Open-source tool provided by MariaDB to export a database
- Restic - Backup program with cloud storage integration
Ghost-backend can run on a local machine for testing purposes or in a production environment. The setup has been tested locally on macOS and a Virtual Private Server (VPS) running Ubuntu 20.04 LTS. The cloud backup functionality has been tested with Backblaze B2.
Ghost is a relatively light-weight application that requires 1 GB of memory.
Most VPS providers offer several Linux distributions to be installed on your VPS. Although Docker and Ghost are compatible with many of them, Ghost recommends Ubuntu 16.04 LTS or Ubuntu 18.04 LTS. The Long Time Support (LTS) edition is the most stable version and is the recommended environment for a production system.
-
A registered domain name is required - Not only will this help people to find your blog, but it is also required for configuring SSL certificates to enable secure traffic via https. You should have the ability to manually configure DNS entries for your domain too.
-
Docker Compose and Docker Swarm are required - Ghost and the MariaDB Database will be deployed as Docker containers in swarm mode to enable Docker secrets. This repository provides a script to harden the host and to deploy Docker securely.
-
A (cloud) backup service is highly recommended - To enable versioning and disaster recovery, an offsite backup is highly recommended. Restic provides integrations for Amazon S3, Minio Server, Openstack Swift, Backblaze B2, Microsoft Azure Blob Storage, and Google Cloud Storage. Other methods include SFTP, REST Server, or rclone. This guide uses Backblaze B2 as an example.
- An email service is optional - Having an email service allows you to receive system notifications from Ghost.
It is recommended to test the services locally before deploying them to production. Running the service with docker-compose
greatly simplifies validating everything is working as expected. Below four steps will allow you to run the services on your local machine and validate it is working correctly.
The first step is to clone the repository to a local folder. Assuming you are in the working folder of your choice, clone the repository files. Git automatically creates a new folder ghost-backend
and copies the files to this directory. The option --recurse-submodules
ensures the embedded submodules are fetched too. Change to your working folder once done to be prepared for the next steps.
git clone --recurse-submodules https://github.com/markdumay/ghost-backend.git
cd ghost-backend
As docker-compose
does not support external Swarm secrets, we will create local secret files for testing purposes.
mkdir secrets
printf password > secrets/db_root_password
printf ghost > secrets/db_user
printf password > secrets/db_password
printf ghost_backup > secrets/db_backup_user
printf password > secrets/db_backup_password
printf password > secrets/restic_password
Next, you will need to configure the tokens required to connect with your cloud storage provider. The below example defines the tokens for Backblaze B2. Ghost-backend automatically stages any Docker secret starting with the prefix STAGE_
. In below example, STAGE_B2_ACCOUNT_ID
becomes available as B2_ACCOUNT_ID
for restic. This link provides an overview of the tokens required for each supported cloud provider. Be sure to replace XXX
with the correct values.
printf XXX > secrets/STAGE_B2_ACCOUNT_ID
printf XXX > secrets/STAGE_B2_ACCOUNT_KEY
The docker-compose.yml
file uses environment variables to simplify the configuration. You can use the sample file in the repository as a starting point.
mv sample.env .env
It's convenient to use a
.test
top-level domain for testing. This domain is reserved for this purpose and is guaranteed not to clash with an existing domain name. However, you will still need to resolve these domains on your local machine. Steven Rombauts wrote an excellent tutorial on how to configure this usingdnsmasq
on macOS.
The .env
file specifies eleven variables. Adjust them as needed:
Variable | Default | Description |
---|---|---|
DOMAINS_BLOG | example.test |
Defines the domain name of your blog. Exclude the http:// and https:// protocols. |
DOMAINS_ADMIN | admin.example.test |
Defines the admin domain name of your blog. Exclude the http:// and https:// protocols. |
DB_NAME | ghost |
The name of the database to be used by Ghost and MariaDB. |
DB_USER | ghost |
The name of database user to be used by Ghost when connecting with MariaDB. Ensure it is the same value as the secret db_user . |
ADMIN_EMAIL | admin@example.test |
Email address for notifications from Ghost and Let's Encrypt. |
THEMES | true |
Indicates whether the default Ghost theme (Casper) should be installed. |
BACKUP | remote |
Indicates whether to schedule backups automatically. Settings can be either none for no backups, local for local backups only, or remote for both local and remote backups. |
RESTIC_REPOSITORY | b2:bucketname:/ |
The storage provider and bucket name of the remote repository. For Backblaze B2, the full identifier is b2:bucketname:path/to/repo . The identifier for other storage providers can be found here. |
GHOST_HOST | ghost:2368 |
Specifies the localhost and port of the Ghost server. The default port is 2368. |
STAGE | local |
Instructs HTTPS Portal to request certificates from Let's Encrypt when set to production . When set to local , HTTPS Portal installs self-signed certificates for local testing. |
CACHING | true |
Instructs Nginx to cache static files such as images and stylesheets if set to 'true'. The admin portal remains uncached at all times. |
Test the Docker services with docker-compose
.
docker-compose up
After pulling the images from the Docker Hub, you should see several messages. Below excerpt shows the key messages per section.
During boot, Ghost-backend enables the local and remote backups in line with the BACKUP
setting (see Step 3). First, the cron job using mysqldump
for local backups is scheduled 30 minutes past every hour. Next, the latest restic
binary is downloaded and installed (mysqldump
is already present in the parent's Docker image provided by MariaDB). Once restic is installed, it is scheduled to run 45 minutes past every hour. Restic compares the local files with the latest snapshot available in the repository. If needed, it updates the remote repository automatically using restic_password
as encryption password (see Step 2). In the background, old restic snapshots are removed daily at 01:15 am. Restic also updates itself at 04:15 am if a new binary is available. Finally, the cron daemon is fired up.
mariadb_1 | [Note] Enabling local and remote backup
mariadb_1 | [Note] Adding backup cron job
mariadb_1 | [Note] View the cron logs in '/var/log/mysqldump.log'
mariadb_1 | [Note] Installed restic 0.9.6 compiled with go1.13.4 on linux/amd64
mariadb_1 | [Note] Adding restic cron jobs
mariadb_1 | [Note] View the cron log in '/var/log/restic.log'
mariadb_1 | [Note] Initialized cron daemon
Once the backup jobs are scheduled, the MariaDB database is initialized. MariaDB starts as a temporary server, creates the Ghost database schema, and gives the required privileges to the designated ghost
database user. A user ghost_backup
is created for the mysqldump
cron job scheduled in the previous section as well. The actual MariaDB server is started once the initialization is done.
mariadb_1 | [Note] [Entrypoint]: Entrypoint script for MySQL Server 1:10.3.22+maria~bionic started.
mariadb_1 | [Note] [Entrypoint]: Temporary server started.
mariadb_1 | [Note] [Entrypoint]: Creating database ghost
mariadb_1 | [Note] [Entrypoint]: Creating user ghost
mariadb_1 | [Note] [Entrypoint]: Giving user ghost access to schema ghost
mariadb_1 | [Note] Creating mariadb backup user 'ghost_backup' for database
mariadb_1 | [Note] [Entrypoint]: MySQL init process done. Ready for start up.
With the database properly initialized, MariaDB can start accepting connections. The default port is 3306
.
mariadb_1 | [Note] mysqld: ready for connections.
mariadb_1 | Version: '10.3.22-MariaDB-1:10.3.22+maria~bionic' socket: '/var/run/mysqld/mysqld.sock' port: 3306 [...]
The docker-compose
configuration instructs Ghost to wait for the database to become available on port 3306
. Once the database is available, Ghost will create and populate all tables, models, and relations in the first run.
ghost_1 | docker-compose-wait - Everything's fine, the application can now start!
ghost_1 | INFO Creating table: [...]
ghost_1 | INFO Model: [...]
ghost_1 | INFO Relation: [...]
Once the data is available, Ghost will start running in production mode. Typically the initial run takes up to a minute. The boot time is drastically reduced when reconnecting to an existing database. You can now access Ghost at http://example.test
and set up your (administrative) user(s).
ghost_1 | [2020-06-17 11:45:40] INFO Ghost is running in production...
ghost_1 | [2020-06-17 11:45:40] INFO Your site is now available on http://example.test
ghost_1 | [2020-06-17 11:45:40] INFO Ctrl+C to shut down
ghost_1 | [2020-06-17 11:45:40] INFO Ghost boot 24.849s
The reverse proxy maps the public URLs to the local Ghost service. The main blog is available at example.test
. By default, the www.example.test
subdomain is redirected to example.test
too. The subdomain admin.example.test
redirects to Ghost's admin portal available at example.test/ghost/
. If the variable CACHING
is set to true
, all Ghost content is cached except for the admin portal. The certificates are self-signed by default, which can be changed to trusted certificates by setting STAGE
to production
.
portal_1 | [2020-06-24 04:43:20] INFO Enabling caching
portal_1 | Generating DH parameters, 2048 bit long safe prime, generator 2
portal_1 | Self-signing test certificate for example.test
portal_1 | Self-signing test certificate for www.example.test
portal_1 | Self-signing test certificate for admin.example.test
portal_1 | [services.d] starting services
portal_1 | [services.d] done.
The steps for deploying in production are slightly different than for local testing. Below four steps highlight the changes compared to the testing walkthrough.
Unchanged
Instead of file-based secrets, you will now create secure secrets. Docker secrets can be easily created using pipes. Do not forget to include the final -
, as this instructs Docker to use piped input. Update the credentials as needed.
printf password | docker secret create db_root_password -
printf ghost | docker secret create db_user -
printf password | docker secret create db_password -
printf ghost_backup | docker secret create db_backup_user -
printf password | docker secret create db_backup_password -
printf password | docker secret create restic_password -
printf XXX | docker secret create STAGE_B2_ACCOUNT_ID -
printf XXX | docker secret create STAGE_B2_ACCOUNT_KEY -
If you do not feel comfortable copying secrets from your command line, you can use the wrapper create_secret.sh
. This script prompts for a secret and ensures sensitive data is not displayed in your console. The script is available in the folder /docker-secret
of your repository.
./create_secret.sh db_root_password
./create_secret.sh db_user
./create_secret.sh db_password
./create_secret.sh db_backup_user
./create_secret.sh db_backup_password
./create_secret.sh restic_password
./create_secret.sh STAGE_B2_ACCOUNT_ID
./create_secret.sh STAGE_B2_ACCOUNT_KEY
The docker-compose.yml
in the repository defaults to set up for local testing. Update the secrets
section to use Docker secrets instead of local files.
secrets:
db_root_password:
external: true
db_user:
external: true
db_password:
external: true
db_backup_user:
external: true
db_backup_password:
external: true
restic_password:
external: true
STAGE_B2_ACCOUNT_ID:
external: true
STAGE_B2_ACCOUNT_KEY:
external: true
Unchanged, however, update DOMAINS_BLOG, DOMAINS_ADMIN, and set TARGET to production once everything is working properly
The Docker services will be deployed to a Docker Stack in production. Unlike Docker Compose, Docker Stack does not automatically create local folders. Create empty folders for the mariadb
, ghost
, and portal
data. Next, deploy the Docker Stack using docker-compose
as input. This ensures the environment variables are parsed correctly.
mkdir -p data/mariadb/mysql
mkdir -p data/mariadb/backup
mkdir -p data/mariadb/log
mkdir -p data/ghost
mkdir -p data/portal
docker-compose config | docker stack deploy -c - ghost-backend
Run the following command to inspect the status of the Docker Stack.
docker stack services ghost-backend
You should see the value 1/1
for REPLICAS
for the mariadb
, ghost
, and portal
services if the stack was initialized correctly. It might take a while before the services are up and running, so simply repeat the command after a few minutes if needed.
ID NAME MODE REPLICAS IMAGE PORTS
*** ghost-backend_mariadb replicated 1/1 markdumay/mariadb:latest
*** ghost-backend_ghost replicated 1/1 markdumay/ghost:latest *:2368->2368/tcp
*** ghost-backend_portal replicated 1/1 markdumay/portal:latest *:80->80/tcp, *:443->443/tcp
You can view the service log with docker service logs <service-name>
once the service is up and running. Refer to Step 4 for validation of the logs.
Debugging swarm services can be quite challenging. If for some reason your service does not initiate properly, you can get its task ID with docker service ps <service-name>
. Running docker inspect <task-id>
might give you some clues to what is happening. Use docker stack rm ghost-backend
to remove the docker stack entirely.
Open your internet browser and navigate to the Ghost admin page. The default value is example.test/ghost
or example.com/ghost
pending you are in test mode or production. The site's certificate is self-signed in a local setup, so you might need to instruct your internet browser to trust this certificate. The site should now display the setup screen of Ghost and will ask you to set up an administrative user.
Once you have set up your administrative account and finished configuring Ghost, you can navigate to the main site at either example.test
or example.com
. Ghost is now ready for use.
If enabled in the environment settings, Ghost-backend creates local backups of the database every 30 minutes. See the BACKUP
setting in Step 3 on how to enable this. Under the hood, the script mysqldump-local.sh
embedded in the mariadb
container exports the Ghost data to a file in the /var/backup/mariadb
folder. The same script can also be used to restore the database. To do so, connect to the shell of your running mariadb container by running below command from your host.
docker exec -it ghost-backend_mariadb_1 bash
From within the container, run the following command to restore the Ghost database from the latest backup available in /var/backup/mariadb
. You can replace the backup path with another path if needed.
mysqldump-local.sh restore /var/backup/mariadb
By default, mysqldump-local.sh
uses the latest available backup. You can specify a specific file using the -b
flag.
Once the operation is confirmed, all existing data of the Ghost database is replaced with the content from the backup file. When completed, the scripts should return below message:
Completed restore from '/var/backup/mariadb/ghost_backup_YYYYMMDD_HHhMMmSSs.sql' in 2 seconds
You can now exit the container with the command exit
. Finally, restart the ghost
container to ensure Ghost works correctly with the restored data.
- Clone the repository and create a new branch
$ git checkout https://github.com/markdumay/ghost-backend.git -b name_for_new_branch
- Make and test the changes
- Submit a Pull Request with a comprehensive description of the changes
Ghost-backend is inspired by the following blog article:
- Scott Helme - Caching Ghost with Nginx
Copyright © Mark Dumay