Search code examples
asp.net-coredocker-composeswagger-uitraefikswashbuckle.aspnetcore

How to make SwaggerUI via Swashbuckle work in a docker container behind a traefik proxy?


I have a simple .NET core/.NET 5 API that returns some data via OData and some Data via "traditional" endpoints though all in the same controller inheriting ODataController.

When I'm starting the project with IIS Express from Visual Studio 2019 and hit http://dev-machine/swagger, I get the expected SwaggerUI-output for my API.

If I deploy it to a docker container, I can still get to my API endpoints and to /swagger/v1/swagger.json and to the SwaggerUI at /swagger.

As soon as I put that thing behind a traefik router, I'm no longer able to get to /swagger, but /swagger/v1/swagger.json still works.

I'm at a loss here and don't know where to start - so: if anyone could point me in the right direction, that'd be greatly appreciated!

My docker-compose.yaml for the project:

    version: '3'
    services:
      traefik_msstockentries:
        restart: always
        image: "traefik"
        container_name: "traefik_msstockentries"
        command:
          - "--api.insecure=true"
          - "--providers.docker=true"
          - "--providers.docker.exposedbydefault=false"
          - "--entrypoints.web.address=:80"
        ports:
          - "9016:80"
          - "8016:8080"
        volumes:
          - "/var/run/docker.sock:/var/run/docker.sock:ro"
        networks:
          default:
            ipv4_address: 192.168.239.254
      msstockentries:
        restart: always
        container_name: "msstockentries"
        image: "msstockentries:dev"
        environment:
          - 'ASPNETCORE_ENVIRONMENT=Development'
          - 'TZ=Europe/Berlin'
        labels:
         - "traefik.enable=true"
         - "traefik.http.routers.msstockentries.rule=Host(`msstockentries`)"
         - "traefik.http.routers.msstockentries.entrypoints=web"
        networks:
          - default
        depends_on:
          - traefik_msstockentries
    networks:
      default:
        driver: bridge
        ipam:
          config:
            - subnet: 192.168.239.0/24

My Dockerfile (pretty simple...)

WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM base AS final
WORKDIR .
COPY /publish/MS.StockEntries .
ENTRYPOINT ["dotnet", "MS.StockEntries.dll"]

My Startup.cs

public class Startup
{
    public IConfiguration Configuration { get; }

    public static readonly LoggerFactory loggerFactory = new LoggerFactory(new[]
    {
        new DebugLoggerProvider()
    });

    private readonly ILogger _logger = loggerFactory.CreateLogger("StartupLogger");

    public Startup(IWebHostEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();

        this.Configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {

        services.Configure<MSStockEntriesConfig>(Configuration.GetSection("MSStockEntriesConfig"));
        MSStockEntriesConfig config = new MSStockEntriesConfig();
        Configuration.GetSection("MSStockEntriesConfig").Bind(config);

        // EdgeBlood Datenbank-Context
        services.AddDbContext<EdgeBloodDbContext>(optionsBuilder =>
        {
            optionsBuilder
            //.UseLoggerFactory(loggerFactory)
            .UseOracle(config.xxx, oracleOptions =>
            {
                oracleOptions.UseOracleSQLCompatibility("11");
            })
            //.EnableSensitiveDataLogging()
            ;
        });

        services.AddControllers();

        services.AddOData();

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "MS.StockEntries",
                Version = "v1",
                Description = "Get items on stock",
                Contact = new OpenApiContact
                {
                    Name = "Team xxx",
                    Email = "[email protected]"
                }
            });

            // Set the comments path for the Swagger JSON and UI.
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            c.IncludeXmlComments(xmlPath);

            // Use method name as operationId
            c.CustomOperationIds(apiDesc =>
            {
                return apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null;
            });
        });

        SetOutputFormatters(services);
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseAuthorization();
        app.UseAuthentication();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();               endpoints.Expand().Count().MaxTop(null).SkipToken().OrderBy().Filter();
            // Set OData-Route
            endpoints.MapODataRoute("OData", "OData", GetEdmModel());
        });
        
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Activate Swagger
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "MS.StockEntries v1");
        });

        // Http-Redirection
        app.UseHttpsRedirection();
    }

    /// <summary>
    /// Create EdmModel for OData
    /// </summary>
    /// <returns>EdmModel für OData</returns>
    private IEdmModel GetEdmModel()
    {
        var odataBuilder = new ODataConventionModelBuilder();
        odataBuilder.EntitySet<StockEntry>("StockEntries");
        return odataBuilder.GetEdmModel();
    }

    /// <summary>
    /// Use swagger with ODataControllers
    /// </summary>
    /// <param name="services"></param>
    private static void SetOutputFormatters(IServiceCollection services)
    {
        services.AddMvcCore(options =>
        {
            IEnumerable<ODataOutputFormatter> outputFormatters =
                options.OutputFormatters.OfType<ODataOutputFormatter>()
                    .Where(formatter => formatter.SupportedMediaTypes.Count == 0);

            IEnumerable<ODataInputFormatter> inputFormatters =
                options.InputFormatters.OfType<ODataInputFormatter>()
                    .Where(formatter => formatter.SupportedMediaTypes.Count == 0);

            foreach (var outputFormatter in outputFormatters)
            {
                outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
                outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            }

            foreach (var inputFormatter in inputFormatters)
            {
                inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
                inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));

            }

        });
    }
}

My traefik config:

http:
  routers:
    router-msstockentriesping:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/ping`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesping-stripprefix"
        - "msstockentries-header"
        - "msstockentriesping-addendpoint"
    router-msstockentriesswagger:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/index.html`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesswagger-stripprefix"
        - "msstockentries-header"
        - "msstockentriesswagger-addendpoint"
    router-msstockentriesswaggerdesc:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1/swagger.json`)"
      service: service-msstockentries
      middlewares:
        - "msstockentriesswaggerdesc-stripprefix"
        - "msstockentries-header"
        - "msstockentriesswaggerdesc-addendpoint"
    router-msstockentries:
      rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries`)"
      service: service-msstockentries
      middlewares:
        - "msstockentries-stripprefix"
        - "msstockentries-header"
        - "msstockentries-addendpoint"
  middlewares:
    msstockentries-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries"
    msstockentriesping-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/ping"
    msstockentriesswagger-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/swagger/index.html"
    msstockentriesswaggerdesc-stripprefix:
      stripPrefix:
        prefixes:
          - "/msstockentries/swagger/v1/swagger.json"
    msstockentries-header:
      headers:
        customRequestHeaders:
          Host: "msstockentries"
    msstockentriesping-addendpoint:
      addPrefix:
        prefix: "/StockEntries/Ping"
    msstockentries-addendpoint:
      addPrefix:
        prefix: "/OData/StockEntries"
    msstockentriesswagger-addendpoint:
      addPrefix:
        prefix: "/swagger/index.html"
    msstockentriesswaggerdesc-addendpoint:
      addPrefix:
        prefix: "/swagger/v1/swagger.json"
  services:
    service-msstockentries:
      loadBalancer:
        passHostHeader: true
        servers:
          - url: 'http://zzz:9016'

Solution

  • Finally found my mistake(s)...

    I ended up defining explicit Traefik routers and associated rules for the files needed by SwaggerUI as well as detailing the SwaggerEndpoint (at app.UseSwaggerUI()). Now everything is working as expected.

    Relevant parts of traefik config:

        http:
          routers:
            router-msstockentriesswagger:
              rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger`)"
              service: service-msstockentries
              middlewares:
                - "msstockentriesswagger-addendpoint"
            router-msstockentriesswaggercss:
              rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui.css`)"
              service: service-msstockentries
              middlewares:
                - "msstockentriesswaggercss-addendpoint"
            router-msstockentriesswaggerbundle:
              rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-bundle.js`)"
              service: service-msstockentries
              middlewares:
                - "msstockentriesswaggerbundle-addendpoint"
            router-msstockentriesswaggerpreset:
              rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-standalone-preset.js`)"
              service: service-msstockentries
              middlewares:
                - "msstockentriesswaggerpreset-addendpoint"
            router-msstockentriesswaggerdesc:
              rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1`)"
              service: service-msstockentries
              middlewares:
                - "msstockentriesswaggerv1-addendpoint"
          middlewares:
            msstockentriesswagger-addendpoint:
              addPrefix:
                prefix: "/swagger/index.html"
            msstockentriesswaggercss-addendpoint:
              addPrefix:
                prefix: "/swagger/swagger-ui.css"
            msstockentriesswaggerbundle-addendpoint:
              addPrefix:
                prefix: "/swagger/swagger-ui-bundle.js"
            msstockentriesswaggerpreset-addendpoint:
              addPrefix:
                prefix: "/swagger/swagger-ui-standalone-preset.js"
            msstockentriesswaggerv1-addendpoint:
              addPrefix:
                prefix: "/swagger/v1/swagger.json"
    

    Relevant parts of Startup.cs:

        app.UseSwaggerUI(c =>
        {
            c.RoutePrefix = "swagger";
            c.SwaggerEndpoint($"{misApiServiceConfig.SwaggerUiEndpoint}", $"{misApiServiceConfig.SwaggerUiServicename} {misApiServiceConfig.SwaggerUIServiceversion}");
         });
    

    The SwaggerUIEndpoit to use can be set in appsettings.json.