Deployment Guide
Deploying to production (Docker, Kubernetes, serverless)
This guide walks you through deploying tlsproxy to production environments, including Docker containers, Kubernetes clusters, and serverless platforms. tlsproxy is a Twisted-based TLS termination proxy that accepts encrypted inbound connections and forwards them as plain-text to your backend services — letting your backends remain simple while your edge handles TLS correctly. By the end of this guide you will have a running, production-ready tlsproxy deployment that terminates TLS and routes traffic to one or more backend services.
Before you begin, ensure you have the following in place:
- Python 3.8+ installed on your build or host machine
- Twisted 22.1+ (
pip install twisted[tls]) - Docker 20.10+ (for container-based deployments)
- kubectl 1.25+ and access to a Kubernetes cluster (for Kubernetes deployments)
- A valid TLS certificate and private key for the domain(s) tlsproxy will terminate — PEM format is expected
- A backend service already running and reachable on a plain-text TCP/HTTP port within the same network
- Network/firewall rules that allow inbound traffic on the port tlsproxy will listen on (default:
8443) - Sufficient permissions to bind to privileged ports if you intend to listen on port
443
Docker
-
Pull or build the tlsproxy image.
If you maintain a private registry image, pull it:
docker pull your-registry/tlsproxy:latestAlternatively, build from source:
git clone https://github.com/your-org/tlsproxy.git cd tlsproxy docker build -t tlsproxy:latest . -
Place your TLS credentials on the host.
mkdir -p /etc/tlsproxy/certs cp server.crt /etc/tlsproxy/certs/server.crt cp server.key /etc/tlsproxy/certs/server.key -
Write a minimal configuration file (see the Configuration section for all options).
cat > /etc/tlsproxy/config.yaml <<'EOF' listen_port: 8443 backend_host: "127.0.0.1" backend_port: 8080 tls_cert: "/certs/server.crt" tls_key: "/certs/server.key" EOF -
Start the container.
docker run -d \ --name tlsproxy \ -p 8443:8443 \ -v /etc/tlsproxy/certs:/certs:ro \ -v /etc/tlsproxy/config.yaml:/etc/tlsproxy/config.yaml:ro \ tlsproxy:latest
Kubernetes
-
Create a namespace for tlsproxy.
kubectl create namespace tlsproxy -
Store your TLS credentials as a Kubernetes Secret.
kubectl create secret tls tlsproxy-tls \ --cert=server.crt \ --key=server.key \ -n tlsproxy -
Create a ConfigMap for tlsproxy configuration.
kubectl create configmap tlsproxy-config \ --from-file=config.yaml=/etc/tlsproxy/config.yaml \ -n tlsproxy -
Apply the Deployment and Service manifests (see the Examples section for complete YAML).
kubectl apply -f tlsproxy-deployment.yaml -n tlsproxy kubectl apply -f tlsproxy-service.yaml -n tlsproxy -
Verify the pods are running.
kubectl get pods -n tlsproxy
Serverless (container-based, e.g., AWS App Runner / Cloud Run)
-
Push your Docker image to your cloud registry.
docker tag tlsproxy:latest gcr.io/your-project/tlsproxy:latest docker push gcr.io/your-project/tlsproxy:latest -
Deploy to Cloud Run (example; adapt flags to your provider).
gcloud run deploy tlsproxy \ --image gcr.io/your-project/tlsproxy:latest \ --port 8443 \ --min-instances 1 \ --set-env-vars TLSPROXY_BACKEND_HOST=10.0.0.5,TLSPROXY_BACKEND_PORT=8080 -
Mount your TLS secret using your provider's secret manager integration so the certificate and key are available at the paths referenced in your config file.
tlsproxy reads its configuration from a YAML file (default path: /etc/tlsproxy/config.yaml) and supports overrides via environment variables prefixed with TLSPROXY_. The table below describes every key option.
| Key | Environment Variable | Default | Valid Values | Effect |
|---|---|---|---|---|
listen_host | TLSPROXY_LISTEN_HOST | 0.0.0.0 | Any valid IP or hostname | The interface tlsproxy binds to. Set to 127.0.0.1 to restrict to loopback. |
listen_port | TLSPROXY_LISTEN_PORT | 8443 | 1–65535 | The port on which tlsproxy accepts TLS connections. Use 443 for standard HTTPS (may require elevated privileges or CAP_NET_BIND_SERVICE). |
backend_host | TLSPROXY_BACKEND_HOST | 127.0.0.1 | Any valid IP or hostname | The address of your plain-text backend service. Keep this on a private network in production. |
backend_port | TLSPROXY_BACKEND_PORT | 8080 | 1–65535 | The TCP port of your backend service. |
tls_cert | TLSPROXY_TLS_CERT | (required) | Absolute file path | Path to the PEM-encoded TLS certificate. In Kubernetes, mount this from a Secret. |
tls_key | TLSPROXY_TLS_KEY | (required) | Absolute file path | Path to the PEM-encoded private key. Keep this file readable only by the tlsproxy process user. |
log_level | TLSPROXY_LOG_LEVEL | info | debug, info, warning, error | Controls log verbosity. Use debug during initial deployment to trace connection issues; revert to info or warning in steady-state production. |
connection_timeout | TLSPROXY_CONNECTION_TIMEOUT | 30 | Positive integer (seconds) | Seconds before an idle or stalled connection is dropped. Reduce this under high load to free resources quickly. |
max_connections | TLSPROXY_MAX_CONNECTIONS | 1000 | Positive integer | Maximum simultaneous proxied connections. Requests beyond this limit are rejected immediately. Size this based on your backend capacity. |
Security recommendations
- Never commit
tls_keyor any file containing your private key to source control. - Set file permissions so only the tlsproxy process user can read the key:
chmod 600 server.key. - In Kubernetes, reference the TLS Secret rather than baking credentials into your image or ConfigMap.
Once tlsproxy is running, your clients connect to it over TLS and are transparently forwarded to your plain-text backend. From a client's perspective, they are talking to a normal HTTPS service.
Verifying connectivity after deployment
Use curl with your certificate to confirm TLS termination is working end-to-end:
curl -v --cacert /path/to/ca.crt https://your-proxy-host:8443/health
If your certificate is self-signed during testing, add -k (insecure) — never use -k in production automation.
Sending API requests through the proxy
Because tlsproxy is transparent, your existing HTTP client code needs no changes — just point it at the tlsproxy host and port instead of the backend directly:
# Before: direct to backend (plain HTTP, internal network only)
curl http://backend-service:8080/api/v1/resource
# After: through tlsproxy (TLS, externally reachable)
curl https://your-proxy-host:8443/api/v1/resource
Reloading configuration without downtime
Send SIGHUP to the tlsproxy process to reload the configuration file (including reloading rotated TLS certificates) without dropping existing connections:
# Docker
docker kill --signal=SIGHUP tlsproxy
# Kubernetes — send signal to the running pod
kubectl exec -n tlsproxy <pod-name> -- kill -HUP 1
Graceful shutdown
To stop tlsproxy and allow in-flight requests to complete:
# Docker
docker stop tlsproxy # sends SIGTERM; tlsproxy drains connections
# Kubernetes
kubectl rollout restart deployment/tlsproxy -n tlsproxy
Example 1 — Docker Compose stack with a Node.js backend
This is the most common local-to-production pattern: tlsproxy in one container, your app in another, sharing a private Docker network.
# docker-compose.yml
version: "3.9"
services:
app:
image: node:18-alpine
command: node server.js
expose:
- "3000"
networks:
- internal
tlsproxy:
image: tlsproxy:latest
ports:
- "443:8443"
volumes:
- ./certs:/certs:ro
- ./tlsproxy-config.yaml:/etc/tlsproxy/config.yaml:ro
networks:
- internal
depends_on:
- app
networks:
internal:
driver: bridge
# tlsproxy-config.yaml
listen_port: 8443
backend_host: "app"
backend_port: 3000
tls_cert: "/certs/server.crt"
tls_key: "/certs/server.key"
log_level: "info"
docker compose up -d
curl -v --cacert ./certs/ca.crt https://localhost/api/v1/ping
Expected output:
* SSL connection using TLSv1.3 / ...
* Server certificate: ...
< HTTP/1.1 200 OK
{"status":"ok"}
Example 2 — Kubernetes Deployment and Service
A complete manifest that deploys two replicas of tlsproxy, mounts the TLS Secret, and exposes it via a LoadBalancer Service.
# tlsproxy-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tlsproxy
namespace: tlsproxy
spec:
replicas: 2
selector:
matchLabels:
app: tlsproxy
template:
metadata:
labels:
app: tlsproxy
spec:
containers:
- name: tlsproxy
image: your-registry/tlsproxy:latest
ports:
- containerPort: 8443
volumeMounts:
- name: tls-certs
mountPath: /certs
readOnly: true
- name: config
mountPath: /etc/tlsproxy
readOnly: true
env:
- name: TLSPROXY_BACKEND_HOST
value: "backend-service.default.svc.cluster.local"
- name: TLSPROXY_BACKEND_PORT
value: "8080"
volumes:
- name: tls-certs
secret:
secretName: tlsproxy-tls
- name: config
configMap:
name: tlsproxy-config
---
apiVersion: v1
kind: Service
metadata:
name: tlsproxy
namespace: tlsproxy
spec:
type: LoadBalancer
selector:
app: tlsproxy
ports:
- port: 443
targetPort: 8443
kubectl apply -f tlsproxy-deployment.yaml
kubectl get svc -n tlsproxy
Expected output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tlsproxy LoadBalancer 10.96.0.42 203.0.113.10 443:30443/TCP 45s
Example 3 — Certificate rotation with zero downtime
After renewing your certificate and replacing the files on disk, signal tlsproxy to reload without dropping connections:
# Replace cert files
cp new-server.crt /etc/tlsproxy/certs/server.crt
cp new-server.key /etc/tlsproxy/certs/server.key
# Signal reload
docker kill --signal=SIGHUP tlsproxy
# Verify new cert is live
openssl s_client -connect localhost:8443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
Expected output:
notBefore=Jan 1 00:00:00 2025 GMT
notAfter=Apr 1 00:00:00 2026 GMT
Use this section to diagnose the most common production issues. Each issue follows the pattern Symptom → Likely Cause → Fix.
tlsproxy container exits immediately on startup
- Symptom:
docker psshows the container inExitedstate within seconds of starting. - Likely cause: Missing or unreadable TLS certificate/key files, or a malformed config file.
- Fix: Check container logs with
docker logs tlsproxy. Look for aFileNotFoundErroror YAML parse error. Confirm the volume mounts in yourdocker runor Compose file point to the correct host paths and that the files exist.
TLS handshake fails — clients receive SSL_ERROR_RX_RECORD_TOO_LONG or similar
- Symptom:
curlor a browser reports a TLS handshake error; connections are refused or reset. - Likely cause: tlsproxy is listening on the correct port but the backend is accidentally receiving raw TLS instead of plain TCP, or the listen port is mis-configured and a non-TLS service is responding instead.
- Fix: Confirm
listen_portin your config matches the port clients are connecting to. Confirmbackend_host/backend_portpoint to a plain-text HTTP service, not another TLS endpoint.
Connection refused when reaching the proxy from outside the cluster
- Symptom:
curl https://your-proxy-host:8443returnscurl: (7) Failed to connect. - Likely cause: Firewall or security group rules are blocking inbound traffic on the proxy port, or the Kubernetes Service
EXTERNAL-IPis still<pending>. - Fix: In cloud environments, add an inbound rule for the proxy port to your security group or firewall policy. For Kubernetes, run
kubectl describe svc tlsproxy -n tlsproxyand check the Events section for LoadBalancer provisioning errors.
Backend returns 502 Bad Gateway or connection is reset after proxying
- Symptom: TLS handshake succeeds, but the proxied request returns a 5xx error or the connection drops mid-response.
- Likely cause: tlsproxy cannot reach the backend (
backend_host/backend_portunreachable), the backend crashed, orconnection_timeoutis too low for slow responses. - Fix: From inside the tlsproxy container, test backend reachability directly:
curl http://<backend_host>:<backend_port>/health. If unreachable, fix network routing or DNS. If the backend is slow, increaseconnection_timeoutin your config and reload.
Certificate is not reloaded after SIGHUP
- Symptom: After rotating certs and sending
SIGHUP,openssl s_clientstill reports the old certificate expiry. - Likely cause: The new certificate file was not fully written to disk before the signal was sent, or the volume mount is read-only and the file replacement did not propagate.
- Fix: Verify the new file is on disk (
openssl x509 -in /etc/tlsproxy/certs/server.crt -noout -dates) before sending the signal. In Kubernetes, Secret updates can take up to 60 seconds to propagate to volume mounts — wait for the kubelet sync before reloading.
High memory or file-descriptor usage under load
- Symptom: tlsproxy process memory grows steadily; OS reports
too many open fileserrors in logs. - Likely cause:
max_connectionsis set higher than the OS file-descriptor limit (ulimit -n), or connections are not being closed by clients, exhausting the connection pool. - Fix: Set
max_connectionsto no more thanulimit -nminus a safety margin (e.g., 80% of the limit). Lowerconnection_timeoutto reclaim stalled connections faster. In Docker, pass--ulimit nofile=65536:65536; in Kubernetes, set resource limits and configure the container'ssecurityContextappropriately.