The blog origination
I decided finally that I want to blog in some form and for this purpose to set up self-hosted blog on private VPS. I asked ChatGPT for help. I have never set up any CMS on my own and wanted to ease my life with LLM help. And it was very fruitful despite some issues I had. And not always ChatGPT could resolve all problems. Used mostly o1-preview
for testing but it thought too long. Aside I used good old 4o
which was much faster and was giving very good response. Sometimes even better than o1-preview
.
On server, Docker, docker-compose, nginx were already installed. Whole set up with GPT guidance took me around 3 hours.
There was also funny situation during set up. I tried to create certifcate using robo cert and because of some domain record issues and DNS propagation I was getting exception. When I was pretty sure configuration is fine and that time should work I started getting letsencrypt internal issues. Could not believe but checked and there was actually ongoing incident started 1 min ago before my first cert creation with valid configuration 😄 But after some time it was resolved and I was able to generate certificate.
To follow up in future:
- customize the used theme. More details in https://mjac.dev/ghost-custom-theme-development-deployment-and-maintenance/
Below is summary generated by GPT on our conversation.
GPT 4o
generated summary
How I Set Up a Self-Hosted Ghost Blog on a VPS
Setting up a self-hosted Ghost blog on my VPS was a rewarding journey, though it came with its fair share of challenges. Here’s a brief overview of the process, the main issues I faced, and how they were resolved.
1. Initial Setup: Docker and MySQL Configuration
Ghost CMS can be easily hosted using Docker. I started by setting up Docker and Docker Compose on my Ubuntu VPS, with Nginx already installed as the reverse proxy. Ghost requires a database, and since SQLite is deprecated for production, I configured MySQL.
The basic docker-compose.yml
looked like this:
services:
ghost:
image: ghost:latest
environment:
database__client: mysql
database__connection__host: mysql
database__connection__user: ghost_user
database__connection__password: ghost_password
database__connection__database: ghost_db
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ghost_db
MYSQL_USER: ghost_user
MYSQL_PASSWORD: ghost_password
I made sure to persist the data using volumes for both MySQL and Ghost content.
2. Overcoming the Database Connection Issue
One of the first issues I encountered was Ghost failing to connect to MySQL, returning an Invalid database host
error. This happened because Ghost couldn’t resolve the MySQL container by its name. The solution was to ensure both services were on the same Docker network and properly configure the database__connection__host
as the MySQL service name.
3. MySQL Compatibility Problem
At one point, I faced a glibc error: CPU does not support x86-64-v2
. This was due to the VPS not supporting the newer CPU architecture required by MySQL 8. To fix this, I switched to MySQL 5.7, which is compatible with older systems.
4. Migration Lock Issue
After resolving database issues, another challenge arose: Migration lock was never released
. This error occurs when a previous database migration is left incomplete. I tried rolling back the migration using Ghost's internal command:
yarn knex-migrator rollback
Eventually, I manually removed the lock from the MySQL database, which resolved the issue.
5. Finalizing and Customizing Ghost
Once Ghost was up and running, I created the basic structure for the blog, including an "About" page. This was done easily within Ghost’s admin panel under the Pages section. To add this page to the navigation, I simply updated the Navigation settings in the admin interface.
For deeper customization, like editing templates, Ghost uses Handlebars templates. I downloaded the theme, edited the templates locally, and re-uploaded the theme via the Ghost admin panel.
Final Thoughts
Setting up a self-hosted Ghost blog was both challenging and rewarding. From configuring MySQL to troubleshooting migration locks, I encountered a few bumps but learned a lot about Docker, databases, and Ghost itself. Now, the blog is running smoothly, and the experience has given me a deeper appreciation for the simplicity and power of Ghost CMS.
If you're considering hosting Ghost yourself, be prepared for some technical hurdles, but with persistence, it’s absolutely worth it!
GPT o1-preview
generated summary
Setting up my own blog using Ghost on a VPS was an enlightening journey. I chose to use Docker for its simplicity and the isolation it provides. Here's a concise summary of the key steps I took and the challenges I encountered along the way.
Preparing the VPS
I had a VPS running Ubuntu with Docker, Docker Compose, and Nginx already installed. My goal was to set up Ghost using Docker and configure Nginx as a reverse proxy to handle incoming traffic.
Creating the Docker Compose File
I created a docker-compose.yml
file to define the Ghost service, utilizing SQLite as the database for simplicity:
version: '3.3'
services:
ghost:
image: ghost:latest
container_name: ghost-blog
restart: always
environment:
url: https://mydomain.com
volumes:
- ./content:/var/lib/ghost/content
ports:
- "2368:2368"
This configuration sets the blog URL, maps the content directory for data persistence, and exposes port 2368 for Ghost.
Configuring Nginx
Next, I configured Nginx to act as a reverse proxy and handle SSL termination. I created a server block in /etc/nginx/sites-available/ghost
:
server {
listen 80;
server_name mydomain.com www.mydomain.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name mydomain.com www.mydomain.com;
ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2368;
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;
}
}
Obtaining an SSL Certificate
I used Certbot to obtain a free SSL certificate from Let's Encrypt. Initially, I faced an "unauthorized" error during the certificate issuance process. To resolve this, I:
- Verified DNS Settings: Ensured my domain's A record pointed to my VPS's IP address.
- Checked Nginx Configuration: Confirmed that the
server_name
directive matched my domain. - Adjusted Firewall Rules: Allowed HTTP and HTTPS traffic through the firewall using UFW.
After these adjustments, Certbot successfully issued the SSL certificate.
Encountering a 502 Bad Gateway Error
Upon accessing my domain, I encountered a 502 Bad Gateway error. The error logs indicated:
Error: connect ECONNREFUSED 127.0.0.1:3306
This suggested that Ghost was attempting to connect to a MySQL database on port 3306, even though I intended to use the default SQLite database.
Resolving the Database Configuration
To fix this issue:
- Checked
docker-compose.yml
: Ensured there were no MySQL configurations present. - Removed Unintended Environment Variables: Cleared any variables that might force Ghost to use MySQL.
- Reset Ghost Configurations: Deleted the existing
content
directory to remove old configurations. - Restarted the Container: Brought the Ghost container back up with
docker-compose up -d
.
After these steps, Ghost started using SQLite as intended, and the blog became accessible.
Setting up Ghost with Docker on my VPS was a valuable learning experience. Despite a few hurdles, careful troubleshooting and attention to configuration details led to a successful deployment. Now, with my blog up and running, I'm excited to start sharing content.