How I Moved To Ghost 3 (And Made Ghost Dawn Dark-Mode Only)
Back in January 2019 I deployed self-hosted Ghost 2 at my home PC server. It was almost 2 years ago, I stopped writing posts to my Ghost blog in April 2019 despite lots of solutions for different projects that would help people on the Internet if they were documented. So now I decided to resurrect this blog and migrate it (and all other stuff on home servers) to Contabo VPS.
TL;DR: for quick Ghost 3 setup check README.md here.
Content
Preparation
My Ghost deployment lives behind nginx proxy and uses Postfix for mail. I happened to create simple management CLIs for both :)
PyPI links: webserver and kolombo (both rely on Docker)
You can use your solutions nginx and mail server, nginx part will be explained in details and for mail server - you just need it to exist somewhere.
Setting up nginx
Before we begin, start webserver via webserver run
. This will start default HTTP server on 80 that will help us to get LetsEncrypt TLS certificates.
Blocks FYI will help you set it up without webserver - just make sure you have default HTTP server on 80 running via nginx in Docker with attachable overlay network "nginx" created and attached to container.
When you see domain blog.neigor.me in bold below - replace it with your domain.
Generating certificates and DH params
With webserver this is easy - just run commands below and it will generate LetsEncrypt TLS certificates and DH params needed for HTTPS configuration.
webserver gentls blog.neigor.me webserver gendh blog.neigor.me
FYI: Commands above use certbot and openssl (use them directly if not using webserver):
sudo certbot certonly --webroot -w /path/to/http/root/ \ --post-hook "nginx -t && nginx -s reload" \ -d blog.neigor.me sudo openssl dhparam \ -out /path/to/dhparams/blog.neigor.me.pem 3072
Underlined paths must exist and must be accessible to nginx:
- /path/to/http/root/ is accessed when you go to blog.neigor.me/
- /path/to/dhparams/ is where DH params are stored for nginx to use
Adding HTTPS config for nginx
After that we can finally add HTTPS config for nginx (but not yet reload nginx). Config content should be similar to the one I generated for webserver, add it via webserver conf add blog.neigor.me file.conf
:
# Config used with webserver server { server_name blog.neigor.me; #### Set up SSL/TLS include /etc/nginx/sites-enabled/defaults/ssl; ssl_certificate /etc/letsencrypt/live/blog.neigor.me/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/blog.neigor.me/privkey.pem; ssl_dhparam /path/to/dhparams/blog.neigor.me.pem; #### Set body limit to 4 mbytes (for pictures and lots of text) client_max_body_size 4m; location / { include /etc/nginx/sites-enabled/defaults/proxy; proxy_pass http://blog:2368/; } }
FYI: Expanded version of config above (add it yourself if not using webserver):
server { server_name blog.neigor.me; #### Set up SSL/TLS #### Use HTTP/2 and modern TLS versions listen 443 ssl http2; ssl_protocols TLSv1.2 TLSv1.3; #### Do not send nginx version server_tokens off; #### Use strong, PFS and TLSv1.3 compatible ciphers and let user choose between them ssl_ciphers "EECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"; ssl_prefer_server_ciphers off; ssl_ecdh_curve secp384r1; #### Reuse SSL session parameters to avoid SSL handshakes for parallel and subsequent connections ssl_session_timeout 1h; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; #### Security headers (for browsers) add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; ssl_certificate /etc/letsencrypt/live/blog.neigor.me/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/blog.neigor.me/privkey.pem; ssl_dhparam /path/to/dhparams/blog.neigor.me.pem; #### Set body limit to 4 mbytes (for pictures and lots of text) client_max_body_size 4m; location / { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Early-Data $ssl_early_data; proxy_pass http://blog:2368/; } }
We will reload nginx later, for now we will leave it.
You can see that blog in bold on proxy_pass line - it is the hostname that will become accessible after we attach network "nginx" (mentioned above) to Ghost service.
Setting up Postfix
Essentially you just need to set up any mail server. In this tutorial I will set it up with kolombo (link from above) in those few easy steps:
# Generate valid TLS certificates for domain webserver gentls mail.neigor.me # Setup kolombo kolombo setup nano /etc/kolombo/kolombo.env # Add mail domain kolombo domain add neigor.me mail.neigor.me # [...] Deal with DNS TXT record for DKIM # Add user for emails from blog kolombo user add blog@neigor.me # Re-build and deploy kolombo kolombo run all --build
Boom, mail server is set up and sending should be possible via mail.neigor.me:587 over secure connection.
Deploying Ghost 3
Files used can be cloned from my ghost-docker repository.
For deployment I use Docker with Swarm mode, so swarm should be initialized:
sudo docker swarm init
Configuration
Create a file stack.yml that will contain Docker Swarm configuration for your Ghost 3 deployment:
version: '3.7' services: blog: image: ghost:3-alpine ports: - 2368:2368 environment: url: https://blog.neigor.me mail__from: "'Ghost Blog' <blog@neigor.me>" mail__transport: SMTP mail__options__host: mail.neigor.me mail__options__port: 587 mail__options__secureConnection: "true" mail__options__auth__user: blog@neigor.me mail__options__auth__pass: actualPassword volumes: - /home/actualUser/ghost_content:/var/lib/ghost/content hostname: blog networks: - nginx deploy: update_config: delay: 5s failure_action: rollback restart_policy: condition: any resources: limits: cpus: '1' memory: 750M networks: nginx: external: true
I underlined things that you should change. Some things to mention:
- Volume folder should already exist when deploying, Docker Swarm will not create it.
- Network nginx should already exist when deploying, if you are using webserver - it is created, otherwise - create it via
docker network create -d overlay --attachable nginx
- In case your mail server does not use secure connection - you can remove mail__options__secureConnection.
- That
hostname: blog
part is there so nginx can access Ghost container by hostname when they are in one network.
Deployment
To deploy it you just need to run one command:
sudo docker stack deploy -c stack.yml ghost
Docker Swarm will take care of everything else. Now we can finally reload nginx for new HTTPS configuration to be enabled:
# If using webserver webserver conf reload # If nginx is in some container sudo docker exec -it nginx-container sh -c 'nginx -t && nginx -s reload'
Voila! Ghost 3 blog should be on https://blog.neigor.me. One drawback - admin panel can be accessed by anybody from this point, so don't waste your time - set up admin user!
Updating
You will need to update the container image at some point. Update is done with this script:
#!/bin/bash set -e # Scaling before update to keep availability sudo docker service scale ghost_blog=2 # Updating to latest Ghost 3 Alpine Linux image sudo docker service update --image ghost:3-alpine ghost_blog # Scaling back sudo docker service scale ghost_blog=1
Tweaking Ghost Dawn theme
As you can see - this theme has only Dark Mode (as it should be everywhere tbh). It was not always like that, Ghost Dawn can actually be in Light Mode, but I removed the switch and set Dark Mode when page loads.
- Go to your /home/actualUser/ghost_content/themes/Dawn-master
- Open default.hbs in editor and apply change below
Remove <script>
tag with switch-case block and make sure first tag contains following code (new code is underlined):
var siteUrl = '{{@site.url}}'; // Force dark mode localStorage.setItem('dawn_theme', 'dark'); document.documentElement.classList.add('theme-dark');
- Open partials/footer.hbs and apply change below
Remove <a>
tag in the end inside last <nav class="footer-nav">
so that it looks like below:
<nav class="footer-nav">
{{navigation type="secondary"}}
</nav>
Restart Ghost container and you should have Dark Mode by default without a way to switch it to Light Mode!
sudo docker service scale --detach ghost_blog=0 sudo docker service scale ghost_blog=1
Happy Experimenting!