I'm building a single-page application using Devise and Devise Token Auth for authentication. Everything works fine in development, but in production, clicking the email confirmation link results in a 500 Internal Server Error, and the confirmation fails.
In production, the app is running on AWS ECS Fargate. The request flow is as follows:
Route 53 → ALB → Nginx (reverse proxy) → Rails API
Both Nginx and Rails are containerized and deployed together in a single ECS task. They communicate via a shared Unix domain socket.
Here’s the problematic flow:
- After signing up, a confirmation email is sent to the user.
- The email contains a link to an endpoint like:
https://api.my-service.net/api/v1/auth/confirmation?config=default&confirmation_token=hogefugabar - When clicked in production, it causes a 500 error and does not redirect to the expected frontend route (
https://my-service.net/account_confirmation_success=true).
Some additional details:
api.my-service.netis correctly configured in Route 53 with an A record pointing to the ALB.- In development, this flow works as expected.
- I suspect password reset emails may have the same issue, although I haven't verified that yet.
- The Nginx reverse proxy works correctly for other API requests, so I believe the Rails app is reachable and responsive.
What could be causing this 500 error only in production when hitting the confirmation endpoint?
Any guidance or suggestions would be greatly appreciated.
Ruby: 3.3.7 Rails: 7.2.2.1 Rails is in API mode.
The relevant code is as follows:
# user.rb
devise :database_authenticatable, :registerable,
:recoverable, :validatable, :confirmable
# :lockable, :timeoutable, :trackable, :omniauthable, :rememberable
include DeviseTokenAuth::Concerns::User
# confirmation_instructions.html.erb
<p><%= @email %>, </p>
<p>Thank you for registering your account!</p>
<p>Please click the link below to complete your registration. </p><br>
<p>
<%= link_to “Verify your account”, confirmation_url(@resource, {
confirmation_token: @token,
config: message[‘client-config’].to_s,
}).html_safe %>
</p>
<p>The link is valid for 3 days. Please respond as soon as possible. </p><br>
# devise_token_auth.rb
# DEFAULT_CONFIRM_SUCCESS_URL and DEFAULT_PASSWORD_RESET_URL are passed as environment variables in the ECS task definition.
config.default_confirm_success_url = ENV["DEFAULT_CONFIRM_SUCCESS_URL"] || "http://localhost:3000"
config.default_password_reset_url = ENV["DEFAULT_PASSWORD_RESET_URL"] || "http://localhost:3000/password-reset"
config.redirect_whitelist = [
"https://www.my-service.net",
"https://www.my-service.net/password-reset"
]
# production.rb
config.action_mailer.default_url_options = { host: "https://api.my-service.net" }
config.hosts << "api.my-service.net"
I'm having trouble figuring out why the confirmations_controller.rb in Davise Token Auth is returning a 500 error. The request from Next.js to Rails is successful, and I've double-checked that the prod infrastructure has the correct settings, so I assume that the GET /api/v1/auth/confirmation is reaching its destination but is failing for some reason.
I'll add the following.
# cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:3000", "https://www.my-service.net", "https://api.my-service.net"
resource "*",
headers: :any,
expose: [
"access-token", "expiry", "token-type", "uid", "client",
"current-page", "page-items", "total-count", "total-pages"
],
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
# nginx.conf
upstream api {
server unix:///app/tmp/sockets/puma.sock;
}
server {
listen 80;
server_name localhost;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
root /app/public;
client_max_body_size 100m;
keepalive_timeout 5;
location = /healthcheck {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
error_page 404 /404.html;
error_page 502 503 504 505 /500.html;
location = /404.html {
internal;
}
location = /500.html {
internal;
}
try_files $uri @api;
location @api {
proxy_pass http://api;
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_intercept_errors on;
}
}
nginx container logs on cloudwatch following
"GET /api/v1/auth/confirmation?config=default&confirmation_token=ZhpLxqjmVbs4kgxS_QfH HTTP/1.1" 500 26 "-" "Mozilla/5.0
[error] 14#14: *99 open() "/app/public/404.html" failed (2: No such file or directory), client: 10.0.1.23, server: localhost, request: "GET /favicon.ico HTTP/1.1", upstream: "http://unix:///app/tmp/sockets/puma.sock/favicon.ico", host: "api.my-service.net", referrer: "https://api.my-service.net/api/v1/auth/confirmation?config=default&confirmation_token=ZhpLxqjmVbs4kgxS_QfH"