Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Dancer2 App Deployment through Apache Proxy

by choroba (Cardinal)
on May 20, 2025 at 15:53 UTC ( [id://11165066]=perlquestion: print w/replies, xml ) Need Help??

choroba has asked for the wisdom of the Perl Monks concerning the following question:

I wrote a Dancer2 application to be used by about 10 people from work. It ran fine on http://localhost, but the pages needed to be available from the outside world. Our IT admin told me the standard practice is to run it on a virtual host and use Apache's mod_proxy.

Dancer2::Manual::Deployment even mentions such a possibility, so I followed the instructions. I included

behind_proxy: 1
to config.yml, and the admin configured the proxy similarly as shown in the manual (see Using-Apache's-mod_proxy of Dancer2::Manual::Deployment.

I was given a prefix under which the app would be running, e.g. https://example.com/prefix/login.

But it doesn't work correctly. For example, the css files are ignored. Or rather, they can't be found.

The main.tt template was created with the following link (part of the initial scaffolding):

<link rel="stylesheet" href="<% request.uri_base %>/css/style.css">

But the request.uri_base doesn't expand to /prefix, it remains empty.

Similarly, I use Dancer2::Plugin::Auth::Tiny for user authentication. Again, I almost copied verbatim the synopsis code:

get '/' => needs login => sub { # ... }; get '/login' => sub { template 'login' }; post '/login' => sub { my $user = body_parameters->get('uname'); if (is_valid($user, body_parameters->get('psw'))) { session(user => $user); redirect('/') } else { template index => {error => 'Wrong username or password'} } };

But again, when I try to open the page, the authentication plugin redirects the browser to /login instead of /prefix/login.

I was able to fix the problems by

  • removing the request.uri_base from the templates
    <link rel="stylesheet" href="css/style.css">

  • by configuring the plugin to use the prefix
    plugins: Auth::Tiny: login_route: /prefix/login

  • by not using the / path, instead the main page is now /menu (because redirect('/') ignored the prefix, again, so I have to do redirect('menu') — i.e. no slash).

Mentioning the prefix in the config definitely feels wrong. Hardcoding the prefix into the app? It also means the app can't be run locally on localhost for testing anymore.

How should I properly write the app, configure it, and configure Apache to make it work both locally and in production, without hardcoding the prefix anywhere in the app?

Interestingly, all Python flask and whatever apps written by other colleagues run as written without problems.

map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

Replies are listed 'Best First'.
Re: Dancer2 App Deployment through Apache Proxy
by hippo (Archbishop) on May 21, 2025 at 08:30 UTC
    the admin configured the proxy similarly as shown in the manual

    In your shoes I would be double-checking this. Most of what you have told us here is symptomatic of either the ProxyPassReverse not being set or the proxy_http and/or proxy_html modules not being loaded or correctly configured. I note that the doc to which you linked makes no mention at all of the proxy_html module. You can check by simply replacing your entire application with a light webserver which just sends back a static page referencing a stylesheet reachable by an absolute path. If that fails to load the stylesheet then the problem lies with the proxy and not with your application/framework.


    🦛

Re: Dancer2 App Deployment through Apache Proxy
by Corion (Patriarch) on May 21, 2025 at 09:01 UTC

    This is my setup for Mojolicious, but I think that the hoops I have to jump through there are not too different from the hoops for Dancer2:

    My setup runs locally on http://localhost:3001 and gets remotely exposed as https://datenzoo.de/notes.

    The Apache config is:

    ProxyVia Block SSLProxyVerify none SSLProxyCheckPeerCN off SSLProxyCheckPeerName off SSLProxyCheckPeerExpire off <Location "/notes"> ProxyPass https://localhost:3001 RequestHeader set X-Forwarded-Host 'datenzoo.de' Header edit Location ^https(\:\/\/localhost/.*)$ https://datenzoo.de/n +otes/$1 Header edit Location ^https(\:\/\/localhost)$ https://datenzoo.de/note +s/ </Location>

    In my Mojolicious code, I have to munge each request to have the proper value for the (proxied) URL:

    # If we are behind a reverse proxy, prepend our path if ( my $path = $ENV{MOJO_REVERSE_PROXY} ) { my $path_uri = Mojo::URL->new($path); my @path_parts = grep /\S/, split m{/}, $path_uri->path; app->hook( before_dispatch => sub( $c ) { my $url = $c->req->url; warn "URL is <$url>"; my $base = $url->base; unshift @{ $base->path }, @path_parts; $base->path->trailing_slash(1); $url->path->leading_slash(0); #$url->scheme($path_uri->protocol); $base->scheme($path_uri->protocol); if( my $f = $c->req->headers->header('X-Forwarded-Host') and not $path_uri->host ) { # We could guess the host here if it wasn't set in MOJO_RE +VERSE_PROXY # This requires that the outside-facing server resets # X-Forwarded-Host , so that header is not allowed to be u +ser-controllable (my $host) = split /,/, $f; #$url->host( $host ); $base->host( $host ); } else { #$url->host($path_uri->host); $base->host($path_uri->host); } warn "Base is <$base>"; warn "URL is now <$url>"; $url->base( $base ); }); }

    This still requires the code to know (via $ENV{MOJO_REVERSE_PROXY} the external URL, but I found no better way.

      Thank you and all others who helped me. I had a conversation with brtastic on Perl Applications and Algorithms Discord, he first recommended trying Plack::Middleware::ReverseProxy, but it didn't solve everything, so I wrote my own middleware like this:
      package Plack::Middleware::Proxy; use warnings; use strict; use parent 'Plack::Middleware'; use Plack::Util::Accessor qw( prefix scheme ); sub call { my ($self, $env) = @_; if ($env->{REMOTE_ADDR} ne '127.0.0.1') { $env->{SCRIPT_NAME} = $self->prefix; $env->{HTTP_X_FORWARDED_PROTO} = $self->scheme; } my $res = $self->app->($env); return Plack::Util::response_cb($res, sub { my $res = shift; if ($res->[0] == 302) { my %header = @{ $res->[1] }; my $prefix = $self->prefix; $header{Location} =~ s{^\Q$prefix/}{/$prefix/}; @{ $res->[1] } = %header; } return }); return $res } __PACKAGE__

      It sovles several problems at once:

      1. It fixes links like /login to include the prefix when rendered through the proxy. All such links must have the form <% response.uri_base %>/path in the template files.
      2. On the VM, the server runs on http, but it's only accessible from the outside world through https (the proxy redirects http to https). Some links kept the http scheme, this fixes the behaviour.
      3. In the code, I have several instances of redirect('/');. For some reason, this lead to errors with the path becoming prefix/prefix, the postprocessing of the response fixes this. I had to use the callback, because the app also streams files, so a simple change to $res would crash the download.

      I use it like this:

      use Plack::Builder; builder { enable 'Plack::Middleware::Proxy', prefix => 'prefix', scheme => ' +https'; 'MyApp'->to_app; };

      So, in the end, I also need to have the prefix in the code, but at least as a parameter to middleware, which seems much nicer.

      map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
        IMO the "Perfect Solution" is to have the app receive its mount prefix from the reverse proxy, and then you can even mount the app at different host/paths simultaneously. In other words, each request supplies the prefix to be used for that request.

        I don't use Dancer, and I try to avoid Apache, but the checklist looks something like:

        • Check your HTTP headers. The request path should have the prefix removed. There should be another header like X-Forwarded-Prefix (the default in Traefik) that should specify the prefix. If not, fix the config in the reverse proxy.
        • Check your Plack environment. It should have the prefix in SCRIPT_NAME, and not in PATH_INFO. If not, fix your middleware until it does.
        • When generating URLs in templates, always include the prefix, which your framework should provide in some convenient variable. Never include the hostname or protocol. If your framework insists on that, modify it until it doesn't.
        • When returning redirects, find out whether your framework automatically adds the prefix, or if you need to add it manually. Never include the protocol or host name in the redirect URL (even though that's mandated by the standard) and leave that task to your reverse proxy. (path-only 3xx locations work in every browser since forever, and proxies will usually rewrite them to full URLs for you anyway)

        There are many ways to do it of course, including Apache mod_proxy_html that can rewrite your html output to modify the generated URLs, but this just leads to frustration. (If you're not in control of the application, sometimes its the only option, though)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11165066]
Approved by pfaut
Front-paged by Discipulus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others pondering the Monastery: (4)
As of 2025-06-20 14:00 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?
    erzuuliAnonymous Monks are no longer allowed to use Super Search, due to an excessive use of this resource by robots.