Guide

Deployment Guide

Deploying to production (Docker, Kubernetes, serverless)


Overview

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.


Prerequisites

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

Installation

Docker

  1. Pull or build the tlsproxy image.

    If you maintain a private registry image, pull it:

    docker pull your-registry/tlsproxy:latest
    

    Alternatively, build from source:

    git clone https://github.com/your-org/tlsproxy.git
    cd tlsproxy
    docker build -t tlsproxy:latest .
    
  2. 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
    
  3. 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
    
  4. 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

  1. Create a namespace for tlsproxy.

    kubectl create namespace tlsproxy
    
  2. Store your TLS credentials as a Kubernetes Secret.

    kubectl create secret tls tlsproxy-tls \
      --cert=server.crt \
      --key=server.key \
      -n tlsproxy
    
  3. Create a ConfigMap for tlsproxy configuration.

    kubectl create configmap tlsproxy-config \
      --from-file=config.yaml=/etc/tlsproxy/config.yaml \
      -n tlsproxy
    
  4. 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
    
  5. Verify the pods are running.

    kubectl get pods -n tlsproxy
    

Serverless (container-based, e.g., AWS App Runner / Cloud Run)

  1. 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
    
  2. 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
    
  3. 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.


Configuration

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.

KeyEnvironment VariableDefaultValid ValuesEffect
listen_hostTLSPROXY_LISTEN_HOST0.0.0.0Any valid IP or hostnameThe interface tlsproxy binds to. Set to 127.0.0.1 to restrict to loopback.
listen_portTLSPROXY_LISTEN_PORT84431–65535The port on which tlsproxy accepts TLS connections. Use 443 for standard HTTPS (may require elevated privileges or CAP_NET_BIND_SERVICE).
backend_hostTLSPROXY_BACKEND_HOST127.0.0.1Any valid IP or hostnameThe address of your plain-text backend service. Keep this on a private network in production.
backend_portTLSPROXY_BACKEND_PORT80801–65535The TCP port of your backend service.
tls_certTLSPROXY_TLS_CERT(required)Absolute file pathPath to the PEM-encoded TLS certificate. In Kubernetes, mount this from a Secret.
tls_keyTLSPROXY_TLS_KEY(required)Absolute file pathPath to the PEM-encoded private key. Keep this file readable only by the tlsproxy process user.
log_levelTLSPROXY_LOG_LEVELinfodebug, info, warning, errorControls log verbosity. Use debug during initial deployment to trace connection issues; revert to info or warning in steady-state production.
connection_timeoutTLSPROXY_CONNECTION_TIMEOUT30Positive integer (seconds)Seconds before an idle or stalled connection is dropped. Reduce this under high load to free resources quickly.
max_connectionsTLSPROXY_MAX_CONNECTIONS1000Positive integerMaximum simultaneous proxied connections. Requests beyond this limit are rejected immediately. Size this based on your backend capacity.

Security recommendations

  • Never commit tls_key or 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.

Usage

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

Examples

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

Troubleshooting

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 ps shows the container in Exited state 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 a FileNotFoundError or YAML parse error. Confirm the volume mounts in your docker run or 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: curl or 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_port in your config matches the port clients are connecting to. Confirm backend_host/backend_port point 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:8443 returns curl: (7) Failed to connect.
  • Likely cause: Firewall or security group rules are blocking inbound traffic on the proxy port, or the Kubernetes Service EXTERNAL-IP is 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 tlsproxy and 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_port unreachable), the backend crashed, or connection_timeout is 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, increase connection_timeout in your config and reload.

Certificate is not reloaded after SIGHUP

  • Symptom: After rotating certs and sending SIGHUP, openssl s_client still 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 files errors in logs.
  • Likely cause: max_connections is 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_connections to no more than ulimit -n minus a safety margin (e.g., 80% of the limit). Lower connection_timeout to reclaim stalled connections faster. In Docker, pass --ulimit nofile=65536:65536; in Kubernetes, set resource limits and configure the container's securityContext appropriately.