Carrierwave + NGINX rails production images not displaying - ruby-on-rails

I've deployed a rails app that allows a user to upload a photo and have it display on another page, fairly simple. I tested it in development, the image uploads to the public folder and displays properly. In production and deployed the image uploads to the server but isn't being rendered on the page.
404 errors for only the images I've uploaded, path looks like this:
http://IP-OF-APP/uploads/blog/name-of-image.jpg
I read in another S.O. article mentioning setting a production config serve_static_files as true. This didn't fix the problem.
I thought it might be a server configuration with NGINX not picking up the uploads path here is my /sites-default/nginx.conf file.
upstream app {
server unix: /home/deploy/MYAPPNAME/shared/tmp/sockets/puma.sock fail_timeout=0;
}
server {
listen 80;
server_name IP_ADDRESS_OF_SERVER;
root /home/deploy/MYAPPNAME/public;
location #app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://app;
proxy_redirect off;
}
location ~ ^/(assets)/ {
root /home/deploy/MYAPPNAME/shared/public
gzip_static on;
expires max;
add_header Cache-Control public;
}
location ~ ^/uploads/ {
root /home/deploy/MYAPPNAME/shared/public;
expires 24h;
add_header Cache-Control public;
break;
}
location ~ ^/(fonts|system)/favicon.ico/robots.txt {
gzip_static on;
expires max;
add_header Cache-Control public;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 10;
}
I also thought it might be that the image isn't uploading to the server correctly so I SSH'd into the server and found the image uploaded living in:
home/deploy/MYAPPNAME/shared/public/uploads/blog/name-of-image.jpg
Also found what I assume to be the same image in:
home/deploy/MYAPPNAME/current/public/uploads/blog/name-of-image.jpg
My uploader looks like this:
class BlogUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
storage :file
def store_dir
'uploads/blog'
end
def extension_whitelist
%w(jpg jpeg gif png)
end
process :resize_to_fit => [825, 825]
#titles are validated to be unique
def filename
"#{model.title}"+".#{file.extension}" if original_filename.present?
end
end
using
Rails 4.2.6
Ruby 2.3.0
Carrierwave 0.11.0
EDIT
All of the other static images, CSS and JS are displaying and rendering properly.
The NGINX error when rending the upload image produces this:
2016/04/29 17:32:34 [error] 4993#0: *23 open() "/home/deploy/MYAPPNAME/shared/public/assets/uploads/blog/name-of-image.jpg" fails (2: no such file or directory), client: *******, server: SERVER_IP, request: "GET /assets/uploads/blog/name-of-image.jpg HTTP/1.1", host: "SERVER_IP"

From your log:
User is requesting:
/assets/uploads/blog/name-of-image.jpg
Nginx is looking for the image in:
/home/deploy/MYAPPNAME/shared/public/assets/uploads/blog/name-of-image.jpg
You confirmed that the image is in:
home/deploy/MYAPPNAME/shared/public/uploads/blog/name-of-image.jpg
Nginx is looking in public/assets/uploads, your file is in public/uploads.

Related

Nginx how to serve digest favicon file when request is for non-digest favicon.ico

I'm using Nginx in front of a rails app. Nginx is serving static assets from public with the following location rule:
location ~ ^/assets/ {
gzip_static on;
gzip off;
expires 1y;
add_header Cache-Control public;
add_header Last-Modified "";
add_header ETag "";
break;
}
This is working fine for the standard Rails digest assets but every now and then I get a request for /favicon.ico directly which will fail as it is a non-digest asset.
What I'd like to do is use a Nginx location directive to map/alias/rewrite /favicon.ico to /assets/favicons/favicon-somereallylongdigest.ico.
As rails will keep up to the last 3 assets there could be three files which match, I'm not worried about that and are happy to match any of the files found.
I'm not strong on Nginx config so any help is appreciated.
You could use a permanent redirect:
location = /favicon.ico {
return 301 $scheme://$host/assets/favicons/favicon-somereallylongdigest.ico;
}

Nginx + Puma + Sidekiq web interface not showing css styles

I have an angularJS web app running in a Nginx server that sends request to a Rails API running in a Puma server. I have integrated Sidekiq 5.2.8 and everything works great but the Sidekiq web interface.
In my Nginx config file, I have a rule to pass request to the API. Please find the whole nginx.conf document:
events {
worker_connections 1024;
}
http {
upstream api.development {
# Path to Puma SOCK file, as defined previously
server unix:/tmp/puma.sock fail_timeout=0;
}
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# set client body size to 10M #
client_max_body_size 10M;
gzip on;
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
#charset koi8-r;
#access_log logs/host.access.log main;
root /Users/Rober/Projects/domain/dev/domain/app;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
# Proxy requests to the backoffice Rails API
location /api {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
#proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
rewrite ^/api(.*) /$1 break;
proxy_pass http://api.development;
}
# Rule to proxy the sidekiq web UI
location /sidekiq {
proxy_pass http://api.development;
}
# Expire rules for static content
# RCM: WPO
# Images
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
root /Users/Rober/Projects/domain/dev/domain/app;
expires 1w;
add_header Cache-Control "public";
}
# This rule is the root cause of the problems with the sidekiq css
# I have commented it for testing purposes
# CSS and Javascript
#location ~* \.(?:css|js)$ {
# root /Users/Rober/Projects/domain/dev/domain/app;
# expires 1w;
# add_header Cache-Control "public";
#}
# I have replaced the previous location above for this as suggested by #Beena Shetty.
location ~* \.(?:css|js)$ {
add_header X-debug-message "Into the location css" always;
if ($uri !~* "^/sidekiq/\w*(.*)+$") {
add_header X-debug-message "Into the location css if" always;
root /Users/Rober/Projects/domain/dev/domain/app;
expires 1w;
add_header Cache-Control "public";
}
}
# cache.appcache, your document html and data
location ~* \.(?:manifest|appcache|html?|xml|json)$ {
root /Users/Rober/Projects/domain/dev/domain/app;
expires -1;
}
}
include servers/*;
}
In Rails:
routes:
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
I have included next rule in Nginx config file and now when I request http://localhost/sidekiq I can see the web interface and navigate, but still cannot see the styles.
location /sidekiq {
proxy_pass http://api.development;
}
See screenshot.
The dev tools shows that when I load sideqik is trying to get bootstrap.css and some other css and javascript in http://localhost/sidekiq/stylesheets/bootstrap.css
What am I missing?
UPDATE:
I have found out the root cause of the problem in my nginx.conf. I have next rule that set a cache expiration time for performance purposes. If I comment this code, everything works. But how can I have both things living together?
CSS and Javascript
location ~* \.(?:css|js)$ {
root /Users/Rober/Projects/domain/dev/domain/app;
expires 1w;
add_header Cache-Control "public";
}
UPDATE 2: Just in case the problem comes from another point, I have included my whole nginx conf.
Now, with the provided config, the expiration rules in my web app are still working, but the css in the sidekiq webapp do not.
I have included two headers as debug. One when the server is accessing the location rule and the second when the server is accessing inside the if condition. When I request my home page with localhost and I check the request for my own css, such as app.css, I can see the header X-debug-message: Into the location css if, which is right.
If I request sidekiq with localhost/sidekiq I still get 404 error for css, let´s say http://localhost/sidekiq/stylesheets/bootstrap.css and I can see the header X-debug-message: Into the location css.
Current conclusions:
As soon as I include the location ~* .(?:css|js)$ rule, sidekiq css stops working. Even if the rule is empty, like:
location ~* \.(?:css|js)$ {
}
As soon as I delete or comment the whole rule, the sidekiq css works perfectly, but unfortunately this is not compatible with the expires rules that we need to include for performance purposes.
Try this:
location ~* \.(?:css|js)$ {
if ($uri !~* "^/sidekiq/\w*(.*)+$"){
root /Users/Rober/Projects/domain/dev/yanpy/app;
expires 1w;
add_header Cache-Control "public";
}
}
I wasn't able to find a fix for this, so I hacked it with the following method: I copied sidekiq's assets to the public folder and it started working ( since they're referenced by the UI ).
|- images
|-- favicon.io
|-- logo.png
|-- status.png
|- javascripts
|-- application.js
|-- dashboard.js
|- stylesheets
|-- application.css
|-- application-rtl.css
|-- application-dark.css
|-- application-rtl.min.css
|-- bootstrap.css
Mainly, these files: https://github.com/mperham/sidekiq/tree/master/web/assets

Streaming mp4 in Chrome with rails, nginx and send_file

I can't for the life of me stream a mp4 to Chrome with a html5 <video> tag. If I drop the file in public then everything is gravy and works as expected. But if I try to serve it using send_file, pretty much everything imaginable goes wrong. I am using a rails app that is proxied by nginx, with a Video model that has a location attribute that is an absolute path on disk.
At first I tried:
def show
send_file Video.find(params[:id]).location
end
And I was sure I would be basking in the glory that is modern web development. Ha. This plays in both Chrome and Firefox, but neither seek and neither have any idea how long the video is. I poked at the response headers and realized that Content-Type is being sent as application/octet-stream and there is no Content-Length set. Umm... wth?
Okay, I guess I can set those in rails:
def show
video = Video.find(params[:id])
response.headers['Content-Length'] = File.stat(video.location).size
send_file(video.location, type: 'video/mp4')
end
At this point everything works pretty much as expected in Firefox. It knows how long the video is and seeking works as expected. Chrome appears to know how long the video is (doesn't show timestamps, but seek bar looks appropriate) but seeking doesn't work.
Apparently Chrome is pickier than Firefox. It requires that the server respond with a Accept-Ranges header with value bytes and respond to subsequent requests (that happen when the users seeks) with 206 and the appropriate portion of the file.
Okay, so I borrowed some code from here and then I had this:
video = Video.find(params[:id])
file_begin = 0
file_size = File.stat(video.location).size
file_end = file_size - 1
if !request.headers["Range"]
status_code = :ok
else
status_code = :partial_content
match = request.headers['Range'].match(/bytes=(\d+)-(\d*)/)
if match
file_begin = match[1]
file_end = match[2] if match[2] && !match[2].empty?
end
response.header["Content-Range"] = "bytes " + file_begin.to_s + "-" + file_end.to_s + "/" + file_size.to_s
end
response.header["Content-Length"] = (file_end.to_i - file_begin.to_i + 1).to_s
response.header["Accept-Ranges"]= "bytes"
response.header["Content-Transfer-Encoding"] = "binary"
send_file(video.location,
:filename => File.basename(video.location),
:type => 'video/mp4',
:disposition => "inline",
:status => status_code,
:stream => 'true',
:buffer_size => 4096)
Now Chrome attempts to seek, but when you do the video stops playing and never works again until the page reloads. Argh. So I decided to play around with curl to see what was happening and I discovered this:
$ curl --header "Range: bytes=200-400" http://localhost:8080/videos/1/001.mp4
ftypisomisomiso2avc1mp41 �moovlmvhd��#��trak\tkh��
$ curl --header "Range: bytes=1200-1400" http://localhost:8080/videos/1/001.mp4
ftypisomisomiso2avc1mp41 �moovlmvhd��#��trak\tkh��
No matter the byte range request, the data always starts from the beginning of the file. The appropriate amount of bytes is returned (201 bytes in this case), but it's always from the beginning of the file. Apparently nginx respects the Content-Length header but ignores the Content-Range header.
My nginx.conf is untouched default:
user www-data;
worker_processes 4;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
and my app.conf is pretty basic:
upstream unicorn {
server unix:/tmp/unicorn.app.sock fail_timeout=0;
}
server {
listen 80 default deferred;
root /vagrant/public;
try_files $uri/index.html $uri #unicorn;
location #unicorn {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HOST $http_host;
proxy_redirect off;
proxy_pass http://unicorn;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 5;
}
First I tried the nginx 1.4.x that comes with Ubuntu 14.04, then tried 1.7.x from a ppa - same results. I even tried apache2 and had exactly the same results.
I would like to reiterate that the video file is not the problem. If I drop it in public then nginx serves it with the appropriate mime types, headers and everything needed for Chrome to work properly.
So my question is a two-parter:
Why doesn't nginx/apache handle all this stuff automagically with send_file (X-Accel-Redirect/X-Sendfile) like it does when the file is served statically from public? Handling this stuff in rails is so backwards.
How the heck can I actually use send_file with nginx (or apache) so that Chrome will be happy and allow seeking?
Update 1
Okay, so I thought I'd try to take the complication of rails out of the picture and just see if I could get nginx to proxy the file correctly. So I spun up a dead-simple nodjs server:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {
'X-Accel-Redirect': '/path/to/file.mp4'
});
res.end();
}).listen(3000, '127.0.0.1');
console.log('Server running at http://127.0.0.1:3000/');
And chrome is happy as a clam. =/ curl -I even shows that Accept-Ranges: bytes and Content-Type: video/mp4 is being inserted by nginx automagically - as it should be. What could rails be doing that's preventing nginx from doing this?
Update 2
I might be getting closer...
If I have:
def show
video = Video.find(params[:id])
send_file video.location
end
Then I get:
$ curl -I localhost:8080/videos/1/001.mp4
HTTP/1.1 200 OK
Server: nginx/1.7.9
Date: Sun, 18 Jan 2015 12:06:38 GMT
Content-Type: application/octet-stream
Connection: keep-alive
Status: 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Disposition: attachment; filename="001.mp4"
Content-Transfer-Encoding: binary
Cache-Control: private
Set-Cookie: request_method=HEAD; path=/
X-Meta-Request-Version: 0.3.4
X-Request-Id: cd80b6e8-2eaa-4575-8241-d86067527094
X-Runtime: 0.041953
And I have all the problems described above.
But if I have:
def show
video = Video.find(params[:id])
response.headers['X-Accel-Redirect'] = video.location
head :ok
end
Then I get:
$ curl -I localhost:8080/videos/1/001.mp4
HTTP/1.1 200 OK
Server: nginx/1.7.9
Date: Sun, 18 Jan 2015 12:06:02 GMT
Content-Type: text/html
Content-Length: 186884698
Last-Modified: Sun, 18 Jan 2015 03:49:30 GMT
Connection: keep-alive
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: request_method=HEAD; path=/
ETag: "54bb2d4a-b23a25a"
Accept-Ranges: bytes
And everything works perfectly.
But why? Those should do exactly the same thing. And why doesn't nginx set Content-Type automagically here like it does for the simple nodejs example? I have config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' set. I have moved it back and forth between application.rb and development.rb with the same results. I guess I never mentioned... this is rails 4.2.0.
Update 3
Now I've changed my unicorn server to listen on port 3000 (since I already changed nginx to listen on 3000 for the nodejs example). Now I can make requests directly to unicorn (since it's listening on a port and not a socket) so I have found that curl -I directly to unicorn shows that no X-Accel-Redirect header is sent and just curling unicorn directly actually sends the file. It's like send_file isn't doing what it's supposed to.
I finally have the answers to my original questions. I didn't think I'd ever get here. All my research had lead to dead-ends, hacky non-solutions and "it just works out of the box" (well, not for me).
Why doesn't nginx/apache handle all this stuff automagically with send_file (X-Accel-Redirect/X-Sendfile) like it does when the file is served statically from public? Handling this stuff in rails is so backwards.
They do, but they have to be configured properly to please Rack::Sendfile (see below). Trying to handle this in rails is a hacky non-solution.
How the heck can I actually use send_file with nginx (or apache) so that Chrome will be happy and allow seeking?
I got desperate enough to start poking around rack source code and that's where I found my answer, in the comments of Rack::Sendfile. They are structured as documentation that you can find at rubydoc.
For whatever reason, Rack::Sendfile requires the front end proxy to send a X-Sendfile-Type header. In the case of nginx it also requires a X-Accel-Mapping header. The documentation also has examples for apache and lighttpd as well.
One would think the rails documentation could link to the Rack::Sendfile documentation since send_file does not work out of the box without additional configuration. Perhaps I'll submit a pull request.
In the end I only needed to add a couple lines to my app.conf:
upstream unicorn {
server unix:/tmp/unicorn.app.sock fail_timeout=0;
}
server {
listen 80 default deferred;
root /vagrant/public;
try_files $uri/index.html $uri #unicorn;
location #unicorn {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HOST $http_host;
proxy_set_header X-Sendfile-Type X-Accel-Redirect; # ADDITION
proxy_set_header X-Accel-Mapping /=/; # ADDITION
proxy_redirect off;
proxy_pass http://localhost:3000;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 5;
}
Now my original code works as expected:
def show
send_file(Video.find(params[:id]).location)
end
Edit:
Although this worked initially, it stopped working after I restarted my vagrant box and I had to make further changes:
upstream unicorn {
server unix:/tmp/unicorn.app.sock fail_timeout=0;
}
server {
listen 80 default deferred;
root /vagrant/public;
try_files $uri/index.html $uri #unicorn;
location ~ /files(.*) { # NEW
internal; # NEW
alias $1; # NEW
} # NEW
location #unicorn {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HOST $http_host;
proxy_set_header X-Sendfile-Type X-Accel-Redirect;
proxy_set_header X-Accel-Mapping /=/files/; # CHANGED
proxy_redirect off;
proxy_pass http://localhost:3000;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 5;
}
I find this whole thing of mapping one URI to another and then mapping that URI to a location on disk to be totally unnecessary. It's useless for my use case and I'm just mapping one to another and back again. Apache and lighttpd don't require it. But at least it works.
I also added Mime::Type.register('video/mp4', :mp4) to config/initializers/mime_types.rb so the file is served with the correct mime type.

Content-Length Header missing from Nginx-backed Rails app

I've a rails app that serves large static files to registered users. I was able to implement it by following the excellent guide here: Protected downloads with nginx, Rails 3.0, and #send_file. The downloads and everything else is working great, but there is just this problem - The Content-Length header isn't being sent.
It's okay for small files, but it gets really frustrating when large files are downloaded, since download managers and browsers don't show any progress. How can I fix this? Do I have to add something to my nginx configuration or do I have to pass along some other option to the send_file method in my rails controller? I have been searching online for quite some time but have been unsuccessful. Please Help! Thanks!
Here's my nginx.conf:
upstream unicorn {
server unix:/tmp/unicorn.awesomeapp.sock fail_timeout=0;
}
server {
listen 80 default_server deferred;
# server_name example.com;
root /home/deploy/apps/awesomeapp/current/public;
location ~ /downloads/(.*) {
internal;
alias /home/deploy/uploads/$1;
}
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri #unicorn;
location #unicorn {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Sendfile-Type X-Accel-Redirect;
proxy_set_header X-Accel-Mapping /downloads/=/home/deploy/uploads/;
proxy_pass http://unicorn;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 20M;
keepalive_timeout 10;
}
Okay, here's something. I don't know if it's the right way or not but I was able to fix the issue by manually sending the Content-Length Header from my Rails Controller. Here's what I'm doing:
def download
#file = Attachment.find(params[:id])
response.headers['Content-Length'] = #file.size.to_s
send_file(#file.path, x_sendfile: true)
end
nginx should be automatically able to set the header. There must be something that I'm missing; but until I find a 'proper' solution, I guess this will have to do.
P.S: The Header needs to be a string to work properly with some webservers, hence the .to_s

Nginx raises 404 when using format => 'js'

I upload images to my App using Ajax and an Iframe. In Development everything works like a charm. But in production Nginx suddenly raises a 404 error. When I look into the log, the request never hits the Rails app. So I guess it has something to do with my Nginx configuration (maybe gzip compression).
The failing requests is send to "/images.js".
Any ideas on how to solve this? Google couldn't help me...
My Nginx config:
server {
listen 80;
server_name www.myapp.de;
root /var/www/apps/myapp/current/public; # <--- be sure to point to 'public'!
passenger_enabled on;
rails_env production;
# set the rails expires headers: http://craigjolicoeur.com/blog/setting-static-asset-expires-headers-with-nginx-and-passenger
location ~* \.(ico|css|js|gif|jp?g|png)(\?[0-9]+)?$ {
expires max;
break;
}
gzip on;
gzip_http_version 1.0;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/html text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# make sure gzip does not lose large gzipped js or css files
# see http://blog.leetsoft.com/2007/7/25/nginx-gzip-ssl
gzip_buffers 16 8k;
# this rewrites all the requests to the maintenance.html
# page if it exists in the doc root. This is for capistrano?^?^?s
# disable web task
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}
# Set the max size for file uploads to 10Mb
client_max_body_size 10M;
error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/apps/myapp/current/public;
}
}
nginx will serve the request for /images.js from your root /var/www/apps/myapp/current/public since it matches
location ~* \.(ico|css|js|gif|jp?g|png)(\?[0-9]+)?$ {
expires max;
break;
}
(the break directive only applies to rewrite rules afaik so it can be removed)
If you want to serve /images.js from rails you need to enable rails for that location.
It's worth noting that any .json requests will get caught if you use the regex above. Add a $ to avoid that.
location ~* \.(ico|css|js$|gif|jp?g|png)(\?[0-9]+)?$ {
expires max;
break;
}
For this purpose I use this location directives:
location ~* ^.+\.(jpg|jpeg|gif|png|css|swf|ico|mov|flv|fla|pdf|zip|rar|doc|xls)$
{
expires 12h;
add_header Cache-Control private;
}
location ~* ^/javascripts.+\.js$
{
expires 12h;
add_header Cache-Control private;
}
I ran into a very similar issue, and put format.json instead of format.js - this made a URL that had a .json extension, but allowed me to not modify my Nginx config.

Resources