404 issue with a non standard HTTP service

Hi,

Sorry, if there are any information I missed here, but this is my first post here, as I am a newbie on Traefik and this forum.
I looked over a lot of 404 issues on this forum, but that it did not help me much.

I am trying to setup a R Shiny application (https://shiny.rstudio.com/), in order to bypass usual solutions for this kind of application (shinyproxy, shiny-server, etc...). Main purpose is to have a more responsive application with an OpenSource well-managed full stack.

There are a bunch of specific behaviour with R shiny, including the fact that it must answer both to http request and web services.

I tried to follow this tutorial and well, that works fine. But as I am trying to remove the shinyproxy, I had to dig into traefik configuration.

And whatever I do, I get a 404 issue with the docker application. The traefik dashboard works fine.

Here are my configurations:

traefik.yml

version: '3.3'

services:

  traefik:
    # Use the latest Traefik image
    image: traefik:v2.2
    ports:
      ## Listen on port 80, default for HTTP, necessary to redirect to HTTPS
      #- 80:80
      ## Listen on port 443, default for HTTPS
      #- 443:443
      - target: 80
        published: 80
        mode: host
      - target: 443
        published: 443
        mode: host
    deploy:
      placement:
        constraints:
          # Make the traefik service run only on the node with this label
          # as the node with it has the volume for the certificates
          - node.labels.traefik-public.traefik-public-certificates == true
      labels:
        # Enable Traefik for this service, to make it available in the public network
        - traefik.enable=true
        # Use the traefik-public network (declared below)
        - traefik.docker.network=traefik-public
        # Use the custom label "traefik.constraint-label=traefik-public"
        # This public Traefik will only use services with this label
        # That way you can add other internal Traefik instances per stack if needed
        - traefik.constraint-label=traefik-public
        # admin-auth middleware with HTTP Basic auth
        # Using the environment variables USERNAME and HASHED_PASSWORD
        - traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
        # https-redirect middleware to redirect HTTP to HTTPS
        # It can be re-used by other stacks in other Docker Compose files
        - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
        - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
        # traefik-http set up only to use the middleware to redirect to https
        # Uses the environment variable DOMAIN
        - traefik.http.routers.traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
        - traefik.http.routers.traefik-public-http.entrypoints=http
        - traefik.http.routers.traefik-public-http.middlewares=https-redirect
        # traefik-https the actual router using HTTPS
        # Uses the environment variable DOMAIN
        - traefik.http.routers.traefik-public-https.rule=Host(`${DOMAIN?Variable not set}`)
        - traefik.http.routers.traefik-public-https.entrypoints=https
        - traefik.http.routers.traefik-public-https.tls=true
        # Use the special Traefik service api@internal with the web UI/Dashboard
        - traefik.http.routers.traefik-public-https.service=api@internal
        # Use the "le" (Let's Encrypt) resolver created below
        - traefik.http.routers.traefik-public-https.tls.certresolver=le
        # Enable HTTP Basic auth, using the middleware created above
        - traefik.http.routers.traefik-public-https.middlewares=admin-auth
        # Define the port inside of the Docker service to use
        - traefik.http.services.traefik-public.loadbalancer.server.port=8080
    volumes:
      # Add Docker as a mounted volume, so that Traefik can read the labels of other services
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # Mount the volume to store the certificates
      - traefik-public-certificates:/certificates
    command:
      # Enable Docker in Traefik, so that it reads labels from Docker services
      - --providers.docker
      # Add a constraint to only use services with the label "traefik.constraint-label=traefik-public"
      - --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
      # Do not expose all Docker services, only the ones explicitly exposed
      - --providers.docker.exposedbydefault=false
      # Enable Docker Swarm mode
      - --providers.docker.swarmmode
      # Create an entrypoint "http" listening on address 80
      - --entrypoints.http.address=:80
      # Create an entrypoint "https" listening on address 80
      - --entrypoints.https.address=:443
      # Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
      - --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
      # Store the Let's Encrypt certificates in the mounted volume
      - --certificatesresolvers.le.acme.storage=/certificates/acme.json
      # Use the TLS Challenge for Let's Encrypt
      - --certificatesresolvers.le.acme.tlschallenge=true
      # Enable the access log, with HTTP requests
      - --accesslog
      # Enable the Traefik log, for configurations and errors
      - --log
      - --log.level=DEBUG
      # Enable the Dashboard and API
      - --api
    networks:
      # Use the public network created to be shared between Traefik and
      # any other service that needs to be publicly available with HTTPS
      - traefik-public

volumes:
  # Create a volume to store the certificates, there is a constraint to make sure
  # Traefik is always deployed to the same Docker node with the same volume containing
  # the HTTPS certificates
  traefik-public-certificates:

networks:
  # Use the previously created public network "traefik-public", shared with other
  # services that need to be publicly available via this Traefik
  traefik-public:
    external: true

myapp.yml

version: '3.3'

services:

  myapp:
    image: myapp
    #build: .
    ports:
      - 3838
    deploy:
      mode: replicated
      #replicas: 8
      replicas: 1
      restart_policy:
        condition: on-failure
    networks:
      - traefik-public
      - back-net
    command: Rscript -e "shiny::runApp('/myapp', port=3838, host='0.0.0.0')"
    # next will listen on http://127.0.0.1:3838 by container
    #command: Rscript -e "shiny::runApp('/myapp', port=3838)"
    labels:
        - traefik.enable=true # enable traefik
        - traefik.docker.network=traefik-public # put it in the same network as traefik
        - traefik.constraint-label=traefik-public # assign the same label as traefik so it can be discovered
        - traefik.http.routers.myappserv.rule=Host(`${APP_DOMAIN?Variable not set}`) # listen to port 80 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.myappserv.entrypoints=http
        - traefik.http.middlewares.myappserv.redirectscheme.scheme=https   # redirect traffic to https
        - traefik.http.middlewares.myappserv.redirectscheme.permanent=true # redirect traffic to https
        - traefik.http.routers.myappserv-secured.rule=Host(`${APP_DOMAIN?Variable not set}`) # listen to port 443 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.myappserv-secured.entrypoints=https
        - traefik.http.routers.myappserv-secured.tls.certresolver=le # use the Let's Encrypt certificate we set up earlier
        - traefik.http.services.myappserv-secured.loadbalancer.server.port=3838 # Ask Traefik to loadbalancing on this port
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

networks:
  traefik-public:
    external: true
  back-net:
    external: true

I tried many port and loadbalacing port configurations but that did not work either.

Any help would be greatly appreciated.

Thanks

One thing I do see is that your bash parameter expansions are incorrect. They should be ${varname:?message} yours are missing the colon.

Edit: looks like either format is acceptable.

Oh yes, swarm mode. Swarm labels need to be nested under deploy. It is correct in your traefik service but not in the myapp service.

Hi @cakiwi

Thanks. Indeed, I did not notice this error.

I did the modification, but I still have 404 issue. What I am not sure to understand is the loadbalancing behaviour for ports with swarm provider; do I need to loadbalance the same port for both services or, on the contrary, should not I, to avoid "already in use" kind of error ? Or does it really matter (not the same service/network) ?

I am also wondering if there is a way to debug traefik and networks at a lower level, to check routes between all the traefik layers ?
Maybe I should have mentioned I created my networks with the overlay driver.

Here is my new myapp.yml files:

version: '3.3'

services:

  myapp:
    image: myapp
    ports:
      - 3838
    deploy:
      mode: replicated
      replicas: 2
      #replicas: 12
      restart_policy:
        condition: on-failure
      labels:
        - traefik.enable=true # enable traefik
        - traefik.docker.network=traefik-public # put it in the same network as traefik
        - traefik.constraint-label=traefik-public # assign the same label as traefik so it can be discovered
        - traefik.http.routers.myappserv.rule=Host(`${APP_DOMAIN?Variable not set}`) # listen to port 80 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.myappserv.entrypoints=http
        - traefik.http.middlewares.myappserv.redirectscheme.scheme=https   # redirect traffic to https
        - traefik.http.middlewares.myappserv.redirectscheme.permanent=true # redirect traffic to https
        - traefik.http.routers.myappserv-secured.rule=Host(`${APP_DOMAIN?Variable not set}`) # listen to port 443 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.myappserv-secured.entrypoints=https
        - traefik.http.routers.myappserv-secured.tls.certresolver=le # use the Let's Encrypt certificate we set up earlier
        - traefik.http.services.myappserv-secured.loadbalancer.server.port=3838 # Ask Traefik to loadbalancing on this port
        - traefik.http.services.myappserv-secured.loadbalancer.stickyness=true
    networks:
      - back-net
      - traefik-public
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

networks:
  traefik-public:
    external: true
  back-net:
    external: true

And I replaced the loadbalancing port on 8080 to 3838 in the traefik.yml file, just in case (3838 is the default port for shiny applications).

I also tried to serve the shiny application through a nginx proxy inside the container, but that did not work either (it works with a single container, but not with the swarm/traefik stack).

Best regards,
Rémy.

edit: important update: I have been able to launch portainer with ~almost the same yml file, changing just the image name, the FQDN variable and the port. I was first thinking about a DNS/FQDN issue, but after changing it, it did not work either. So, I am now checking network and port listening variable inside the image. My container, by default, is listening on http://127.0.0.1:3838 or http://0.0.0.0:3838 if I am specifying an address. I saw with traefik logs that portainer container is automatically listening on the right network...

By the way, I did a basic working compose file using my shiny application and traefik.

docker-compose.yml:

version: "3.3"

services:

  traefik:
    image: "traefik:v2.2"
    command:
      #- "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.myresolver.acme.email=logs@domain.tld"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "443:443"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  myapp:
    image: "myapp"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`myapp.domain.tld`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=myresolver"

But what I need, is a full swarm stack to be able to loadbalance between shiny containers.

Ok.

After digging into traefik logs, it seems that I was not using the right syntax for stickiness...

My new myapp.yml file:

version: '3.7'

services:

  myapp:
    image: myapp
    ports:
      - ${MYAPP_PORT?Variable not set}
    deploy:
      mode: replicated
      replicas: 12
      restart_policy:
        condition: on-failure
      labels:
        - traefik.enable=true # enable traefik
        - traefik.docker.network=traefik-public # put it in the same network as traefik
        - traefik.constraint-label=traefik-public # assign the same label as traefik so it can be discovered
        - traefik.http.routers.idcovserv.rule=Host(`${MYAPP_FQDN?Variable not set}`) # listen to port 80 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.idcovserv.entrypoints=http
        - traefik.http.middlewares.idcovserv.redirectscheme.scheme=https   # redirect traffic to https
        - traefik.http.middlewares.idcovserv.redirectscheme.permanent=true # redirect traffic to https
        - traefik.http.routers.idcovserv-secured.rule=Host(`${MYAPP_FQDN?Variable not set}`) # listen to port 443 for request to APP_DOMAIN (use together with the line below)
        - traefik.http.routers.idcovserv-secured.entrypoints=https
        - traefik.http.routers.idcovserv-secured.tls.certresolver=le # use the Let's Encrypt certificate we set up earlier
        - traefik.http.services.idcovserv-secured.loadbalancer.server.port=${MYAPP_PORT?Variable not set} # Ask Traefik to loadbalancing on this port
        - traefik.http.services.idcovserv-secured.loadbalancer.sticky.cookie=true
    networks:
      - back-net
      - traefik-public
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

networks:
  traefik-public:
    external: true
  back-net:
    external: true

Hope it would help other people. And just to clarify, the loadbalancing port on traefik service has nothing to do with the app loadbalancing port.

Thanks.

@remyd1

Ooomph, missed the sticky label in there. Good self solve!