Published on

Building Ghost Version 2 Blog for the RaspberryPi

Authors

In this post we will setup Ghost 2.0.3 for the Raspberry Pi on Docker Swarm

Dockerfile

Our dockerfile:

Dockerfile
FROM rbekker87/armhf-node:8.11

RUN apk add --no-cache 'su-exec>=0.2' && apk --update add bash gcc g++ make python && npm install sqlite3 --build-from-source

ENV NODE_ENV production
ENV GHOST_CLI_VERSION 1.9.1
ENV GHOST_VERSION 2.0.3
ENV GHOST_INSTALL /var/lib/ghost
ENV GHOST_CONTENT /var/lib/ghost/content

RUN npm install -g "ghost-cli@$GHOST_CLI_VERSION"

RUN set -ex; \
        mkdir -p "$GHOST_INSTALL" \
        && adduser -s /bin/sh -D node \
        && chown node:node "$GHOST_INSTALL" \
        && su-exec node ghost install "$GHOST_VERSION" --db sqlite3 --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL" \
        && cd "$GHOST_INSTALL" \
        && su-exec node ghost config --ip 0.0.0.0 --port 2368 --no-prompt --db sqlite3 --url http://localhost:2368 --dbpath "$GHOST_CONTENT/data/ghost.db" \
        && su-exec node ghost config paths.contentPath "$GHOST_CONTENT" \
        && su-exec node ln -s config.production.json "$GHOST_INSTALL/config.development.json" \
        && readlink -f "$GHOST_INSTALL/config.development.json" \
        && mv "$GHOST_CONTENT" "$GHOST_INSTALL/content.orig" \
        && mkdir -p "$GHOST_CONTENT" && chown node:node "$GHOST_CONTENT" \
        && "$GHOST_INSTALL/current/node_modules/knex-migrator/bin/knex-migrator" --version

ENV PATH $PATH:$GHOST_INSTALL/current/node_modules/knex-migrator/bin

WORKDIR $GHOST_INSTALL

COPY docker-entrypoint.sh /usr/local/bin
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

ENTRYPOINT ["docker-entrypoint.sh"]

CMD ["node", "current/index.js"]

Our Boot Script

Our entrypoint script docker-entrypoint.sh:

#!/bin/bash
set -e

if [[ "$*" == node*current/index.js* ]] && [ "$(id -u)" = '0' ];
  then
    chown -R node "$GHOST_CONTENT"
    exec su-exec node "$BASH_SOURCE" "$@"
fi

if [[ "$*" == node*current/index.js* ]];
  then
    baseDir="$GHOST_INSTALL/content.orig"
    for src in "$baseDir"/*/ "$baseDir"/themes/*;
      do
        src="${src%/}"
        target="$GHOST_CONTENT/${src#$baseDir/}"
        mkdir -p "$(dirname "$target")"
        if [ ! -e "$target" ];
          then
            tar -cC "$(dirname "$src")" "$(basename "$src")" | tar -xC "$(dirname "$target")"
        fi
      done

    knex-migrator-migrate --init --mgpath "$GHOST_INSTALL/current"
fi

prod() {
cat > /var/lib/ghost/config.development.json << EOF
{
  "url": "http://${SERVER_URL:-localhost}:${SERVER_PORT:-2368}",
  "server": {
    "port": ${SERVER_PORT:-2368},
    "host": "0.0.0.0"
  },
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "/var/lib/ghost/content/data/ghost.db"
    }
  },
  "mail": {
    "transport": "SMTP",
    "from": "${FROM_NAME:-MyBlog} <${FROM_EMAIL:-ghost-blog@localhost}>",
    "options": {
      "service": "Mailgun",
      "host": "${SMTP_HOST:-localhost}",
      "port": ${SMTP_PORT:-25},
      "auth": {
        "user": "${SMTP_AUTH_USERNAME:-root}",
        "pass": "${SMTP_AUTH_PASSWORD:-password}"
      }
    }
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}
EOF
}

dev() {
cat > /var/lib/ghost/config.development.json << EOF
{
  "url": "http://${SERVER_URL:-localhost}:${SERVER_PORT:-2368}",
  "server": {
    "port": ${SERVER_PORT:-2368},
    "host": "0.0.0.0"
  },
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "/var/lib/ghost/content/data/ghost.db"
    }
  },
  "mail": {
    "transport": "Direct"
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}
EOF
}

test(){
cat > /var/lib/ghost/config.development.json << EOF
{
  "url": "http://localhost:2368",
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "/var/lib/ghost/content/data/ghost.db"
    }
  },
  "mail": {
    "transport": "Direct"
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}
EOF
}

if  [ "${ENV_TYPE}" = "PROD" ]
  then prod

elif [ "${ENV_TYPE}" = "DEV" ]
  then dev
  else test

fi

exec "$@"

The entrypoint script takes a couple of environment variables, as you can see if they are not defined, defaults will be inherited.

Configurable Environment Variables:

      - ENV_TYPE=PROD
      - SERVER_PORT=2368
      - SERVER_URL=myblog.pistack.co.za
      - FROM_NAME=MyName
      - SMTP_HOST=mail.mydomain.co.za
      - SMTP_PORT=587
      - SMTP_AUTH_USERNAME=me@mydomain.co.za
      - SMTP_AUTH_PASSWORD=secret

Building our Ghost Image

I have a public image available if you dont want to build/push, but for building:

$ docker build -t your-name/repo:tag

Deploy Ghost with Traefik

Our ghost-compose.yml with traefik will look like the following, note that I mounted the source path to the container's path, the source path is running on a replicated glusterfs volume, which can be setup following this post

Also for this demonstration I was using the domain pistack.co.za, where you need to utilize the domain of your choice.

ghost-compose.yml
version: "3.4"

services:
  ghost:
    image: rbekker87/armhf-ghost:2.0.3
    networks:
      - appnet
    volumes:
      - type: bind
        source: /mnt/volumes/myblog/content/data
        target: /var/www/ghost/content/data
    environment:
      - ENV_TYPE=PROD
      - SERVER_PORT=2368
      - SERVER_URL=myblog.pistack.co.za
      - FROM_NAME=MyName
      - SMTP_HOST=mail.mydomain.co.za
      - SMTP_PORT=587
      - SMTP_AUTH_USERNAME=me@mydomain.co.za
      - SMTP_AUTH_PASSWORD=secret
    deploy:
      replicas: 1
      labels:
        - "traefik.enable=true"
        - "traefik.backend=ghost"
        - "traefik.backend.loadbalancer.swarm=true"
        - "traefik.docker.network=appnet"
        - "traefik.port=2368"
        - "traefik.frontend.passHostHeader=true"
        - "traefik.frontend.rule=Host:myblog.pistack.co.za"
      replicas: 3
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == worker]

networks:
  appnet:
    external: true

Deploy the stack:

$ docker stack deploy -c ghost-compose.yml web

Once the service is up, you will be able to reach your blog on the provided traefik.frontend.rule. If you don't have traefik running, you can follow this post to get traefik up and running.

Resources:

Thank You

Thanks for reading, if you like my content, feel free to check out my website, and subscribe to my newsletter or follow me at @ruanbekker on Twitter.

Buy Me A Coffee