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/tlsin 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 Controller | Kubernetes community controller | |
|---|---|---|
| Helm repository | https://helm.nginx.com/stable | https://kubernetes.github.io/ingress-nginx |
| Chart | nginx/nginx-ingress | ingress-nginx/ingress-nginx |
| Annotation prefix | nginx.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, ordocker loginreturns 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=trueSnippet Support Is Off by Default
The
location-snippetsandserver-snippetsannotations below carry the Docker registry rewrite rules. Withoutcontroller.enableSnippets=truethe controller discards them without error. JFrog Artifactory starts and the UI loads normally, so the failure surfaces later as a 404 ondocker loginordocker push. If the controller is already installed, rerun the command withhelm 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 ingressclassStep 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.
| Configuration | TLS | Docker access | Extra requirements |
|---|---|---|---|
| TLS Enabled | At the ingress | Repository path, example.com/docker-local | TLS secret for one hostname |
| Without TLS | None | Repository path | None. Testing only |
| Docker Subdomain | At the ingress | Subdomain, docker-local.example.com | Wildcard 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.comclient-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
tlsblock. The controller then emitsreturn 301 https://$host:443$request_urifor 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.
- Artifactory set to the Subdomain access method. See Set the Artifactory Docker Access Method.
- A wildcard TLS certificate covering both
artifactory.example.comand*.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. - A wildcard DNS record for
*.artifactory.example.compointing at the controller's external address. - The
X-JFrog-Override-Base-Urlheader, 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 forwardHostandX-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: -1wheneveruseHttpisfalse. Posting that value back unchanged fails withFound config violations: httpPort should be > 0, even though the port is unused. SethttpPortto any positive placeholder, such as80, inwebserver.jsonbefore posting it back.
Confirm it took effect.
curl -s -u admin:<password> \
"https://artifactory.example.com/artifactory/api/system/configuration/webServer" \
| grep dockerReverseProxyMethodReads 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/_cataloglists repositories.Pushes fail at the blob upload step. Docker follows Artifactory's
Locationheader, and that header carries the repository name. The rewrite then prepends the name a second time, so the manifest upload ends withMANIFEST_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.yamlLater Ingress Changes Need databaseUpgradeReady
Changing any
ingressvalue on a deployed release means runninghelm 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-platformCheck 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/readinessFor 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.comTroubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
Annotations present in kubectl describe ingress but have no effect | Wrong annotation prefix for your controller | Match the prefix to your controller. See Community Controller Equivalents |
docker login or docker push returns 404 | Snippet support disabled | Reinstall or upgrade with controller.enableSnippets=true |
kubectl apply rejected with annotation group ServerSnippet contains risky annotation | Community controller blocks snippets as a critical risk | Set both controller.allowSnippetAnnotations=true and controller.config.annotations-risk-level=Critical |
| HTTP 413 Request Entity Too Large | client-max-body-size not applied | Set nginx.org/client-max-body-size: "0" and confirm the prefix |
No address assigned to the Ingress | className does not match a registered class | Run kubectl get ingressclass and use the exact value |
| Redirect loop, or connection refused after redirect | Controller redirects to HTTPS with no TLS configured | Set nginx.org/redirect-to-https: "false", or configure TLS |
Subdomain reads work, push fails MANIFEST_INVALID after blobs upload | Artifactory still on the repository path method | Set dockerReverseProxyMethod to SUBDOMAIN. See Set the Artifactory Docker Access Method |
Docker transfer fails partway, Location header shows a wrong hostname | Upstream 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 only | Certificate does not cover the wildcard hostname | Reissue covering both the apex and *. hostnames |
| Two external IP addresses provisioned | Bundled Nginx pod still enabled | Set artifactory.nginx.enabled: false |
| Timeout on large transfers | Default 60 second proxy timeout | Raise proxy-read-timeout and proxy-send-timeout |
Community Controller Equivalents
To use the Kubernetes community controller instead of F5, translate the annotations.
| Purpose | F5 (nginx.org/) | Community (nginx.ingress.kubernetes.io/) |
|---|---|---|
| Maximum upload size | client-max-body-size | proxy-body-size |
| Backend read timeout | proxy-read-timeout | proxy-read-timeout |
| Backend send timeout | proxy-send-timeout | proxy-send-timeout |
| Per-location config | location-snippets | configuration-snippet |
| Per-server config | server-snippets | server-snippet |
| Disable HTTPS redirect | redirect-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.
| Controller | Flags 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.
Frequently Asked Questions
Q: What is the difference between routerPath and artifactoryPath?
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
Updated about 11 hours ago
