Installing Ghost 5.4 on Virtualmin Virtual Server - any problems / recommendations?

Is it recommended or supported to install Ghost 5.4 on a Virtualmin Virtual server?

I have completed the install, however there is an issue because the pages that I create using the Admin interface are not visible in Preview, or in the live site when I have published them.

[Details of the install]
My server is a VPS running Webmin (v1.994) / Virtualmin (v7.1) plus LEMP stack (strictly following Ghost recommended config):

  • Ubuntu 18.04.6 LTS
  • NGINX v1.14.0
  • MySQL v5.7.38
  • Node.js v16.16.0
  • server has 16GB RAM

The URL of the Ghost instance is: My first post is (of course…) Hello World :) and should be available at, however it results in a 404 Not Found when I browse to it.

I followed the installation instructions from here (How to install & setup Ghost on Ubuntu 16.04, 18.04 and 20.04) to the letter on a fresh reinstall of the VPS.

[What’s going wrong…]
The strange thing is that after the install, the Ghost Admin / Start page was showing as Unavailable, and I had to add a “location” bliock into the NGINX conf file for the virtual server to get it visible at all.

Following that, I could create a post, insert some text and a picture, and then Publish it. However, when I publish it, it is not visible in the preview pane, neither on the live site. BTW The homepage of the live site is still, as of now, showing as 403 Forbidden.

I suspect this is something related to NGINX and that I haven’t got the changes to the conf file correct. I don’t know enough about the Ghost architecture, or indeed NGINX, to be able to tell what is wrong. I have searched widely but can’t find much reference to installing Ghost 5.4 on Virtualmin.

Can anyone point me in the right direction please? Either that, or put me out of my misery and tell me that I am crazy to even try this installation??? he he…


For starters, this isn’t supported with Ghost V5. You need MySQL 8.

Please share your Nginx and Ghost config (redacting passwords etc.)

Aha! My bad… I think perhaps I read another article on the Ghost website that said that both are supported, however maybe they were referring to a previous version. I am surprised that the Ghost install script doesn’t check that and flag it - it seems to check everything else ;)

I am currently reading up on MySQL upgrade process and will do that, and probably then delete my two test Ghost virtual servers and reinstall them with MySQL 8.0 installed.

I will find those config files and then paste them in here for you before I trash the virtual servers, just in case there is anything obviously wrong in them.

Thanks for the quick reply @mjw Martin!

1 Like

[file: NGINX.CONF]

user www-data;
worker_processes auto;
pid /run/;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;

http {

        # Basic Settings

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

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

        # SSL Settings

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        # Logging Settings

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        # Gzip Settings

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascrip$

        # Virtual Host Configs

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
        server_names_hash_bucket_size 128;

#mail {
#       # See sample authentication script at:
#       #
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";

[file: /etc/nginx/sites-enabled/]

The last section, a location block starting “location ^~ /ghost {...” I added to get the Ghost Admin / Start page to work at all. Following that I then created a new post and noticed that after publishing it didn’t go live, or appear in the Preview pane.

server {
        root /home/blog/public_html;
        index index.php index.htm index.html;
        access_log /var/log/virtualmin/blog.delimo.ga_access_log;
        error_log /var/log/virtualmin/blog.delimo.ga_error_log;
        fastcgi_param GATEWAY_INTERFACE CGI/1.1;
        fastcgi_param SERVER_SOFTWARE nginx;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_param REQUEST_METHOD $request_method;
        fastcgi_param CONTENT_TYPE $content_type;
        fastcgi_param CONTENT_LENGTH $content_length;
        fastcgi_param SCRIPT_FILENAME /home/blog/public_html$fastcgi_script_nam$
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        fastcgi_param REQUEST_URI $request_uri;
        fastcgi_param DOCUMENT_URI $document_uri;
        fastcgi_param DOCUMENT_ROOT /home/blog/public_html;
        fastcgi_param SERVER_PROTOCOL $server_protocol;
        fastcgi_param REMOTE_ADDR $remote_addr;
        fastcgi_param REMOTE_PORT $remote_port;
        fastcgi_param SERVER_ADDR $server_addr;
        fastcgi_param SERVER_PORT $server_port;
        fastcgi_param SERVER_NAME $server_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS $https;
        location ~ \.php(/|$) {
                try_files $uri $fastcgi_script_name =404;
                fastcgi_pass unix:/var/php-nginx/165822549730942.sock/socket;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        location /cgi-bin/ {
                gzip off;
                root /home/blog/cgi-bin;
                fastcgi_pass unix:/var/fcgiwrap/165822549730942.sock/socket;
                fastcgi_param SCRIPT_FILENAME /home/blog$fastcgi_script_name;
        listen ssl;
        ssl_certificate /home/blog/ssl.combined;
        ssl_certificate_key /home/blog/ssl.key;

        location ^~ /ghost {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-Proto $scheme;

[file: /home/blog/public_html/config.production.json]

  "url": "",
  "server": {
    "port": 2369,
    "host": ""
  "database": {
    "client": "mysql",
    "connection": {
      "host": "localhost",
      "user": "blog",
      "password": "---REDACTED---",
      "database": "blog"
  "mail": {
    "transport": "Direct"
  "logging": {
    "transports": [
  "process": "systemd",
  "paths": {
    "contentPath": "/home/blog/public_html/content"

Not sure why you’re using fast-cgi as that is generally used for PHP sites. You’d do better letting Ghost setup Nginx first, even if you decide to modify later. Ditto SSL.

Use ghost setup…

The only reason fast_cgi is there is because I used the --bundle LEMP option with VirtualMin’s

So you are saying that I should install VirtualMin without a web server at all, and then use the ghost setup to install NGINX? The other option might be to use Virtualmin with its default LAMP stack, and then install NGINX using ghost setup. Run them on different ports…

The problem is that, as far as I know, I need one web server or other installed so that I can create Virtual Servers through the Virtualmin web interface, and then Ghost is installed / setup only on one (or more) of those virtual servers.

Any additional thoughts? Again very much appreciating your assistance!

Thanks for the linked ghost setup documentation Martin. Very useful! When I did the install I just used ghost install as recommended in the article linked in my original post.

Checking the directories that are supposed to be created, it seems that the system directory is missing (where the NGINX config should be located). That could explain this behaviour, but why would it have not been created when the rest of the Ghost file structure seems to be present?

OK, I have had some success. Not sure if it is entirely working, however there is a big improvement!

I replaced my location block in the file with the following:

        location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header Host $http_host;

Now the Ghost Admin page and the site BOTH load correctly. So far so good… I will keep you posted.

When I resolve the issue of the order of installation (on Virtualmin Virtual Servers not in general) then I will most likely reinstall the whole server, with the correct version (8.0) of MySQL, and allow the Ghost script to install NGINX and see what difference it makes. For now I am stuck on this point since I can’t do the Ghost install until I have created the Virtual Server, and for that I need Virtualmin installed, which itself requires the web server to operate… Catch-22 anyone? ;)

My main point of confusion at present is why doesn’t Ghost insert the correct lines into the NGINX conf file? I can’t imagine that this is related to the MySQL version, and must either be because the installation is on a Virtualmin Virtual Server, or because NGINX was already installed before by Virtualmin

Final question is why isn’t there a system directory created during the Ghost install? I have read that there should be one… perhaps this and the previous question are related.

Ghost won’t install Nginx; you need to do this with the package manager.

It does if you tell ghost install to do so, or use ghost setup nginx.

Not sure what you mean here, but a Ghost website is typically installed in a subdirectory off /var/www.

OK, thanks I understand that now.

OK, will try that on the next install and see what changes it makes. It would be helpful to have a description of what lines should be added to the NGINX conf file(s) in order for Ghost to work, so that I can check that they are correct. I can’t seem to find that anywhere in the documentation…?

I mean that I am installing this on a Virtualmin Virtual Server, which mean that nothing is put into the /var/www directory. Rather it is installed in the /home/virtual-server-name/public_html directory. In this case mine is /home/blog/public_html.
I have checked /var/www and nothing is installed there. The system directory I was referring to is the one mentioned in the Ghost-CLI document you linked to previously in this thread. In that document it details the root folder structure created by the ghost install as follows:

├── .config.[env].json  # The config file for your Ghost instance
├── .ghost-cli          # Utility system file for Ghost CLI, don't modify
├── /content            # Themes/images/content, not changed during updates
├── /current            # A symlink to the currently active version of Ghost
├── /system             # NGINX/systemd/SSL files on production installs
└── /versions           # Installed versions of Ghost available roll forward/back to

In my install location I can find all of the items except the /system directory. This is where Ghost puts the NGINX conf file(s) amongst other items (as I understand it). Perhaps this directory being absent is part of the problem?

If you cd /home/blog/public_html and run ghost install, this is where it is installed. However, the Nginx will be found in /etc/nginx/sites-available/blog.conf with a symbolic link in /etc/nginx/sites-enabled.

Since you’re using Virtualmin, there won’t be a specific guide, but the following guide should give you a very good idea. Indeed, if you have SSH access, I’d follow these steps rather than using Virtualmin to install services etc.

Since I configure Nginx, manually, sharing my file would be unhelpful. However, it should look similar to this with variations for SSL.

server {
    access_log /var/log/nginx/blog-delimo-ga.log;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $http_host;