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/*.templatethroughenvsubstinto 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_FILTERare substituted — helpful, because nginx itself uses$namesyntax thatenvsubstwould 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 }| Concern | Where it lives | Note |
|---|---|---|
| TLS certificate | A Kubernetes Secret | cert-manager can issue and renew it |
| Body size and timeouts | Ingress annotations | Per-ingress, not global |
| Rate limits | limit-rps annotation | Applied per client address |
| Extra config | A ConfigMap for the controller | Global, affects every ingress |
| Sticky sessions | Cookie annotation | Prefer 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.Related
WebSockets, gRPC and streaming responses Performance tuning: workers, buffers and timeouts
Last refreshed 2026-09-18.