nginx in containers and as a Kubernetes ingress

Configure with templates and environment variables, run as a non-root user, use the ingress controller, and understand annotations and canary routing.

nginx in a container

FROM nginx:1.27-alpine

# templates are rendered into /etc/nginx/conf.d at container start,
# substituting environment variables in files ending .template
COPY default.conf.template /etc/nginx/templates/default.conf.template
ENV NGINX_PORT=8080
ENV API_UPSTREAM=http://api:8080

# the templated file can use the variables directly
# server { listen ${NGINX_PORT}; location / { proxy_pass ${API_UPSTREAM}; } }

# the official image's entrypoint already calls envsubst on the templates.
# Run as a non-root user on an unprivileged port.
RUN chown -R nginx:nginx /var/cache/nginx /etc/nginx/conf.d \
 && touch /var/run/nginx.pid && chown nginx:nginx /var/run/nginx.pid

USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8080/health || exit 1
  • The official image renders /etc/nginx/templates/*.template through envsubst into the config directory before starting nginx.
  • Non-root containers cannot bind port 80, so listen on 8080 and let the platform map it.
  • Only environment variables you list in NGINX_ENVSUBST_FILTER are substituted — helpful, because nginx itself uses $name syntax that envsubst would otherwise eat.
  • Keep the container stateless: logs to stdout and stderr, no config edits at runtime.

The ingress controller

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/limit-rps: "20"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port: { number: 8080 }
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port: { number: 80 }
ConcernWhere it livesNote
TLS certificateA Kubernetes Secretcert-manager can issue and renew it
Body size and timeoutsIngress annotationsPer-ingress, not global
Rate limitslimit-rps annotationApplied per client address
Extra configA ConfigMap for the controllerGlobal, affects every ingress
Sticky sessionsCookie annotationPrefer stateless backends

Canary and blue-green routing

# canary: 10 percent of traffic to the new version
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
    nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "always"
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-canary
                port: { number: 80 }
⚠️
A canary splits traffic, not state. If the new version writes to the same database with a different schema, or a session lives only in memory, the canary is not isolated and a rollback will not undo its writes. Plan the data compatibility before routing a single request.

FAQ

Should I run my own nginx in front of the ingress controller?
Rarely. Two layers of proxies add latency and two places to debug. Put edge behaviour such as rate limiting and WAF at the ingress or the cloud load balancer, and keep nginx containers for application-specific static or proxy duties.
How do I reload config in a container without downtime?
Render a new config, run nginx -t, then send a reload signal. In a container the usual approach is to roll a new pod, because an in-place reload of a mounted ConfigMap is not atomic and can serve a half-written file.

WebSockets, gRPC and streaming responses Performance tuning: workers, buffers and timeouts

Last refreshed 2026-09-18.