HTTPS Configuration
How to put ActionsManager Self-Hosted behind HTTPS for any non-localhost deployment.
Why this is enforced
Section titled “Why this is enforced”ActionsManager refuses to start if APP_URL is set to a non-loopback http:// address without ALLOW_INSECURE_HTTP=true, and it separately blocks PAT login over non-local HTTP as defense-in-depth. localhost/127.0.0.1/::1 are always exempt — this only applies once you’re serving the app on a real network address or domain.
If you’re intentionally testing without HTTPS (not recommended for production), set:
ALLOW_INSECURE_HTTP=trueFor anything else, put a reverse proxy in front of ActionsManager that terminates TLS. Pick whichever of the three below fits your infrastructure.
Option 1: Caddy (easiest — automatic HTTPS)
Section titled “Option 1: Caddy (easiest — automatic HTTPS)”Caddy automatically obtains and renews TLS certificates from Let’s Encrypt with zero extra configuration.
Caddyfile:
actionsmanager.example.com { reverse_proxy localhost:8080
encode gzip zstd
header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" }}Install and run:
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.listsudo apt updatesudo apt install caddy
sudo systemctl enable caddysudo systemctl start caddyOption 2: Traefik (Docker-native)
Section titled “Option 2: Traefik (Docker-native)”Traefik integrates directly with Docker Compose via labels and automatic service discovery.
services: traefik: image: traefik:v2.10 command: - "--api.insecure=false" - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - "--entrypoints.web.http.redirections.entryPoint.scheme=https" ports: - "80:80" - "443:443" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./letsencrypt:/letsencrypt" networks: - web
actionsmanager: image: ghcr.io/greywolflabs/actions-manager:latest env_file: - .env.self-hosted environment: - APP_URL=https://actionsmanager.example.com volumes: - ./data:/app/data labels: - "traefik.enable=true" - "traefik.http.routers.actionsmanager.rule=Host(`actionsmanager.example.com`)" - "traefik.http.routers.actionsmanager.entrypoints=websecure" - "traefik.http.routers.actionsmanager.tls.certresolver=letsencrypt" - "traefik.http.services.actionsmanager.loadbalancer.server.port=8080" networks: - web
networks: web: driver: bridgemkdir -p letsencrypt && chmod 600 letsencryptdocker compose -f docker-compose.traefik.yml up -dOption 3: Nginx (manual configuration)
Section titled “Option 3: Nginx (manual configuration)”Maximum control, manual TLS certificate setup via Certbot.
upstream backend { server localhost:8080;}
server { listen 80; listen [::]:80; server_name actions-manager.example.com; return 301 https://$server_name$request_uri;}
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name actions-manager.example.com;
ssl_certificate /etc/letsencrypt/live/actions-manager.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/actions-manager.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 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; proxy_cache_bypass $http_upgrade;
# WebSocket connections need long timeouts proxy_read_timeout 86400; proxy_send_timeout 86400; }}sudo ln -s /etc/nginx/sites-available/actions-manager /etc/nginx/sites-enabled/actions-managersudo nginx -tsudo systemctl restart nginx
sudo apt-get install certbot python3-certbot-nginxsudo certbot certonly --nginx -d actions-manager.example.comsudo certbot renew --dry-runPoint ActionsManager at the new HTTPS URL
Section titled “Point ActionsManager at the new HTTPS URL”Whichever proxy you chose, update .env.self-hosted (or your docker run -e flags) and restart:
APP_URL=https://actionsmanager.example.com
docker compose -f docker-compose.self-hosted.yml restartAPP_URL derives the frontend URL, backend URL, OAuth callback, and WebSocket URL (wss:// for an https:// APP_URL) automatically — you don’t need to set them individually. See Installation for the full environment variable reference.
Troubleshooting: PAT login still blocked behind HTTPS
Section titled “Troubleshooting: PAT login still blocked behind HTTPS”If the browser shows a padlock but signing in with a token fails with:
PAT login over non-local HTTP is disabled for security.
then the backend is not being told the request arrived over HTTPS. Your proxy terminates TLS and forwards plain HTTP to port 8080, so the backend can only learn the original scheme from the X-Forwarded-Proto header.
Check your proxy sends it. Caddy and Traefik set X-Forwarded-Proto: https automatically — no configuration needed. A hand-written nginx proxy must set it explicitly:
proxy_set_header X-Forwarded-Proto $scheme;Check your image is recent enough. Older images ran an internal nginx that overwrote the header with its own (always plain HTTP) scheme, so the proxy’s https never reached the backend no matter what the proxy sent. If your proxy demonstrably sets the header and login is still refused, that is the symptom. Pull the current image and restart:
docker compose -f docker-compose.self-hosted.yml pulldocker compose -f docker-compose.self-hosted.yml up -dKeep the published port private. ActionsManager reads the scheme from the header your proxy sends, so port 8080 should be reachable only by that proxy — bind it to loopback (127.0.0.1:8080:8080) or keep it on an internal Docker network. Anything that can reach the port directly can claim its own connection is HTTPS.
As a stop-gap, ALLOW_INSECURE_HTTP=true unblocks PAT login while you sort the header out. It disables the check entirely, so understand what it exposes: the hop from your proxy to the container is plain HTTP, and tokens cross it in cleartext. That is contained if the proxy runs on the container’s own host, and a genuine interception risk if it reaches the container across a network. Fixing the header is the real answer.
© 2026 ActionsManager. All rights reserved.