Ghost: 用docker搭建Ghost博客详细过程(nginx+https证书全套)

Ghost: 用docker搭建Ghost博客详细过程(nginx+https证书全套)

【概述】

  • 简单docker搭建Ghost
  • 复杂docker-compose搭建Ghost+Nginx

步骤概述:

  1. 解析域名
  2. 启动docker容器
  3. 就可以访问网页了

详细步骤:

  1. 域名:购买服务器VPS+解析域名
  2. 环境:创建帐号,并预设权限
  3. 环境:安装docker和docker-compose
  4. 环境:创建docker网络,用于串联ghost和nginx等容器
  5. 博客:配置ghost的映射目录和博客配置文件
  6. 博客:创建运行ghost容器
  7. 代理:配置nginx映射目录和代理配置文件
  8. 代理:创建运行nginx容器
  9. 证书:创建运行certbot容器,并更新证书
  10. 部署成果,访问成功

下面就开始一个折腾的部署过程

【1 购买服务器VPS+解析域名】

  • Linode注册充值
    官网:www.linode.com
    推荐码:注册充值时,填写推荐码,可以获得20美金赠送。
    推荐码:acd1469162f9392327ba6850077ea3512a521ec3
  • Linode创建多个实例
  • ping一番,考察下哪里的服务器快,保留快的那个实例,其它都可以删了
  • 部署centos7系统,linode4669841_tokyo » Dashboard » Deploy an Image
  • 在域名服务商那,修改dns记录,添加a记录指向你的服务器IP

【2 创建帐号,并预设权限】

  • 切换到root管理员,创建后续用到的ghost专用帐号
# 创建帐号
[root@instance-20210526-1447 ~]# groupadd ghost
[root@instance-20210526-1447 ~]# useradd -m -g ghost ghost -c "Ghost Blog User"
[root@instance-20210526-1447 ~]# passwd ghost
Changing password for user ghost.
New password: 
Retype new password: 
passwd: all authentication tokens updated successfully.
[root@instance-20210526-1447 ~]# mkdir /www
[root@instance-20210526-1447 ~]# mkdir -p /www/ghost
[root@instance-20210526-1447 ~]# mkdir -p /www/nginx
[root@instance-20210526-1514 ~]# chown -R ghost:ghost /www
[root@instance-20210526-1447 ~]# ll /www | grep ghost
drwxr-xr-x. 3 ghost ghost 21 May 26 07:01 ghost

#设置ghost用户的docker执行权限
[root@instance-20210526-1447 ~]# chmod +w /etc/sudoers;vi /etc/sudoers;chmod -w /etc/sudoers;
[root@li1696-145 ghost]# chmod +w /etc/sudoers;vi /etc/sudoers;chmod -w /etc/sudoers;
#修改sudoers文件,
#使得用户ghost执行docker相关命令时,不需要密码
ghost ALL=(ALL) NOPASSWD: /usr/bin/docker,/usr/bin/docker-compose,/usr/bin/yum 
[root@li1696-145 ghost]# sudo su ghost
[ghost@li1696-145 ghost]$ cd ~
[ghost@li1696-145 ~]$ ls
[ghost@li1696-145 ~]$ ls -a
.  ..  .bash_history  .bash_logout  .bash_profile  .bashrc
[ghost@li1696-145 ~]$ vi .bashrc
alias docker="sudo /usr/bin/docker"  
alias docker-compose="sudo /usr/bin/docker-compose"
alias apt-get="sudo /usr/bin/yum" 
[ghost@li1696-145 ~]$ source ~/.bashrc 

# 弃用centos默认的firewalld=
[ghost@instance-20210526-1514 nginx]$ sudo su
[sudo] password for ghost:
[root@instance-20210526-1514 nginx]# systemctl stop firewalld
# 启用iptables管理 22,80,443
[root@instance-20210526-1514 nginx]# yum install -y iptables-services 
[root@instance-20210526-1514 nginx]# systemctl start iptables
# 拷贝iptables备份文件覆盖/etc/sysconfig/iptables
[root@instance-20210526-1514 nginx]# vi /etc/sysconfig/iptables
[root@instance-20210526-1514 nginx]# cp /home/ghost/iptables /etc/sysconfig/iptables
cp: overwrite ‘/etc/sysconfig/iptables’? yes
[root@instance-20210526-1514 nginx]# systemctl restart iptables.service

【3 安装docker和docker-compose】

# 更新yum包
[root@instance-20210526-1514 ~]# yum update
Loaded plugins: fastestmirror ...     
Determining fastest mirrors ...
Resolving Dependencies
    [...一堆需要更新的包名...]
Dependencies Resolved
    Installing:	[...一堆包...]
    Updating:	[...一堆包...]  
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Updating:    [...一堆包...]    97/97 
Installed:   [...一堆包...]             
Updated:  [...一堆包...]            
Replaced:  [...一堆包...] 
Complete!
[root@instance-20210526-1514 ~]# yum install -y epel-release
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile ...
Installed:
  epel-release.noarch 0:7-9
Complete!
# 使用官方源
[root@instance-20210526-1514 ~]# yum-config-manager --add-repo http://download.docker.com/linux/centos/docker-ce.repo
[root@instance-20210526-1514 ~]# yum install docker-ce
* docker安装
[root@instance-20210526-1514 ~]# yum install docker-ce
Loaded plugins: fastestmirror, langpacks, product-id, search-disabled-repos, subscription-manager

This system is not registered with an entitlement server. You can use subscription-manager to register.

Loading mirror speeds from cached hostfile
...
...
Dependency Installed:
  containerd.io.x86_64 0:1.6.33-3.1.el7        docker-buildx-plugin.x86_64 0:0.14.1-1.el7  docker-ce-cli.x86_64 1:26.1.4-1.el7  docker-ce-rootless-extras.x86_64 0:26.1.4-1.el7 
  docker-compose-plugin.x86_64 0:2.27.1-1.el7 

Complete!
[root@instance-20210526-1514 ~]# systemctl start docker # 启动docker服务
[root@instance-20210526-1514 ~]# systemctl enable docker # 设置为开机服务
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
[root@instance-20210526-1514 ~]# docker version
Client: Docker Engine - Community
 Version:           26.1.4
 API version:       1.45
 Go version:        go1.21.11
 Git commit:        5650f9b
 Built:             Wed Jun  5 11:32:04 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          26.1.4
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.21.11
  Git commit:       de5c9cf
  Built:            Wed Jun  5 11:31:02 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.33
  GitCommit:        d2d58213f83a351ca8f528a95fbd145f5654e957
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
  
# 安装docker-compose
[root@instance-20210526-1514 ~]# sudo curl -L "https://github.com/docker/compose/releases/download/v2.18.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/bin/docker-compose
[root@instance-20210526-1514 ~]# sudo chmod +x /usr/bin/docker-compose
[ghost@instance-20210526-1514 certbot]$ docker-compose version
Docker Compose version v2.18.1

【4 创建docker网络】

[ghost@instance-20210526-1514 ~]$ docker network create ghost_net
6622eb06ad225655bf8b680a0de93825413ad8569b98d2ba0bc7cf60fb5b752b
[ghost@instance-20210526-1514 ghost]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
8dce76598c2e bridge bridge local
179cc6d32121 certbot_default bridge local
3ba098121ff5 ghost_net bridge local
67fca2971343 host host local
d837e32119cb none null local

【5 配置ghost的映射目录和博客配置文件】

  • 宿主机目录将用于映射到容器里被读写,做到程序和数据分离
    [ghost@instance-20210526-1514 ghost]$ mkdir -p data/var/lib/ghost/content
    [ghost@instance-20210526-1514 ghost]$ mkdir -p data/var/lib/ghost/current/content
    [ghost@instance-20210526-1514 ghost]$ cat data/var/lib/ghost/config.production.json
    {
    "url": "https://ghost.atibm.com/",
    "server": {
    "port": 2368,
    "host": "0.0.0.0"
    },
    "database": {
    "client": "sqlite3",
    "connection": {
    "filename": "/var/lib/ghost/content/data/ghost.db"
    }
    },
    "logging": {
    "transports": [
    "file",
    "stdout"
    ]
    },
    "process": "systemd",
    "paths": {
    "contentPath": "/var/lib/ghost/content"
    }
    }

【6 创建运行ghost容器】

[ghost@instance-20210526-1514 nginx]$ cat /www/ghost/docker-compose.yml
version: '3.8'

services:
  ghost:
    container_name: ghost
    restart: unless-stopped
    image: ghost:5.30.0 #4.24.0 # 4.5.0
    #privileged: true
    networks: 
      - ghost_net
    volumes:
      - /www/ghost/data/config.production.json:/var/lib/ghost/config.production.json
      - /www/ghost/data/content:/var/lib/ghost/content
      - /www/ghost/data/currentcontent:/var/lib/ghost/current/content
networks:
  ghost_net:
    external: true
  • 使用ghost用户启动ghost容器
[ghost@instance-20210526-1514 ghost]$ docker-compose up -d
[+] Building 0.0s (0/0)                                                                                                                                    
[+] Running 1/1
 ✔ Container ghost  Started                                                                                                                           1.9s 
[ghost@instance-20210526-1514 ghost]$ docker-compose logs
ghost  | [2024-06-12 02:06:35] INFO Ghost is running in production...
ghost  | [2024-06-12 02:06:35] INFO Your site is now available on https://ghost.atibm.com/
ghost  | [2024-06-12 02:06:35] INFO Ctrl+C to shut down
ghost  | [2024-06-12 02:06:35] INFO Ghost server started in 4.068s
ghost  | [2024-06-12 02:06:37] INFO Database is in a ready state.
ghost  | [2024-06-12 02:06:37] INFO Ghost database ready in 5.194s

【7 配置nginx映射目录和代理配置文件】

[ghost@instance-20210526-1514 ~]$ cd /www/nginx
[ghost@instance-20210526-1514 nginx]$ cat data/nginx.conf  # 映射的nginx配置文件
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '\$remote_addr - \$remote_user [\$time_local] "\$request" '
                      '\$status \$body_bytes_sent "\$http_referer" '
                      '"\$http_user_agent" "\$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

[ghost@instance-20210526-1514 nginx]$ cat data/conf.d/default.conf  # 映射的代理配置模板文件
server {
    listen       80;
    #server_name  localhost;
    server_name localhost nginx.atibm.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}

    # Let's encrypt 
    location ^~ /.well-known/acme-challenge/ {
        root    /usr/share/nginx/html;
    }

    location = /.well-known/acme-challenge/ {
        return 404;
    }

}

[ghost@instance-20210526-1514 nginx]$ cat data/conf.d/ghost.conf  # 映射的ghost博客代理配置文件
# redirect all http traffic to https
server {
    listen 80;
    server_name ghost.atibm.com atibm.com www.atibm.com;
    # google adsense ads.txt
    location /ads.txt {
        alias /usr/share/nginx/html/ghost/ads.txt;
    }
    return 301 https://$host$request_uri;
}
# redirect some domain https traffic to https://ghost.atibm.com
server {
    listen 443 ssl;
    server_name atibm.com www.atibm.com;    
    ssl_certificate /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ghost.atibm.com/privkey.pem;
    location /ads.txt {
        alias /usr/share/nginx/html/ads.txt;
    }
    return 301 https://ghost.atibm.com$request_uri;
}
# defined ghost.atibm.com 443
server {
    listen 443 ssl;
    server_name ghost.atibm.com;
    ssl_certificate     /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ghost.atibm.com/privkey.pem;
    root /var/lib/ghost/current/core/server/public;
    access_log /var/log/nginx/ghost-access.log main;
    error_log /var/log/nginx/ghost-error.log warn;
    location /ads.txt {
        alias /usr/share/nginx/html/ghost/ads.txt;
    } 
    location / {
        proxy_pass         http://ghost:2368;
        proxy_set_header     Host $host;
        proxy_set_header     X-Real-IP $remote_addr;
        proxy_set_header    X-Forwarded-Proto https;
        proxy_set_header     X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout     150;
        proxy_send_timeout     100;
        proxy_read_timeout    100;
        proxy_buffers        4 32k;
        client_max_body_size    10m;
        client_body_buffer_size    128;    
    }
}

【8 创建运行nginx容器】

[ghost@instance-20210526-1514 nginx]$ cat /www/nginx/docker-compose.yml 
version: '3.8'

services:
  nginx:
    container_name: "nginx"
    image: nginx:1.21.0
    restart: unless-stopped
    #privileged: true
    volumes:
      - ./data/nginx.conf:/etc/nginx/nginx.conf
      - ./data/conf.d:/etc/nginx/conf.d
      - ./data/html:/usr/share/nginx/html
      - ./data/logs:/var/log/nginx
      - /www/certbot/data/letsencrypt:/etc/letsencrypt
    networks: [ghost_net]
    ports:
      - "80:80"
      - "443:443"
    environment:
      - NGINX_HOST=nginx.atibm.com
      - NGINX_PORT=80

networks:
  ghost_net:
    external: true

[ghost@instance-20210526-1514 nginx]$ docker-compose up -d
[+] Building 0.0s (0/0)                                                                                                                                    
[+] Running 1/1
 ✔ Container nginx  Started    

【9 创建运行certbot容器,并更新证书】

  • 流程说明
    用certbot docker -> 通过certbot certonly命令 -> 用邮箱向letsencrypt.org申请更新 -> 配合域名TXT验证记录 -> 获得证书文件 -> nginx配置读取证书文件 -> 访问https://ghost博客成功
[ghost@instance-20210526-1514 ~]$ cd /www/certbot/
# 容器配置文件
[ghost@instance-20210526-1514 certbot]$ cat /www/certbot/docker-compose.yml 
version: '3.8'

services:
  certbot:
    container_name: "certbot"
    image: certbot/certbot:v1.14.0
    restart: unless-stopped    
    tty: true
    stdin_open: true
    volumes:
      - ./data/letsencrypt:/etc/letsencrypt # 将获取的证书导出到容器外的工作文件夹
      - ./data/backup:/var/lib/letsencrypt/backup
    entrypoint: "/bin/sh" # 必须使用 entrypoint 而不是 command, 以重写 certbot image 的 entrypoint
    
# 创建运行certbot容器
[ghost@instance-20210526-1514 certbot]$ docker-compose up -d

# 进入容器
[ghost@instance-20210526-1514 certbot]$ docker-compose exec certbot sh
/opt/certbot #
# 查看证书有效期
/opt/certbot # certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: ghost.atibm.com
    Serial Number: 336402f715a5e2e8d9a37e60f3ef22bebdc
    Key Type: RSA
    Domains: ghost.atibm.com
    Expiry Date: 2021-08-25 04:34:08+00:00 (VALID: 29 days)
    Certificate Path: /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/ghost.atibm.com/privkey.pem
  Certificate Name: ghost.atibm.com
    Serial Number: 4dc16882a468e352fe437d1e10c0bae5bf7
    Key Type: RSA
    Domains: ghost.atibm.com
    Expiry Date: 2021-08-25 23:53:11+00:00 (VALID: 29 days)
    Certificate Path: /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/ghost.atibm.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

  • 更新你的域名证书
[ghost@instance-20210526-1514 certbot]$ docker-compose exec certbot sh
# 这句命令的效果是申请4个域名到一个证书里,按提示输入你的邮箱,能收件就行。
/opt/certbot # certbot certonly -d ghost.atibm.com -d atibm.com -d www.atibm.com -d trilium.atibm.com  -d triliumcn.atibm.com --preferred-challenges dns --
server https://acme-v02.api.letsencrypt.org/directory --manual

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): youraccount@mail.com

...
Account registered.
Requesting a certificate for ghost.atibm.com and 4 more domains
Performing the following challenges:
dns-01 challenge for atibm.com
dns-01 challenge for ghost.atibm.com
dns-01 challenge for trilium.atibm.com
dns-01 challenge for triliumcn.atibm.com
dns-01 challenge for www.atibm.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.atibm.com with the following value:

0j_CAdI8yxRO2yQRmm4pFeyz2bvqdQNJmb4Hc65nAII

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

# 到这一步先别往下,停住,把这串编码,添加到你的域名txt记录里
# txt _acme-challenge.atibm.com 0j_CAdI8yxRO2yQRmm4pFeyz2bvqdQNJmb4Hc65nAII
# 如果登录域名管理后台也能完成操作,但我使用api token调用完成
# 再开一个新的VPS终端连接,进行操作
# 需要用到 dns-lexicon包,容器没销毁之前,安装一次就行
/opt/certbot # pip install dns-lexicon
/opt/certbot # lexicon namecom create atibm.com TXT --name _acme-challenge --content 0j_CAdI8yxRO2yQRmm4pFeyz2bvqdQNJmb4Hc65nAII --auth-username 域名网站的登录帐号 --auth-token 这里是你域名商提供的api_token

# 如果添加成功,会显示以下信息
RESULT
---------
246028402

# 刚才那个证书申请界面,可以继续了,会提示成功如下
Cleaning up challenges
Subscribe to the EFF mailing list (email: xxx@xxx.xxx).

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/ghost.atibm.com/privkey.pem
   Your certificate will expire on 2024-09-10. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
   

# dns记录销毁
/opt/certbot # lexicon namecom delete atibm.com TXT --name _acme-challenge --auth-username 域名网站的登录帐号 --auth-token 这里是你域名商提供的api_token
  • 到这一步,nginx已经能访问证书文件,并且也能处理你的博客访问,需要刷新一下配置
    /opt/certbot # exit #从certbot容器退出,回到宿主机
    [ghost@instance-20210526-1514 certbot]$ docker exec nginx nginx -s reload

  • 后续证书到期之前,会有邮件通知你,进certbot容器更新一下就行了

/opt/certbot # certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/ghost.atibm.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificates are not due for renewal yet:
  /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem expires on 2024-09-10 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

【10 部署成果,访问成功】

  • 博客地址:现在我们访问【ghost.atibm.com】
  • IP解析:经过【域名 dns】解析到【服务器】
  • 反向代理:由【服务器 nginx服务】做代理处理,访问内部网络【服务器 docker network:ghost_net】
  • 博客服务:找到并访问【服务器 ghost服务】+【服务器 certbot服务生成的证书文件】,博客数据存储到【ghost容器映射宿主机目录】下,
  • 经过一番折腾,我们实现了代理、程序、数据、证书全部分离,并一起工作,并且后续维护简单,比如升级ghost,备份数据等等。

备份:后续还要编写一键备份脚本,免费实现数据安全
扩展性:当然还可以体验别的网站程序,比如wiki、ss、leanote、trilium等
运维:有了vps挺爽的,可以随便玩,加上docker的易维护,不怕服务器搞坏
服务器:同时也可以在linode再次新增一个实例出来调试,不影响现有网站
linode已经闲置了,感谢支持