Make your own Blog with Ghost
Do you need a self-hosted blog or home page? Off course – we all need them, and a lot of them. Ghost is my favorite, both as hosted and self-hosted. I prefer to run it in Docker or Kubernetes.
Have you ever considered creating your own blog? Have you tried WD and found it to be a great choice, but also somewhat cumbersome and complicated?
Well, I did, and that's how I found Ghost.
Ghost is not to compare with WordPress, but it's great for the personal blog and a little more. You might like it to be hosted (you can easily use Ghost's services) or self-host the blog. This blog is about how to self-host.
- Complete control over your website and your branding.
- Launch a custom website and tweak it to perfectly match your brand and style.
- Hundreds of custom themes in the Ghost marketplace, or you can build your own completely custom design from scratch.
- Your unique brand sits front and center, while Ghost runs things in the background.
Prerequisites
You need to buy a domain if you do not already have one, and you need to control your DNS to point the blog name to your system. Other things to consider:
- A fixed IP is ideal. A fix could be to use a DDNS service.
- You should understand networking and network security.
- You need a firewall to control what comes in and goes out of your network.
- Use a Revers Proxy to redirect the stream
- Use some security system if you like it to be private, Authelia, Authentic …
- What OS to use, Alpine – Ubuntu, it's your choice.
- Estimate the size you will need for the files associated with the blog
Set up your system
There is a lot of choice how to do it as always on Linux. You can run Ghost on real rust, in a VM or in Docker. Ghost can utilize the power of a Database, but you can use it without in development mode.
Installation
Docker Installation
We will use the Docker approach running in an Alpine VM.
Set up the VM – Alpine 3.15 and newer
Grab a copy of the latest Alpine ISO. Create a VM and install all the parts you require. Here we only concentrate on the basic system required for the blog.
The basic VM will have: 1 vCPU, 1 GiB RAM and 8 GiB for the system and 32 GiB for the data disk. It runs Docker, Dockge, or Portainer. For the purposes of this blog, we assume that the RPM, authentication, and firewall applications are running in another VM on any of our systems.
Adding the community repo
If you need things like the QEMU-Guest-Agent or sudo, you need to activate the community repo. Edit the file /etc/apk/repositories
and remove the #
in front of the …/community
lines.
Adding QEMU-Guest-Agent service
Use doas
or sudo
if not running as root.
- Add the package:
apk add qemu-guest-agent
- Start the service:
rc-service qemu-guest-agent start
- Enable the service on startup:
rc-update add qemu-guest-agent
- Restart the service:
rc-service qemu-guest-agent restart
Adding a disk
Format a disk: e.g., by fdisk /dev/sdb
add a partition by hitting n
and p
1
and enter
to the rest, then hit w
to write to the disk.
Create a file system on the new disk mkfs.ext4 /dev/sdb1
and doas reboot
.
Add a disk to fstab. Find the UUID by blkid
and vi/nano etc/fstab
, add it as UUID=<UUID> /srv ext4 defaults 0 2
. Change /srv
to something and the 2
means it's required.
Expanding a disk. First expand the allocated disk in your VM and then SSH in to the VM and do.
- First
sudo growpart /dev/sdb 1 --update auto
- and then
sudo resize2fs /dev/sdb1
- Use
df -h
to check for success
Install Docker
Docker packages are in the community repo
- Install Docker-ce:
apk add docker
- Add a user to the docker group:
addgroup username docker
- Start at boot:
rc-update add docker default
andservice docker start
- Docker compose:
apk add docker-cli-compose
Docker rootless allows unprivileged users to run the docker daemon and docker containers in user namespaces.
apk add docker-rootless-extras
andrc-update add cgroups
- see the Docker documentation for the rest
- You might encounter this message when executing
docker info
.
To correct this situation, you must configurecgroups
properly. - Create your external networks, one for external and one for internal traffic.
docker network create -d bridge <external name>
docker network create -d bridge <internal name>
- Make the generic folders you need for your stacks
Install Dockge
Dockge is my favorite, but there is thing you benefit from also having Portainer.
doas docker run -d -p 5001:5001 --name Dockge --restart=unless-stopped -v /var/run/docker.sock:/var/run/docker.sock -v /home/$USER/docker/dockge/data:/app/data -v /home/$USER/docker/stacks:/home/$USER/docker/stacks -e DOCKGE_STACKS_DIR=/home/$USER/docker/stacks louislam/dockge:latest
Dockge stores the compose files in docker/stacks/
Portainer
First, create the volume that Portainer Server will use to store its database:
docker volume create portainer_data
Then, download and install the Portainer Server container:
# If you use Dockge to install it
version: "3.3"
services:
portainer-ce:
ports:
- 8000:8000
- 9443:9443
container_name: portainer
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
image: portainer/portainer-ce:latest
volumes:
portainer_data: {}
networks: {}
Point your browser to https://<ServerIP>:9443
and create your admin user.
Portainer stores the docker-compose files at /var/lib/docker/volumes/portainer_data/_data/compose/
Install Ghost
Generic files to use with Dockge/Portainer or use as docker compose -d
The Compose
Change the ports according to your needs, and replace the generic "blog" with the name of the site you are creating for the blog.
version: "3.8"
services:
db:
image: mysql:8.0
container_name: ghost-db
restart: unless-stopped # always
environment:
MYSQL_ROOT_PASSWORD: ${rootpassword}
MYSQL_DATABASE: ${database}
MYSQL_USER: ${user}
MYSQL_PASSWORD: ${password}
volumes:
- /srv/db_mysql:/var/lib/mysql
networks:
- backend
ghost:
image: ghost:alpine
container_name: blog # or the name of the blog
restart: unless-stopped #always
ports:
- 2368:2368
environment:
database__client: ${client}
database__connection__host: ${host}
database__connection__user: ${user}
database__connection__password: ${password}
database__connection__database: ${database}
url: ${url}
admin: ${admin}
NODE_ENV: ${mode}
volumes:
- /srv/ghost/content:/var/lib/ghost/content
networks:
- frontend
- backend
networks:
backend:
external: true
frontend:
external: true
The .env
Replace with your info, see this link. Contrary to the default mentioned in the linked documentation, This image defaults to PRODUCTION, if you want to use the development mode you need to set the parameter in the .env file and MySQL isn't needed, lightsql is used by default.
Creating password 20 - 40 are usually fine for home use:
openssl rand -base64 32
- Run in development mode add
- mode= development
- url: use something like
- http://localhost:2368
- localhost:8080
- https://blog.example.com
- move admin to other url, add
- admin: https://ghostbuster.example.com
client: mysql
host: db
user: <user name>
password: ogDXCux5ZrqwA0Rh7QAPgcUvDAB5US
rootpassword: 1nlKQaR8duD0wy+1oJYfoIJJOxBXpAKWbQb5vgVUk44=
database: ghost
url: http://localhost:2368
Testing
Activate the port setting in your compose. Set up an SSH tunnel from your machine to the VM ssh -L 8888:<VM IP>:2368 <user>@<VM IP>
and point your browser to http://localhost:2368
Add-ons to your setup
Setup with a personal logo (600pxx72px) and a Favicon (60pxx60px). You can also set the Background and Highlight Colors.
Nginx Proxy Manager NPM:
- Expose your private network Web services and get connected anywhere.
- Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves!
Authentication with 2FA/MFA or Single Sign-on
Hosting secured services calls for an authentication tool like Authelia, Authentik or Traefik.
Code Injection – Theme Source
Some things can be manipulated by inserting code into the Main body, Header and/or the Footer area. Here is code I use for TOC, Share to Social Media and bash code layout. I use many more js snippets
In the header
<!-- stylesheet Shades of Purple -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism-themes/1.9.0/prism-shades-of-purple.min.css" integrity="sha512-HM2OlrR8saZI2M4q2qLhGTYpsV8Oh6ktoHraqnYws8D06R7T8a6zIXi/denZBRNxXGHAgpKbIQA3gbz9rQQuuQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Share on Social -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Table of Contents -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.28.2/tocbot.min.js" integrity="sha512-+9XzRSJjnUN2OI106uAbbVZ3f+z1ncIRZFOr56hEdaxbQeZ8i1+B7OVjdF8tG4YhgxM/rWP73K2SiG93x6UJoQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.28.2/tocbot.css" integrity="sha512-Di7Va5KC5NtXyMi+aEyVe2pUnniyhFoxfCrdCAOj8aSA42Te/bWKTz0iumaj5v1sN1nZdsaX8QTCn0k1nN4aLA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<<style>
.gh-content {
position: relative;
}
.gh-content a {
text-decoration: none;
}
.gh-toc > .toc-list {
position: relative;
}
.toc-list {
overflow: hidden;
list-style: none;
}
@media (min-width: 1300px) {
.toc-sidebar {
position: absolute;
top: 0;
bottom: 0;
margin-top: 4vmin;
grid-column: full-start / main-start;
margin-left: 2vmin;
margin-right: 4vmin;
z-index: 99;
}
.gh-toc {
position: sticky; /* On larger screens, TOC will stay in the same spot on the page */
top: 4vmin;
}
}
@media (min-width: 1600px) {
.toc-sidebar {
margin-left: 16vmin;
}
}
.gh-toc .is-active-link::before {
background-color: var(--ghost-accent-color); /* Defines TOC accent color based on Accent color set in Ghost Admin */
font-weight: bold;
}
</style>
In the Footer
<!-- Copy to Clipboard -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js" integrity="sha512-/kVH1uXuObC0iYgxxCKY41JdWOkKOxorFVmip+YVifKsJ4Au/87EisD1wty7vxN2kAhnWh6Yc8o/dSAXj6Oz7A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Table of Contents -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.28.2/tocbot.min.js" integrity="sha512-+9XzRSJjnUN2OI106uAbbVZ3f+z1ncIRZFOr56hEdaxbQeZ8i1+B7OVjdF8tG4YhgxM/rWP73K2SiG93x6UJoQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
tocbot.init({
// Where to render the table of contents.
tocSelector: '.gh-toc',
// Where to grab the headings to build the table of contents.
contentSelector: '.gh-content',
// Which headings to grab inside of the contentSelector element.
headingSelector: 'h1, h2, h3, h4',
// Ensure correct positioning
hasInnerContainers: true,
});
</script>
Share on Social Media is called by:
TOC is called by:
References
Ghost [1] Alpine Linux [2] Docker [3] Dockge [4] Portainer [5]
Authelia [6] Authentik [7] Traefik [8] Nginx Proxy Manager [9] Code Injection [10]
Ghost, Independent technology for modern publishing. homepage, demo, themes ↩︎
Alpine, a distribution based on
musl
andBusyBox
homepage, wikipedia ↩︎Authelia, Single Sign-On Multi-Factor portal for web apps homepage ↩︎
authentik, is an open source Identity Provider focused on flexibility and versatility. homepage ↩︎
Traefik is an open-source Edge Router that automatically discovers and routes requests to your services homepage, GitHub ↩︎
NPM, Nginx Proxy Manager, is a Reverse Proxy Manager that lets you expose your private web services on your network with free SSL, Docker, and multiple users. You can configure and manage your proxy hosts with a beautiful UI and a simple Docker image. homepage ↩︎
CDNjs, free and open source CDN is built to make life easier for developers homepage, GitHub, ↩︎