<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[not{good} Igor - Igor Nehoroshev's blog]]></title><description><![CDATA[Experiments and stories from software engineer]]></description><link>https://blog.neigor.me/</link><image><url>https://blog.neigor.me/favicon.png</url><title>not{good} Igor - Igor Nehoroshev&apos;s blog</title><link>https://blog.neigor.me/</link></image><generator>Ghost 3.40</generator><lastBuildDate>Wed, 08 Apr 2026 08:07:57 GMT</lastBuildDate><atom:link href="https://blog.neigor.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[How I Moved To Ghost 3 (And Made Ghost Dawn Dark-Mode Only)]]></title><description><![CDATA[Story of how I set up Ghost 3 on Docker Swarm behind nginx and with own mail server.]]></description><link>https://blog.neigor.me/migration-to-ghost-3/</link><guid isPermaLink="false">5fe26af0d86f34000134c0e8</guid><category><![CDATA[docker]]></category><category><![CDATA[webserver]]></category><category><![CDATA[kolombo]]></category><category><![CDATA[migration]]></category><dc:creator><![CDATA[Igor Nehoroshev]]></dc:creator><pubDate>Wed, 23 Dec 2020 22:49:08 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1583638666708-5ba77a6f8bd1?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MXwxMTc3M3wwfDF8c2VhcmNofDN8fGdob3N0fGVufDB8fHw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1583638666708-5ba77a6f8bd1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXwxMTc3M3wwfDF8c2VhcmNofDN8fGdob3N0fGVufDB8fHw&ixlib=rb-1.2.1&q=80&w=2000" alt="How I Moved To Ghost 3 (And Made Ghost Dawn Dark-Mode Only)"><p>Back in January 2019 I deployed self-hosted Ghost 2 at my home PC server. It was <strong>almost 2 years ago</strong>, 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 (<em>and all other stuff on home servers</em>) to <a href="https://contabo.com/">Contabo</a> VPS.</p><hr><p><strong>TL;DR</strong>: for quick Ghost 3 setup check <strong>README.md</strong> <a href="https://github.com/HarrySky/ghost-docker">here</a>.</p><h2 id="content">Content</h2><!--kg-card-begin: markdown--><ul>
<li><a href="#preparation">Preparation</a>
<ul>
<li><a href="#setting-up-nginx">Setting up nginx</a>
<ul>
<li><a href="#generating-certificates-and-dh-params">Generating certificates and DH params</a></li>
<li><a href="#adding-https-config-for-nginx">Adding HTTPS config for nginx</a></li>
</ul>
</li>
<li><a href="#setting-up-postfix">Setting up Postfix</a></li>
</ul>
</li>
<li><a href="#deploying-ghost-3">Deploying Ghost 3</a>
<ul>
<li><a href="#configuration">Configuration</a></li>
<li><a href="#deployment">Deployment</a></li>
<li><a href="#updating">Updating</a></li>
</ul>
</li>
<li><a href="#tweaking-ghost-dawn-theme">Tweaking Ghost Dawn theme</a></li>
</ul>
<!--kg-card-end: markdown--><h2 id="preparation">Preparation</h2><p>My Ghost deployment lives behind <a href="https://nginx.org/">nginx</a> proxy and uses <a href="http://www.postfix.org/">Postfix</a> for mail. I happened to create simple management CLIs for both :)</p><p><strong>PyPI links</strong>: <a href="https://pypi.org/project/webserver/">webserver</a> and <a href="https://pypi.org/project/kolombo/">kolombo</a> (<em>both rely on <a href="https://www.docker.com/">Docker</a></em>)</p><p>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.</p><h3 id="setting-up-nginx">Setting up nginx</h3><p>Before we begin, start <strong>webserver</strong> via <code><u>webserver run</u></code>. This will start <a href="https://gist.github.com/HarrySky/b2ac620b845a3a87ecdb0bd827c8050a">default HTTP server on 80</a> that will help us to get LetsEncrypt TLS certificates.</p><p>Blocks <strong><u>FYI</u></strong> will help you set it up without <strong>webserver</strong> - just make sure you have <a href="https://gist.github.com/HarrySky/b2ac620b845a3a87ecdb0bd827c8050a">default HTTP server on 80</a> running via <strong>nginx in Docker</strong> with attachable overlay <strong>network "nginx"</strong> created and attached to container.</p><p>When you see domain <strong>blog.neigor.me</strong> in bold below - replace it with your domain.</p><h4 id="generating-certificates-and-dh-params">Generating certificates and DH params</h4><p>With <strong>webserver</strong> this is easy - just run commands below and it will generate LetsEncrypt TLS certificates and DH params needed for HTTPS configuration.</p><!--kg-card-begin: markdown--><pre>
webserver gentls <b>blog.neigor.me</b>
webserver gendh <b>blog.neigor.me</b>
</pre><!--kg-card-end: markdown--><p><strong><u>FYI</u></strong>: Commands above use <a href="https://certbot.eff.org/">certbot</a> and <a href="https://www.openssl.org/">openssl</a> (<em>use them directly if not using <strong>webserver</strong></em>):</p><!--kg-card-begin: markdown--><pre>
sudo certbot certonly --webroot <u>-w /path/to/http/root/</u> \
    --post-hook "nginx -t && nginx -s reload" \
    -d <b>blog.neigor.me</b>

sudo openssl dhparam \
    -out <u>/path/to/dhparams/</u><b>blog.neigor.me</b>.pem 3072
</pre><!--kg-card-end: markdown--><p>Underlined paths <strong>must exist</strong> and <strong>must be accessible to nginx</strong>:</p><ul><li><u>/path/to/http/root/</u> is accessed when you go to <em><strong>blog.neigor.me</strong>/</em></li><li><u>/path/to/dhparams/</u> is where DH params are stored for nginx to use</li></ul><h4 id="adding-https-config-for-nginx">Adding HTTPS config for nginx</h4><p>After that we can finally add <strong>HTTPS config for nginx</strong> (<em><u>but not yet reload nginx</u></em>). Config content should be similar to the one I generated for <strong>webserver</strong>, add it via <code><u>webserver conf add <strong>blog.neigor.me</strong> file.conf</u></code>:</p><!--kg-card-begin: markdown--><pre>
# Config used with webserver
server {
    server_name <b>blog.neigor.me</b>;

#### Set up SSL/TLS
    include /etc/nginx/sites-enabled/defaults/ssl;
    ssl_certificate /etc/letsencrypt/live/<b>blog.neigor.me</b>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<b>blog.neigor.me</b>/privkey.pem;
    ssl_dhparam <u>/path/to/dhparams/</u><b>blog.neigor.me</b>.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://<b>blog</b>:2368/;
    }
}
</pre><!--kg-card-end: markdown--><p><strong><u>FYI</u></strong>: Expanded version of config above (<em>add it yourself if not using <strong>webserver</strong></em>):</p><!--kg-card-begin: markdown--><pre>
server {
    server_name <b>blog.neigor.me</b>;

#### 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/<b>blog.neigor.me</b>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<b>blog.neigor.me</b>/privkey.pem;
    ssl_dhparam <u>/path/to/dhparams/</u><b>blog.neigor.me</b>.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://<b>blog</b>:2368/;
    }
}
</pre><!--kg-card-end: markdown--><p>We will reload nginx later, for now we will leave it.</p><p>You can see that <strong>blog</strong> in bold on <em>proxy_pass</em> line - it is the hostname that will become accessible after we attach <strong>network "nginx"</strong> (<em>mentioned above</em>) to Ghost service.</p><h3 id="setting-up-postfix">Setting up Postfix</h3><p>Essentially you just need to set up any mail server. In this tutorial I will set it up with <strong>kolombo</strong> (<em><a href="https://pypi.org/project/kolombo/">link from above</a></em>) in those few easy steps:</p><!--kg-card-begin: markdown--><pre>
# Generate valid TLS certificates for domain
webserver gentls <b>mail.neigor.me</b>

# Setup kolombo
kolombo setup
nano /etc/kolombo/kolombo.env

# Add mail domain
kolombo domain add <b>neigor.me</b> <b>mail.neigor.me</b>
# [...] Deal with DNS TXT record for DKIM

# Add user for emails from blog
kolombo user add <b>blog@neigor.me</b>
# Re-build and deploy kolombo
kolombo run all --build
</pre><!--kg-card-end: markdown--><p>Boom, mail server is set up and sending should be possible via <strong>mail.neigor.me:587</strong> over <u>secure connection</u>.</p><h2 id="deploying-ghost-3">Deploying Ghost 3</h2><p>Files used can be cloned from my <a href="https://github.com/HarrySky/ghost-docker">ghost-docker</a> repository.</p><p>For deployment I use <a href="https://www.docker.com/">Docker</a> with <a href="https://docs.docker.com/engine/swarm/">Swarm mode</a>, so swarm should be initialized:</p><!--kg-card-begin: markdown--><pre>
sudo docker swarm init
</pre><!--kg-card-end: markdown--><h3 id="configuration">Configuration</h3><p>Create a file <strong>stack.yml</strong> that will contain Docker Swarm configuration for your Ghost 3 deployment:</p><!--kg-card-begin: markdown--><pre>
version: '3.7'
services:
  blog:
    image: ghost:3-alpine
    ports:
      - 2368:2368
    environment:
      url: https://<u>blog.neigor.me</u>
      mail__from: <u>"'Ghost Blog' &lt;blog@neigor.me&gt;"</u>
      mail__transport: SMTP
      mail__options__host: <u>mail.neigor.me</u>
      mail__options__port: <u>587</u>
      mail__options__secureConnection: <u>"true"</u>
      mail__options__auth__user: <u>blog@neigor.me</u>
      mail__options__auth__pass: <u>actualPassword</u>
    volumes:
      - <u>/home/actualUser/ghost_content</u>:/var/lib/ghost/content
    hostname: <b>blog</b>
    networks:
      - <b>nginx</b>
    deploy:
      update_config:
        delay: 5s
        failure_action: rollback
      restart_policy:
        condition: any
      resources:
        limits:
          cpus: '1'
          memory: 750M

networks:
  <b>nginx</b>:
    external: true
</pre><!--kg-card-end: markdown--><p>I underlined things that you should change. Some things to mention:</p><ul><li><strong><u>Volume folder should already exist when deploying</u></strong>, Docker Swarm will not create it.</li><li><strong><u>Network nginx should already exist when deploying</u></strong>, if you are using webserver - it is created, otherwise - create it via <code><u>docker network create -d overlay --attachable nginx</u></code></li><li>In case your mail server does not use secure connection - you can remove <strong>mail__options__secureConnection</strong>.</li><li>That <code><u>hostname: <strong>blog</strong></u></code> part is there so nginx can access Ghost container by hostname when they are in one network.</li></ul><h3 id="deployment">Deployment</h3><p>To deploy it you just need to run one command:</p><!--kg-card-begin: markdown--><pre>
sudo docker stack deploy -c <b>stack.yml</b> ghost
</pre><!--kg-card-end: markdown--><p>Docker Swarm will take care of everything else. Now we can finally <strong>reload nginx</strong> for new HTTPS configuration to be enabled:</p><!--kg-card-begin: markdown--><pre>
# 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'
</pre><!--kg-card-end: markdown--><p><u><strong>Voila!</strong></u> Ghost 3 blog should be on https://<strong>blog.neigor.me</strong>. One drawback - <strong>admin panel can be accessed by anybody</strong> from this point, so don't waste your time - <strong>set up admin user</strong>!</p><h3 id="updating">Updating</h3><p>You will need to update the container image at some point. Update is done with this script:</p><!--kg-card-begin: markdown--><pre>
#!/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
</pre><!--kg-card-end: markdown--><h2 id="tweaking-ghost-dawn-theme">Tweaking Ghost Dawn theme</h2><p>As you can see - this theme has only <strong>Dark Mode</strong> (<em>as it should be everywhere tbh</em>). It was not always like that, Ghost Dawn can actually be in <strong>Light Mode</strong>, but I removed the switch and set Dark Mode when page loads.</p><ul><li>Go to your <strong>/home/actualUser/ghost_content/themes/Dawn-master</strong></li><li>Open <strong>default.hbs</strong> in editor and apply change below</li></ul><p>Remove <strong><code>&lt;script&gt;</code></strong> tag with <strong>switch-case</strong> block and make sure first tag contains following code (<em>new code is underlined</em>):</p><!--kg-card-begin: markdown--><pre>
var siteUrl = '{{@site.url}}';
<u>// Force dark mode
localStorage.setItem('dawn_theme', 'dark');
document.documentElement.classList.add('theme-dark');</u>
</pre><!--kg-card-end: markdown--><ul><li>Open <strong>partials/footer.hbs</strong> and apply change below</li></ul><p>Remove <code><strong>&lt;a&gt;</strong></code> tag in the end inside last <code><strong>&lt;nav class="footer-nav"&gt;</strong></code> so that it looks like below:</p><!--kg-card-begin: markdown--><pre><code>&lt;nav class=&quot;footer-nav&quot;&gt;
    {{navigation type=&quot;secondary&quot;}}
&lt;/nav&gt;
</code></pre>
<!--kg-card-end: markdown--><p>Restart Ghost container and you should have <strong>Dark Mode</strong> by default without a way to switch it to <strong>Light Mode</strong>!</p><!--kg-card-begin: markdown--><pre>
sudo docker service scale --detach ghost_blog=0
sudo docker service scale ghost_blog=1
</pre><!--kg-card-end: markdown--><hr><p><strong>Happy Experimenting!</strong></p>]]></content:encoded></item></channel></rss>