Ghost blog with static site, no media, nginx issue?

Hi,

We had a blog running on a main domain say example.com. As we now want to have a static site on the homepage, I moved the blog to a sub-directory example.com/blog following the instructions given in response to other questions on the same topic here.

The blog’s text content moved fine and the blog posts as well as the Ghost admin page is accessible from the example.com/blog sub-directory. However, I have an issue with all the media content. It appears media is being served from the wrong location.

Here is the new directory structure:

/var/www/
|---> html    (Static site)
|---> ghost

Here is the output of ghost ls:

│ Location       │ Version │ Status               │ URL                      │ Port │
/var/www/ghost │ 2.31.0  │ running (production) │ https: //example.com/blog │ 2369 

Here is the NGINX error log showing errors opening images. I can’t figure out why NGINX is trying to serve ghost images from the html directory?

2019/09/11 17:50:06 [error] 2565#2565: *1 open() “/var/www/html/content/images/2019/08/awesome_image.png” failed (2: No such file or directory), client: 131.111.5.150, server: example. com, request: “GET /content/images/2019/08/awesome_image.png HTTP/2.0”, host: “example. com”, referrer: “https ://example.com/blog/”

2019/09/11 18:09:03 [error] 2565#2565: *12 upstream prematurely closed connection while reading response header from upstream, client: 131.111.5.150, server: example. com, request: “POST /blog/ghost/api/canary/admin/images/upload/ HTTP/2.0”, upstream: “http ://127.0.0.1:2369/blog/ghost/api/canary/admin/images/upload/”, host: “example. com”, referrer: “https ://example.com/blog/ghost/”

Here is my NGINX conf file.

server_name example.com;
root /var/www/ghost/system/nginx-root;

ssl_certificate /etc/letsencrypt/example.com/fullchain.cer;
ssl_certificate_key /etc/letsencrypt/example.com/example.com.key;
include /etc/nginx/snippets/ssl-params.conf;

# Rewrite previous locations of posts before blog moved to subdirectory
location /old_post {
    return 301 /blog/old_post ;
}
# Rule for Ghost location
location /blog/ {
    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;
    proxy_pass http://127.0.0.1:2369;
}
# Rule for www requests
location / {
    root /var/www/html;
    index index.html;
}
location ~ /.well-known {
    allow all;
}

What am I doing wrong?

Ghost will have stored all the paths to your images relative to the old path. Unless you’ve gone through and changed them, this is entirely expected behaviour.

You have the following options:

  • manually update all your image paths
  • automate the process: export all your content as JSON, run a find and replace tool on the image paths, and then reimport all the content
  • add an extra rule to nginx that forwards all requests that hit /content/images to /blog/content/images

I’d probably opt for the last one, unless you need the path /content/images for your static site.

1 Like

Thanks a lot for the feedback Hannah!

Huh, that explains things; I would have expected that changing the ghost url would also have modified the old image paths to the new path.

So, following your advice I added a new location to my NGINX config:

location /content/images/ {
    return 301 /blog/content/images/;
}

That improves things somewhat. Images in the body of the posts are now being found. Hurray!

But, for some reason Ghost is still not serving the featured image. (I’m not sure if this is the correct way to refer to this, it’s not an in-post image, rather it’s the main “header” image of a post. E.g. what this post is referring to.)

Possibly related, but when I try to add/modify the publication logo, or post’s header image, favicon etc from the Ghost admin page then I am still getting access errors like so:

2019/09/12 10:12:52 [error] 7561#7561: *155 upstream prematurely closed connection while reading response header from upstream, client: 131.111.5.150, server: example.com, request: "POST /blog/ghost/api/canary/admin/images/upload/ HTTP/2.0", upstream: "http ://127.0.0.1:2369/blog/ghost/api/canary/admin/images/upload/", host: "example .com", referrer: "https ://example .com/blog/ghost/"

It would seem that some part of Ghost is still trying to access the old location?

If it helps here is the output of ghost log showing an error:

+ sudo systemctl is-active ghost_example-com
[2019-09-12 10:12:57] WARN Can't connect to the bootstrap socket (localhost 8000) ECONNREFUSED
[2019-09-12 10:12:57] WARN Tries: 1
[2019-09-12 10:12:57] WARN Retrying...
[2019-09-12 10:12:57] WARN Can't connect to the bootstrap socket (localhost 8000) ECONNREFUSED
[2019-09-12 10:12:57] WARN Tries: 2
[2019-09-12 10:12:57] WARN Retrying...
[2019-09-12 10:12:58] WARN Can't connect to the bootstrap socket (localhost 8000) ECONNREFUSED
[2019-09-12 10:12:58] INFO Ghost boot 4.249s
[2019-09-12 10:14:18] WARN Cookie ghost-members-ssr not found
[2019-09-12 10:14:18] INFO "GET /blog/awesome_blog_post" 301 17ms
[2019-09-12 10:14:22] WARN Cookie ghost-members-ssr not found
[2019-09-12 10:14:23] ERROR

NAME: InternalServerError
CODE: IMAGE_SIZE_URL
MESSAGE: URL empty or invalid.

level: critical

"/content/images/2019/08/awesome_image.png"
InternalServerError: URL empty or invalid.
    at new InternalServerError (/var/www/ghost/versions/2.31.0/node_modules/ghost-ignition/lib/errors/index.js:77:23)
    at _imageSizeFromUrl.then.catch (/var/www/ghost/versions/2.31.0/core/server/lib/image/image-size.js:150:31)
    at tryCatcher (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/util.js:16:23)
    at /var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/catch_filter.js:34:37
    at tryCatcher (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:517:31)
    at Promise._settlePromise (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:574:18)
    at Promise._settlePromise0 (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:619:10)
    at Promise._settlePromises (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:695:18)
    at _drainQueueStep (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:138:12)
    at _drainQueue (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:131:9)
    at Async._drainQueues (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:147:5)
    at Immediate.Async.drainQueues (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:810:20)
    at tryOnImmediate (timers.js:768:5)
    at processImmediate [as _immediateCallback] (timers.js:745:5)

[2019-09-12 10:14:23] ERROR

NAME: InternalServerError
CODE: IMAGE_SIZE_URL
MESSAGE: URL empty or invalid.

level: critical

"/content/images/2019/08/awesome_image.png"
InternalServerError: URL empty or invalid.
    at new InternalServerError (/var/www/ghost/versions/2.31.0/node_modules/ghost-ignition/lib/errors/index.js:77:23)
    at _imageSizeFromUrl.then.catch (/var/www/ghost/versions/2.31.0/core/server/lib/image/image-size.js:150:31)
    at tryCatcher (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/util.js:16:23)
    at /var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/catch_filter.js:34:37
    at tryCatcher (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:517:31)
    at Promise._settlePromise (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:574:18)
    at Promise._settlePromise0 (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:619:10)
    at Promise._settlePromises (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/promise.js:695:18)
    at _drainQueueStep (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:138:12)
    at _drainQueue (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:131:9)
    at Async._drainQueues (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:147:5)
    at Immediate.Async.drainQueues (/var/www/ghost/versions/2.31.0/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:810:20)
    at tryOnImmediate (timers.js:768:5)
    at processImmediate [as _immediateCallback] (timers.js:745:5)

[2019-09-12 10:14:23] WARN

NAME: HelperWarning
CODE: SLOW_GET_HELPER
MESSAGE: {{#get}} helper took 308ms to complete

level: normal

ERROR DETAILS:
    {"api":"v2.postsPublic.browse","apiOptions":{"limit":"3"},"returnedRows":3}


[2019-09-12 10:14:23] WARN

NAME: HelperWarning
CODE: SLOW_GET_HELPER
MESSAGE: {{#get}} helper took 497ms to complete

level: normal

ERROR DETAILS:
    {"api":"v2.postsPublic.browse","apiOptions":{"include":"tags","limit":"3","filter":"tags:[awesome-tag]+id:-5c484e80a04c066da0b940ef"},"returnedRows":1}


[2019-09-12 10:14:23] WARN

NAME: HelperWarning
CODE: SLOW_GET_HELPER
MESSAGE: {{#get}} helper took 320ms to complete

level: normal

ERROR DETAILS:
    {"api":"v2.tagsPublic.browse","apiOptions":{"order":"count.posts desc","include":"count.posts","limit":"all"},"returnedRows":3}

[2019-09-12 10:14:23] INFO "GET /blog/content/images/" 404 7ms
[2019-09-12 10:14:23] INFO "GET /blog/awesome_blog_post/" 200 920ms
[2019-09-12 10:28:15] INFO "GET /blog/ghost/api/canary/admin/posts/?limit=30&page=1&filter=status%3A%5Bdraft%2Cscheduled%2Cpublished%5D" 200 265ms
[2019-09-12 10:28:19] INFO "GET /blog/ghost/api/canary/admin/settings/?type=blog%2Ctheme%2Cprivate%2Cmembers" 200 51ms

Changing the config changes all the locations where the static value is output, but it won’t run a migration on the values already in your database.

Without doubt - Ghost hasn’t been designed to support the case of changing the URL to/from having a subdirectory. To be completely honest with you supporting subdirectories at all is such a time sink there’s been discussions about whether it’s worthwhile.

Just to confirm - you’ve updated the config, and restarted ghost?
And now

  1. featured images still won’t render
    and
  2. image uploads on the settings screens are failing?

Do other uploads in the post content or uploading new post feature images work?

I can kinda see how logically the settings uploads might fail in this case (it’s a bug, but it makes sense) buuuuut I’m confused about the featured images.

Could you possibly share your actual url? Or else - LOADS more of your logs :grimacing:?

Yes, with hindsight I would have installed everything to the subdirectory from the very beginning. Didn’t think the blog would evolve to a company though and hence need a separate static site :sweat_smile:

Just to confirm - you’ve updated the config, and restarted ghost?

Yup, restarted ghost umpteen times. Unless there is something I’m missing in doing the restart; I just issue “ghost restart”

Do other uploads in the post content or uploading new post feature images work?

Hmmm, just tried to do a fresh post with image upload in the post content. Got “Request was rejected due to server error” :worried: So yes, featured images don’t render, image uploads via settings fail and in-post image uploads fail too.

Ok here’s the site: https://klevoya.com

Happy to provide more logs as well, just let me know which ones?

I’m not sure what’s happening with your uploads, but this bit of nginx is rewriting anything inside of /content/images to exactly /blog/content/images/, losing the filename, extension, etc.

location /content/images/ {
    return 301 /blog/content/images/;
}

I think what you want is:

location ~ /content/images/(.*)$ {
    rewrite ^ /blog/content/images/$1?$args permanent;
} 

There’s probably a more elegant way to write this, but at least it’s more correct

Hopefully that should get rid of all broken images.

For the image uploads I’ve not seen your specific error before. There’s a stackoverflow post here that indicates it might be something in your main nginx.conf file: node.js - Nginx upstream prematurely closed connection while reading response header from upstream, for large requests - Stack Overflow

Thanks for the modified nginx location syntax, I’ll update my conf file to match.

Good pointer about the upload issue! I’ll dig into that stackoverflow post and see if the suggestions offered there help in my situation.

Don’t bother with a redirect - just put the new ROOT inside the old location block.
It will be faster and handled transparently on the server.

E.g.

location /content/images {
    root /.../blog;
}

Thanks for that.

After several attempts to debug the upload issue, it seemed that there is a chance that the problem could also be with the nodejs server.

I know nothing about nodejs, so at that point I threw in the towel and decided to just bite the bullet and completely reinstall my Ghost setup from scratch on the blog/ sub-directory… After taking suitable backups of course! (Even with backups I found that I still had to re-upload the images.)

For those who attempt the same thing, I found that I had to completely delete the MySQL DB as even after the ghost uninstall the DB still had the records from the previous installation. To do that I followed the steps here.

The site is now finally working as I want it.

Conclusion? If you are thinking of changing your existing ghost site to a sub-directory, be warned: thar be dragons! :japanese_ogre: