AWX Behind Reverse Proxy: Nginx, Traefik, Caddy & Apache Setup Guide
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to running AWX behind Nginx or Traefik reverse proxy. Configure SSL termination, custom domains, WebSocket proxying for live job output, health.
AWX runs on port 8052 by default with its own web server. Putting it behind a reverse proxy gives you SSL termination, custom domains, WebSocket support for live job output, and the ability to run alongside other services. Here's the complete setup for Nginx and Traefik.
Why Use a Reverse Proxy with AWX
• SSL/TLS termination — HTTPS with Let's Encrypt certificates • Custom domain —awx.example.com instead of server:8052
• WebSocket proxying — Required for live job output streaming
• Load balancing — Multiple AWX instances behind one endpoint
• Security — Hide AWX behind a hardened proxy, add rate limiting
• Co-hosting — Run AWX alongside other services on the same server
See also: Automating Nginx Reverse Proxy Setup for Flask on RHEL
AWX with Nginx Reverse Proxy
Basic Nginx Configuration
# /etc/nginx/sites-available/awx.conf
upstream awx_backend {
server 127.0.0.1:8052;
}
server {
listen 80;
server_name awx.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name awx.example.com;
# SSL certificates (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/awx.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/awx.example.com/privkey.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Proxy settings
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Increase timeouts for long-running API calls
proxy_connect_timeout 120;
proxy_send_timeout 120;
proxy_read_timeout 120;
# Max upload size (for project imports)
client_max_body_size 50M;
# Main application
location / {
proxy_pass http://awx_backend;
}
# WebSocket support (critical for live job output)
location /websocket/ {
proxy_pass http://awx_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# API endpoints
location /api/ {
proxy_pass http://awx_backend;
proxy_buffering off;
}
# Static files (optional — let AWX serve them or cache here)
location /static/ {
proxy_pass http://awx_backend;
proxy_cache_valid 200 1d;
add_header Cache-Control "public, max-age=86400";
}
}
Deploy with Ansible
---
- name: Configure Nginx reverse proxy for AWX
hosts: proxy_server
become: true
vars:
awx_domain: awx.example.com
awx_backend: 127.0.0.1:8052
certbot_email: admin@example.com
tasks:
- name: Install Nginx and Certbot
ansible.builtin.apt:
name:
- nginx
- certbot
- python3-certbot-nginx
state: present
update_cache: true
- name: Deploy Nginx configuration
ansible.builtin.template:
src: awx-nginx.conf.j2
dest: "/etc/nginx/sites-available/{{ awx_domain }}.conf"
mode: '0644'
notify: reload nginx
- name: Enable site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ awx_domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ awx_domain }}.conf"
state: link
notify: reload nginx
- name: Obtain SSL certificate
ansible.builtin.command:
cmd: >
certbot --nginx -d {{ awx_domain }}
--non-interactive --agree-tos
--email {{ certbot_email }}
creates: "/etc/letsencrypt/live/{{ awx_domain }}/fullchain.pem"
- name: Enable certbot auto-renewal
ansible.builtin.cron:
name: certbot renewal
minute: "0"
hour: "3"
job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
AWX with Traefik Reverse Proxy
Docker Compose with Traefik
# docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-certs:/letsencrypt
networks:
- proxy
awx-web:
image: quay.io/ansible/awx:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.awx.rule=Host(`awx.example.com`)"
- "traefik.http.routers.awx.entrypoints=websecure"
- "traefik.http.routers.awx.tls.certresolver=letsencrypt"
- "traefik.http.services.awx.loadbalancer.server.port=8052"
# HTTP to HTTPS redirect
- "traefik.http.routers.awx-http.rule=Host(`awx.example.com`)"
- "traefik.http.routers.awx-http.entrypoints=web"
- "traefik.http.routers.awx-http.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# Security headers
- "traefik.http.middlewares.awx-headers.headers.stsSeconds=63072000"
- "traefik.http.middlewares.awx-headers.headers.frameDeny=true"
- "traefik.http.middlewares.awx-headers.headers.contentTypeNosniff=true"
- "traefik.http.routers.awx.middlewares=awx-headers"
networks:
- proxy
depends_on:
- awx-postgres
- awx-redis
awx-postgres:
image: postgres:15
environment:
POSTGRES_DB: awx
POSTGRES_USER: awx
POSTGRES_PASSWORD: "${AWX_DB_PASSWORD}"
volumes:
- awx-postgres:/var/lib/postgresql/data
networks:
- proxy
awx-redis:
image: redis:7
networks:
- proxy
volumes:
traefik-certs:
awx-postgres:
networks:
proxy:
driver: bridge
Traefik File Configuration
# traefik/dynamic/awx.yml
http:
routers:
awx:
rule: "Host(`awx.example.com`)"
service: awx
entryPoints:
- websecure
tls:
certResolver: letsencrypt
middlewares:
- awx-headers
- rate-limit
awx-ws:
rule: "Host(`awx.example.com`) && PathPrefix(`/websocket`)"
service: awx
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
awx:
loadBalancer:
servers:
- url: "http://awx-web:8052"
healthCheck:
path: /api/v2/ping/
interval: 30s
timeout: 5s
middlewares:
awx-headers:
headers:
stsSeconds: 63072000
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
rate-limit:
rateLimit:
average: 100
burst: 50
See also: Ansible Nginx: Install, Configure & Automate (Complete Guide)
Kubernetes Ingress
Nginx Ingress Controller
# awx-ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: awx-ingress
namespace: awx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
spec:
ingressClassName: nginx
tls:
- hosts:
- awx.example.com
secretName: awx-tls
rules:
- host: awx.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: awx-service
port:
number: 80
Deploy Ingress with Ansible
- name: Configure AWX Kubernetes ingress
kubernetes.core.k8s:
state: present
definition: "{{ lookup('file', 'awx-ingress.yml') }}"
AWX Configuration for Reverse Proxy
Set External URL
# Via AWX CLI
awx-manage shell -c "
from django.conf import settings
from awx.conf.models import Setting
Setting.objects.update_or_create(
key='TOWER_URL_BASE',
defaults={'value': '\"https://awx.example.com\"'}
)
"
Or in the AWX UI: Settings → System → Base URL of the service → https://awx.example.com
Trusted Proxies
# AWX settings
REMOTE_HOST_HEADERS:
- HTTP_X_FORWARDED_FOR
- REMOTE_ADDR
- REMOTE_HOST
# Ensure AWX trusts the proxy headers
PROXY_IP_ALLOWED_LIST:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
See also: Ansible London Meetup 2024 Recap: Highlights and Insights
WebSocket Configuration
WebSockets are critical for AWX — they stream live job output. Without proper WebSocket proxying, job output only updates on page refresh.
Test WebSocket Connection
# Install wscat
npm install -g wscat
# Test WebSocket
wscat -c wss://awx.example.com/websocket/
# Should connect without errors
# Type: {"xrftoken": "test"}
Common WebSocket Issues
# ❌ Missing Upgrade headers — WebSocket fails silently
location /websocket/ {
proxy_pass http://awx_backend;
}
# ✅ Correct — with Upgrade and Connection headers
location /websocket/ {
proxy_pass http://awx_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400; # Keep connection alive
}
Health Check Endpoint
# Health check for load balancers
location /api/v2/ping/ {
proxy_pass http://awx_backend;
proxy_connect_timeout 5;
proxy_read_timeout 5;
access_log off;
}
# Ansible health check
- name: Verify AWX is accessible through proxy
ansible.builtin.uri:
url: https://awx.example.com/api/v2/ping/
validate_certs: true
status_code: 200
register: awx_health
retries: 10
delay: 5
until: awx_health.status == 200
Rate Limiting and Security
Nginx Rate Limiting
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=awx_api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=awx_login:10m rate=5r/m;
server {
# Rate limit API
location /api/ {
limit_req zone=awx_api burst=50 nodelay;
proxy_pass http://awx_backend;
}
# Strict rate limit on login
location /api/login/ {
limit_req zone=awx_login burst=3 nodelay;
proxy_pass http://awx_backend;
}
}
IP Allowlisting
# Restrict AWX to internal networks
location / {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://awx_backend;
}
Caddy Configuration
Caddy automatically manages SSL certificates via Let's Encrypt — zero configuration needed for HTTPS:
# /etc/caddy/Caddyfile
awx.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
transport http {
read_timeout 300s
}
}
request_body {
max_size 50MB
}
}
Caddy handles WebSocket upgrades automatically — no extra configuration needed.
sudo systemctl reload caddy
Apache Configuration
# /etc/apache2/sites-available/awx.conf
<VirtualHost *:80>
ServerName awx.example.com
Redirect permanent / https://awx.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName awx.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/awx.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/awx.example.com/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
# WebSocket support
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/websocket/(.*) ws://127.0.0.1:8080/websocket/$1 [P,L]
ProxyTimeout 300
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
Enable required Apache modules:
sudo a2enmod proxy proxy_http proxy_wstunnel ssl rewrite headers
sudo a2ensite awx.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
AWX Configuration for Reverse Proxy
Configure AWX to trust the proxy headers regardless of which reverse proxy you use:
extra_settings:
- setting: CSRF_TRUSTED_ORIGINS
value: '["https://awx.example.com"]'
- setting: REMOTE_HOST_HEADERS
value: '["HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"]'
- setting: USE_X_FORWARDED_HOST
value: 'True'
- setting: USE_X_FORWARDED_PORT
value: 'True'
For AWX Operator on Kubernetes:
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: awx
spec:
extra_settings:
- setting: CSRF_TRUSTED_ORIGINS
value: '["https://awx.example.com"]'
FAQ
Why does AWX job output not update in real-time?
WebSocket connection is not working through your proxy. Ensure you have Upgrade and Connection headers set for the /websocket/ path, and proxy_read_timeout is high enough (86400 seconds) to keep the connection alive.
Do I need to change AWX's built-in SSL settings?
No. With a reverse proxy handling SSL termination, AWX communicates with the proxy over plain HTTP. The proxy handles all HTTPS. Set X-Forwarded-Proto: https so AWX knows the original request was secure.
How do I handle multiple AWX instances behind a load balancer?
Use sticky sessions (session affinity) — AWX uses session-based authentication that needs to hit the same backend. In Nginx, use ip_hash in the upstream block. In Traefik, use the sticky cookie middleware.
Can I run AWX on a subpath like /awx/?
AWX doesn't natively support subpath deployment. It expects to run at the root path. Use a subdomain (awx.example.com) instead of a subpath (example.com/awx/).
Conclusion
A reverse proxy in front of AWX gives you SSL, custom domains, and proper WebSocket support for live job output. The critical configuration is the WebSocket proxy for /websocket/ — without it, job output won't stream in real-time. Use Nginx for simplicity, Traefik for Docker/container environments, or Kubernetes Ingress for K8s deployments.
Related Articles
• AWX Installation Guide • Ansible for Kubernetes • Ansible Connection Types • Ansible Vault Deep DiveCategory: installation