JFrog Platform Installation with NGINX Ingress Controller

Expose the JFrog Platform through the NGINX Ingress Controller instead of the chart's bundled Nginx pod, with TLS, non-TLS, and Docker subdomain configurations.

Expose the JFrog Platform through an NGINX Ingress Controller you manage yourself instead of the Nginx pod bundled with the chart. Set artifactory.nginx.enabled: false to disable the bundled pod and artifactory.ingress.enabled: true to make the chart create an Ingress your controller picks up.

Use this when you already run an ingress controller for other workloads, want one external IP shared across applications, or terminate TLS at the ingress layer by policy.

Before You Begin

You need the following.

  • Kubernetes 1.27+ or later, and Helm 3.17+ or later.
  • Cluster-admin permission to install an ingress controller in its own namespace.
  • A DNS record pointing your hostname at the ingress controller's external address. Docker subdomain access also needs a wildcard record.
  • A TLS secret of type kubernetes.io/tls in the JFrog Platform namespace, unless you deploy the non-TLS configuration.

Then confirm which controller you run, because two different products share the name "NGINX Ingress Controller" and they read different annotation prefixes.

F5 NGINX Ingress ControllerKubernetes community controller
Helm repositoryhttps://helm.nginx.com/stablehttps://kubernetes.github.io/ingress-nginx
Chartnginx/nginx-ingressingress-nginx/ingress-nginx
Annotation prefixnginx.org/nginx.ingress.kubernetes.io/

This page uses the F5 controller and nginx.org/ annotations throughout. On the community controller, translate them using Community Controller Equivalents.

⚠️

Annotations for the Wrong Controller Are Ignored Silently

Each controller ignores the other's annotations with no error and no event. The annotations stay visible in kubectl describe ingress, so the configuration looks correct while doing nothing. Symptoms appear later: uploads fail at the default size limit, or docker login returns 404. If you are migrating between controllers, rewrite every prefix.

Step 1: Install the NGINX Ingress Controller

Install with snippet support enabled.

helm repo add nginx https://helm.nginx.com/stable
helm repo update

helm install nginx-ingress nginx/nginx-ingress \
  --namespace nginx-ingress --create-namespace \
  --set controller.enableSnippets=true
⚠️

Snippet Support Is Off by Default

The location-snippets and server-snippets annotations below carry the Docker registry rewrite rules. Without controller.enableSnippets=true the controller discards them without error. JFrog Artifactory starts and the UI loads normally, so the failure surfaces later as a 404 on docker login or docker push. If the controller is already installed, rerun the command with helm upgrade.

Get the external address for your DNS record.

kubectl get service nginx-ingress-controller \
  --namespace nginx-ingress \
  --output jsonpath='{.status.loadBalancer.ingress[0]}'

Get the registered ingress class name. Your className value must match it exactly.

kubectl get ingressclass

Step 2: Choose Your Configuration

All three configurations disable the bundled Nginx pod, route / to the JFrog Router on port 8082, and route /artifactory/ directly to Artifactory on port 8081. They differ only in TLS and Docker access.

ConfigurationTLSDocker accessExtra requirements
TLS EnabledAt the ingressRepository path, example.com/docker-localTLS secret for one hostname
Without TLSNoneRepository pathNone. Testing only
Docker SubdomainAt the ingressSubdomain, docker-local.example.comWildcard certificate, wildcard DNS, and an Artifactory setting

Examples nest values under artifactory:, the form the jfrog-platform chart uses. For the standalone artifactory or artifactory-ha chart, remove that wrapper and shift the remaining keys left one level.

Configuration 1: TLS Enabled

Terminates TLS at the ingress with a certificate you supply. Docker clients use repository paths.

artifactory:
  nginx:
    enabled: false

  ingress:
    enabled: true
    defaultBackend:
      enabled: true
    hosts:
      - artifactory.example.com
    routerPath: /
    artifactoryPath: /artifactory/
    className: "nginx"
    annotations:
      nginx.org/client-max-body-size: "0"
      nginx.org/proxy-read-timeout: "600"
      nginx.org/proxy-send-timeout: "600"
      nginx.org/location-snippets: |
        rewrite ^/(v2)/token /artifactory/api/docker/null/v2/token;
        rewrite ^/(v2)/([^\/]*)(/.*) /artifactory/api/docker/$2/$1$3;
    tls:
      - secretName: artifactory-tls
        hosts:
          - artifactory.example.com

client-max-body-size: "0" disables the body size check entirely. Artifact uploads have no predictable upper bound, so any finite limit eventually rejects a legitimate push with HTTP 413. The 600 second timeouts cover transfers that exceed the NGINX default of 60 seconds.

location-snippets applies to every path the chart generates, both routerPath and artifactoryPath. The Gateway API differs here. It attaches filters only to the router path rule, so these rewrites stop applying to /artifactory/ traffic if you migrate later.

Configuration 2: Without TLS

Serves plain HTTP. Use only for testing, or when a load balancer upstream terminates TLS.

Take Configuration 1 and make two changes: drop the tls block entirely, and add one annotation.

    annotations:
      nginx.org/redirect-to-https: "false"

redirect-to-https: "false" is defensive here rather than required. With no tls block, the controller generates no redirect at all, so plain HTTP already works without it.

Keep it anyway. It starts mattering in two cases:

  • A load balancer upstream terminates TLS and sets X-Forwarded-Proto: https, and you enabled forwarded header support. See Ingress Behind Another Load Balancer.
  • You later add a tls block. The controller then emits return 301 https://$host:443$request_uri for HTTP requests, and this annotation suppresses it.

Configuration 3: Docker Subdomain Access

Lets Docker clients address repositories as subdomains such as docker-local.artifactory.example.com. The server-snippets block extracts the repository from the hostname and location-snippets rewrites the request to the matching Artifactory Docker API path.

artifactory:
  nginx:
    enabled: false

  ingress:
    enabled: true
    defaultBackend:
      enabled: false
    hosts:
      - artifactory.example.com
      - '*.artifactory.example.com'
    routerPath: /
    artifactoryPath: /artifactory/
    className: "nginx"
    annotations:
      nginx.org/client-max-body-size: "0"
      nginx.org/proxy-read-timeout: "2400s"
      nginx.org/proxy-send-timeout: "2400s"
      nginx.org/proxy-buffers: "40 128k"
      nginx.org/proxy-busy-buffers-size: "128k"
      nginx.org/server-snippets: |
        set $repo "docker";
        if ($host ~* "^(?<captured_repo>.+)\.artifactory\.example\.com$") {
          set $repo $captured_repo;
        }
      nginx.org/location-snippets: |
        rewrite ^/(v1|v2)/(.*) /artifactory/api/docker/$repo/$1/$2 break;
        rewrite ^/(v1|v2)/ /artifactory/api/docker/$repo/$1/ break;
        proxy_set_header X-JFrog-Override-Base-Url $scheme://$host;
    tls:
      - secretName: artifactory-tls
        hosts:
          - artifactory.example.com
          - "*.artifactory.example.com"

Replace artifactory.example.com everywhere it appears, including the two occurrences inside the server-snippets regular expression, which escapes each dot as \. to match literal dots.

The 2400s timeouts exceed the 600 used elsewhere because subdomain routing adds a rewrite hop and large layer transfers can outlast the shorter limit. The proxy-buffers values cover the larger response headers Docker manifest requests produce.

This configuration needs four things the others do not.

  1. Artifactory set to the Subdomain access method. See Set the Artifactory Docker Access Method.
  2. A wildcard TLS certificate covering both artifactory.example.com and *.artifactory.example.com. TLS is negotiated against the subdomain before any HTTP routing happens. An apex-only certificate therefore fails the handshake on every subdomain. Without a wildcard certificate, use repository paths instead.
  3. A wildcard DNS record for *.artifactory.example.com pointing at the controller's external address.
  4. The X-JFrog-Override-Base-Url header, already in the block above. It pins the base URL Artifactory uses for absolute URLs in Docker API responses. Treat it as insurance: both controllers forward Host and X-Forwarded-* by default, from which Artifactory already derives the correct URL. It matters when an upstream proxy drops or rewrites those headers, or when Artifactory's configured Base URL differs from the hostname clients use.

Set the Artifactory Docker Access Method

Artifactory defaults to the repository path method. It embeds the repository name in the upload URLs it returns to Docker clients. The subdomain rewrite already derives the repository from the hostname, so it prepends the name a second time and the resulting path is invalid.

Set the method to Subdomain in Administration > Reverse Proxy, or through the REST API.

curl -u admin:<password> \
  "https://artifactory.example.com/artifactory/api/system/configuration/webServer" \
  --output webserver.json

# change "dockerReverseProxyMethod" to "SUBDOMAIN" in webserver.json, then:
curl -u admin:<password> -X POST \
  -H "Content-Type: application/json" \
  --data @webserver.json \
  "https://artifactory.example.com/artifactory/api/system/configuration/webServer"
⚠️

The POST Can Reject Its Own GET Response

The GET response returns httpPort: -1 whenever useHttp is false. Posting that value back unchanged fails with Found config violations: httpPort should be > 0, even though the port is unused. Set httpPort to any positive placeholder, such as 80, in webserver.json before posting it back.

Confirm it took effect.

curl -s -u admin:<password> \
  "https://artifactory.example.com/artifactory/api/system/configuration/webServer" \
  | grep dockerReverseProxyMethod
⚠️

Reads Succeed While Pushes Fail if This Is Unset

Left as REPOPATHPREFIX, the setup looks healthy. Every read succeeds, because a single read needs no redirect. GET /v2/ returns 401 anonymously and 200 authenticated, and /v2/_catalog lists repositories.

Pushes fail at the blob upload step. Docker follows Artifactory's Location header, and that header carries the repository name. The rewrite then prepends the name a second time, so the manifest upload ends with MANIFEST_INVALID: manifest invalid.

Blobs uploading successfully, followed by that error, means this setting is the cause.

Step 3: Deploy the JFrog Platform

Generate and safely store the master and join keys before your first install. Every later upgrade needs the same values.

export MASTER_KEY=$(openssl rand -hex 32)
export JOIN_KEY=$(openssl rand -hex 32)

Save your chosen configuration as values.yaml and deploy.

helm repo add jfrog https://charts.jfrog.io
helm repo update

helm upgrade --install jfrog-platform jfrog/jfrog-platform \
  --namespace jfrog-platform --create-namespace \
  --set global.masterKey=${MASTER_KEY} \
  --set global.joinKey=${JOIN_KEY} \
  --values values.yaml
⚠️

Later Ingress Changes Need databaseUpgradeReady

Changing any ingress value on a deployed release means running helm upgrade. With the chart's bundled PostgreSQL enabled, the chart blocks every upgrade unless you also pass --set databaseUpgradeReady=true.

The failure message talks about database upgrades, not ingress, which makes the cause easy to miss. It applies even to annotation-only changes that touch no database.

Step 4: Verify the Deployment

Confirm the Ingress exists and your controller assigned it an address, and that the annotations arrived intact.

kubectl get ingress --namespace jfrog-platform
kubectl describe ingress --namespace jfrog-platform

Check platform health through the ingress. The readiness endpoint covers all platform microservices, not just Artifactory, and returns HTTP 200 with {"code": "OK"} when healthy.

curl https://artifactory.example.com/router/api/v1/system/readiness

For a per-service breakdown when readiness fails, use /router/api/v1/system/health. See Load Balancer for the full endpoint list.

If you configured Docker subdomain access, verify it separately.

docker login docker-local.artifactory.example.com

Troubleshooting

SymptomCauseResolution
Annotations present in kubectl describe ingress but have no effectWrong annotation prefix for your controllerMatch the prefix to your controller. See Community Controller Equivalents
docker login or docker push returns 404Snippet support disabledReinstall or upgrade with controller.enableSnippets=true
kubectl apply rejected with annotation group ServerSnippet contains risky annotationCommunity controller blocks snippets as a critical riskSet both controller.allowSnippetAnnotations=true and controller.config.annotations-risk-level=Critical
HTTP 413 Request Entity Too Largeclient-max-body-size not appliedSet nginx.org/client-max-body-size: "0" and confirm the prefix
No address assigned to the IngressclassName does not match a registered classRun kubectl get ingressclass and use the exact value
Redirect loop, or connection refused after redirectController redirects to HTTPS with no TLS configuredSet nginx.org/redirect-to-https: "false", or configure TLS
Subdomain reads work, push fails MANIFEST_INVALID after blobs uploadArtifactory still on the repository path methodSet dockerReverseProxyMethod to SUBDOMAIN. See Set the Artifactory Docker Access Method
Docker transfer fails partway, Location header shows a wrong hostnameUpstream proxy dropped or rewrote Host and X-Forwarded-*Add proxy_set_header X-JFrog-Override-Base-Url. Inspect with curl -D- -X POST .../v2/<image>/blobs/uploads/
TLS handshake fails on subdomains onlyCertificate does not cover the wildcard hostnameReissue covering both the apex and *. hostnames
Two external IP addresses provisionedBundled Nginx pod still enabledSet artifactory.nginx.enabled: false
Timeout on large transfersDefault 60 second proxy timeoutRaise proxy-read-timeout and proxy-send-timeout

Community Controller Equivalents

To use the Kubernetes community controller instead of F5, translate the annotations.

PurposeF5 (nginx.org/)Community (nginx.ingress.kubernetes.io/)
Maximum upload sizeclient-max-body-sizeproxy-body-size
Backend read timeoutproxy-read-timeoutproxy-read-timeout
Backend send timeoutproxy-send-timeoutproxy-send-timeout
Per-location configlocation-snippetsconfiguration-snippet
Per-server configserver-snippetsserver-snippet
Disable HTTPS redirectredirect-to-https: "false"ssl-redirect: "false"

Enabling snippets also takes more work there. F5 needs one flag; the community controller needs two, because it treats snippet annotations as a privilege-escalation risk and rejects them at the admission webhook.

ControllerFlags required for snippet annotations
F5--set controller.enableSnippets=true
Community--set controller.allowSnippetAnnotations=true and --set controller.config.annotations-risk-level=Critical

Without the second flag, kubectl apply fails immediately rather than failing silently. Confirmed on community controller v1.15.1; older versions may need only the first flag. Raise the risk level only if your cluster policy permits snippets, since they allow arbitrary NGINX configuration from any namespace that can create an Ingress.

Frequently Asked Questions

This section provides answers to frequently asked questions.

plusFrequently Asked Questions
Q: What is the difference between routerPath and artifactoryPath?

A: routerPath (default /) routes to the JFrog Router on port 8082, which fronts every platform microservice including the UI, Xray, and Distribution. artifactoryPath (default /artifactory/) routes directly to Artifactory on port 8081, bypassing the Router for high-volume artifact traffic. Set ingress.disableRouterBypass: true to remove the direct route and send everything through the Router.

Q: Should I use Ingress or the Kubernetes Gateway API?

A: Ingress is the mature, widely deployed option, and it is what this page documents. The Gateway API is a newer Kubernetes standard that the Artifactory charts also support. It offers richer routing and a cleaner split between cluster operator and application owner. Choose it for a new deployment if you already run a Gateway controller, and see Kubernetes Gateway API Configuration for Artifactory. You can enable both at once during a migration.

Q: Why do I need to disable the bundled Nginx pod?

A: The bundled pod defaults to enabled and is exposed through a Kubernetes service of type LoadBalancer. Leaving it enabled alongside an Ingress makes your cloud provider provision a second external load balancer that nothing routes to, which you are billed for.

Related Topics


Did this page help you?