Skip to content

Deploying Caddy

Introduction

Caddy is the ultimate HTTP/1.1, HTTP/2, and HTTP/3 web server with automatic HTTPS by default. Built in Go for memory safety and performance, Caddy serves as a powerful web server, reverse proxy, and API gateway that simplifies complex infrastructure into easy-to-manage configurations. With built-in support for automatic certificate provisioning, load balancing, and dynamic configuration, Caddy provides enterprise-grade capabilities while maintaining simplicity and ease of use.

Key Features

  • Automatic HTTPS: All sites served over HTTPS by default with automatic Let’s Encrypt and ZeroSSL certificate management
  • On-Demand TLS: Dynamic certificate provisioning for custom domains and multi-tenant applications
  • HTTP/3 Support: Modern HTTP/3 (QUIC) protocol support alongside HTTP/1.1 and HTTP/2
  • Reverse Proxy: Powerful reverse proxy with load balancing, health checks, and dynamic backends
  • Caddyfile Configuration: Simple, human-readable configuration format for common use cases
  • JSON Config API: Full RESTful API for dynamic configuration changes without downtime
  • Config Adapters: Support for NGINX, YAML, TOML, and other configuration formats via adapters
  • Load Balancing: Advanced load balancing with policy selection and circuit breaking
  • Static File Server: Production-grade file serving with compression, range requests, and directory browsing
  • Middleware Extensibility: Pluggable middleware system for request/response manipulation
  • FastCGI Proxy: Support for PHP and FastCGI applications
  • WebSocket Support: Full WebSocket proxying for real-time applications
  • gRPC Proxying: Native gRPC protocol support for microservices
  • Health Checks: Active and passive health checking for upstream servers
  • Rate Limiting: Built-in request rate limiting and concurrency control
  • Access Logging: Comprehensive JSON and plain text logging with configurable formats
  • Automatic Redirects: HTTP to HTTPS and www redirection
  • Virtual Hosts: Support for multiple domains and subdomains in a single configuration
  • Request Matching: Advanced request matching for routing and conditional logic
  • Response Headers: Easy manipulation of response headers and caching directives
  • File Compression: On-the-fly Gzip and Zstandard compression for optimal bandwidth usage
  • Graceful Reloads: Zero-downtime configuration updates and deployments
  • TLS Customization: Full control over cipher suites, protocols, and certificate management

Prerequisites

Before deploying Caddy on Klutch.sh, ensure you have:

  • A Klutch.sh account with access to the dashboard
  • A GitHub repository for your Caddy deployment (public or private)
  • Basic understanding of web server concepts and HTTP protocols
  • (Optional) Custom domains for HTTPS certificate provisioning

Important Considerations

Deployment Steps

  1. Create a Dockerfile

    Create a Dockerfile in the root of your repository with a Caddy image. This Dockerfile sets up Caddy with the latest version and necessary utilities:

    FROM caddy:latest
    # Copy Caddyfile configuration
    COPY Caddyfile /etc/caddy/Caddyfile
    # Optionally copy additional files
    # COPY public /srv/public
    # Caddy will automatically manage TLS certificates
    # Expose HTTP and HTTPS ports
    EXPOSE 80 443
    # Start Caddy
    CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
  2. Create Caddyfile Configuration

    Create a Caddyfile in your repository root with basic web server or reverse proxy configuration:

    # Simple static file server
    example-app.klutch.sh {
    root * /srv/public
    file_server
    encode gzip
    }
    # Or for reverse proxy to upstream service
    # example-app.klutch.sh {
    # reverse_proxy localhost:3000 {
    # header_up X-Forwarded-Proto {http.request.proto}
    # header_up X-Forwarded-For {http.request.remote}
    # }
    # }
  3. Create Configuration Files

    Create additional configuration files for local development:

    docker-compose.yml (for local testing only):

    version: '3.8'
    services:
    caddy:
    image: caddy:latest
    container_name: caddy-dev
    ports:
    - "80:80"
    - "443:443"
    - "2019:2019"
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile
    - ./public:/srv/public
    - caddy_data:/data
    - caddy_config:/config
    environment:
    - CADDY_ADMIN=0.0.0.0:2019
    volumes:
    caddy_data:
    caddy_config:

    entrypoint.sh (optional startup script):

    #!/bin/bash
    set -e
    # Wait for any upstream services if needed
    # sleep 5
    # Start Caddy
    caddy run --config /etc/caddy/Caddyfile
  4. Create Environment Variables File

    Create a .env.example file for configuration documentation:

    # Caddy Configuration
    CADDY_ADMIN_ADDR=0.0.0.0:2019
    CADDY_LOG_LEVEL=info
    # Upstream Services (if using as reverse proxy)
    UPSTREAM_HOST=localhost
    UPSTREAM_PORT=3000
    # Domain Configuration
    DOMAIN=example-app.klutch.sh
    # TLS Configuration
    TLS_ISSUER=letsencrypt
    TLS_EMAIL=admin@example.com
    # Performance Settings
    CADDY_MAX_HEADER_SIZE=1048576
    CADDY_TIMEOUT=30s
  5. Create .gitignore File

    Create a .gitignore file to exclude sensitive files from version control:

    # Caddy certificate and key data
    /data/
    /config/
    # Environment files
    .env
    .env.local
    # Certificate files
    *.crt
    *.key
    *.pem
    # Temporary files
    *.tmp
    *.log
    # OS files
    .DS_Store
    Thumbs.db
  6. Push to GitHub

    Push your repository to GitHub with all configuration files:

    Terminal window
    git add Dockerfile Caddyfile docker-compose.yml .env.example .gitignore
    git commit -m "Initial Caddy deployment configuration"
    git push origin main
  7. Deploy on Klutch.sh

    1. Navigate to klutch.sh/app and log in to your dashboard
    2. Click Create New App
    3. Select Docker as the deployment method (detected automatically from your Dockerfile)
    4. Connect your GitHub repository containing the Caddy configuration
    5. Confirm the Dockerfile is detected from your root directory
    6. Click Deploy to start the deployment process
    7. Wait for the deployment to complete and your app to become active
  8. Configure Traffic Rules

    1. In your app dashboard, navigate to Traffic settings
    2. Select HTTP as the traffic type
    3. Set the internal port to 80 (Caddy HTTP port)
    4. Save your traffic configuration
    5. Your Caddy server will be accessible at example-app.klutch.sh
    6. HTTPS is automatically enabled and managed by Caddy

Initial Setup and Configuration

After deploying Caddy on Klutch.sh, access your deployment and configure it for your specific use case.

Access Your Deployment

  1. Your Caddy server is now running at http://example-app.klutch.sh
  2. Caddy automatically upgrades HTTP connections to HTTPS
  3. You should see your configured content or reverse proxy working
  4. The admin API is available at http://example-app.klutch.sh:2019/

Configure as a Reverse Proxy

To use Caddy as a reverse proxy for upstream services, update your Caddyfile:

example-app.klutch.sh {
reverse_proxy localhost:3000 {
# Preserve original request information
header_up X-Forwarded-Proto {http.request.proto}
header_up X-Forwarded-Host {http.request.host}
header_up X-Forwarded-For {http.request.remote}
header_up X-Real-IP {http.request.remote}
# Connection settings
health_uri /health
health_interval 10s
health_timeout 5s
}
}

Load Balancing Multiple Backends

Configure Caddy to load balance across multiple upstream servers:

example-app.klutch.sh {
reverse_proxy localhost:3000 localhost:3001 localhost:3002 {
# Load balancing policy (random, least_conn, round_robin, etc.)
policy least_conn
# Health checks
health_uri /health
health_interval 10s
# Timeout settings
fail_duration 30s
try_duration 10s
}
}

Static File Serving with Compression

Configure Caddy to serve static files with automatic compression:

example-app.klutch.sh {
root * /srv/public
# Enable compression
encode gzip zstd
# Serve precompressed files if available
file_server {
precompressed gzip zstd
}
# Directory browsing (optional)
# file_server browse
}

Admin API Management

The Caddy admin API allows dynamic configuration updates without downtime. Access it at:

curl http://example-app.klutch.sh:2019/config/

Query current configuration:

Terminal window
curl http://example-app.klutch.sh:2019/config/apps/http/servers/srv0/routes

Update configuration dynamically:

Terminal window
curl -X PATCH http://example-app.klutch.sh:2019/config/apps/http/servers/srv0/routes \
-H "Content-Type: application/json" \
-d '[{
"match": [{"host": ["example-app.klutch.sh"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "localhost:3000"}]
}]
}]'

Environment Variables

Basic Configuration

Configure these environment variables through your deployment:

  • CADDY_ADMIN_ADDR=0.0.0.0:2019 - Admin API address and port
  • CADDY_LOG_LEVEL=info - Log level (debug, info, warn, error)
  • CADDY_TIMEOUT=30s - Default timeout for requests

Production Environment Variables

For production deployments using Nixpacks, configure these environment variables:

# Admin API configuration
CADDY_ADMIN_ADDR=0.0.0.0:2019
CADDY_ADMIN_ENFORCE_ORIGIN=true
# Logging configuration
CADDY_LOG_LEVEL=info
CADDY_LOG_FORMAT=json
# TLS and certificate management
CADDY_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
CADDY_ACME_EMAIL=admin@example.com
CADDY_EAB_KID=
CADDY_EAB_HMAC=
# Performance tuning
CADDY_GRACE_PERIOD=5s
CADDY_HTTP_IDLE_TIMEOUT=5m
CADDY_HTTP_READ_TIMEOUT=30s
CADDY_HTTP_WRITE_TIMEOUT=30s
# TLS configuration
CADDY_TLS_PREFER_CURVES=x25519,secp384r1,secp256r1
CADDY_TLS_PROTOCOLS=tls1.2,tls1.3
CADDY_TLS_SESSION_TIMEOUT=168h
# Certificate storage
CADDY_STORAGE=file_system
CADDY_STORAGE_LOCK_TIMEOUT=5s
# HTTP/3 and QUIC
CADDY_QUIC_DISABLE=false
CADDY_QUIC_GEOIP_ASN_DB=
CADDY_QUIC_GEOIP_CITY_DB=

Code Examples

JavaScript - Caddy Admin API Client

// Caddy Admin API Client for configuration management
const axios = require('axios');
class CaddyClient {
constructor(host = 'example-app.klutch.sh', adminPort = 2019) {
this.baseURL = `http://${host}:${adminPort}`;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
});
}
// Get current config
async getConfig() {
try {
const response = await this.client.get('/config/');
return response.data;
} catch (error) {
console.error('Error fetching config:', error.message);
throw error;
}
}
// Update reverse proxy upstream
async updateReverse Proxy(newUpstream) {
try {
const config = await this.getConfig();
// Update upstreams in HTTP app
if (config.apps?.http?.servers?.srv0?.routes) {
const routes = config.apps.http.servers.srv0.routes;
routes.forEach(route => {
route.handle?.forEach(handler => {
if (handler.handler === 'reverse_proxy') {
handler.upstreams = newUpstream.map(url => ({
dial: url
}));
}
});
});
}
await this.client.put('/config/', config);
console.log('Configuration updated successfully');
return config;
} catch (error) {
console.error('Error updating configuration:', error.message);
throw error;
}
}
// Get server stats
async getStats() {
try {
const response = await this.client.get('/stats');
return response.data;
} catch (error) {
console.error('Error fetching stats:', error.message);
throw error;
}
}
// Stop Caddy
async stop() {
try {
const response = await this.client.post('/stop');
return response.data;
} catch (error) {
console.error('Error stopping Caddy:', error.message);
throw error;
}
}
// Reload configuration
async reload() {
try {
const config = await this.getConfig();
await this.client.post('/reload', config);
console.log('Configuration reloaded');
} catch (error) {
console.error('Error reloading configuration:', error.message);
throw error;
}
}
// Add a new route
async addRoute(matcher, upstream) {
try {
const config = await this.getConfig();
if (!config.apps.http.servers.srv0.routes) {
config.apps.http.servers.srv0.routes = [];
}
const newRoute = {
match: [{ host: [matcher] }],
handle: [{
handler: 'reverse_proxy',
upstreams: [{ dial: upstream }]
}]
};
config.apps.http.servers.srv0.routes.push(newRoute);
await this.client.put('/config/', config);
console.log(`Route for ${matcher} added`);
} catch (error) {
console.error('Error adding route:', error.message);
throw error;
}
}
}
// Usage example
async function main() {
const caddy = new CaddyClient('example-app.klutch.sh', 2019);
// Get current configuration
const config = await caddy.getConfig();
console.log('Current config:', JSON.stringify(config, null, 2));
// Update upstreams
await caddy.updateReverseProxy(['localhost:3000', 'localhost:3001']);
// Get server stats
const stats = await caddy.getStats();
console.log('Server stats:', stats);
}
main().catch(console.error);

Bash - Caddy Configuration Management Script

#!/bin/bash
# Caddy Configuration Management Script
# Requires: curl, jq for JSON processing
CADDY_HOST="example-app.klutch.sh"
CADDY_ADMIN_PORT="2019"
CADDY_API="http://${CADDY_HOST}:${CADDY_ADMIN_PORT}"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get current configuration
get_config() {
echo -e "${BLUE}Fetching current configuration...${NC}"
curl -s -X GET "${CADDY_API}/config/" | jq . || echo -e "${RED}Failed to fetch config${NC}"
}
# Get configuration from a specific path
get_config_path() {
local path=$1
echo -e "${BLUE}Fetching config from path: $path${NC}"
curl -s -X GET "${CADDY_API}/config/${path}" | jq . || echo -e "${RED}Failed to fetch config at $path${NC}"
}
# Reload configuration
reload_config() {
echo -e "${YELLOW}Reloading configuration...${NC}"
curl -s -X POST "${CADDY_API}/reload" \
-H "Content-Type: application/json" \
-d "{}" && echo -e "${GREEN}Configuration reloaded${NC}" || echo -e "${RED}Failed to reload${NC}"
}
# List routes
list_routes() {
echo -e "${BLUE}Current routes:${NC}"
curl -s -X GET "${CADDY_API}/config/apps/http/servers/srv0/routes" | jq '.[] | {matcher: .match, handler: .handle[0].handler}'
}
# Add reverse proxy route
add_reverse_proxy() {
local hostname=$1
local upstream=$2
if [ -z "$hostname" ] || [ -z "$upstream" ]; then
echo -e "${RED}Usage: add_reverse_proxy <hostname> <upstream>${NC}"
echo "Example: add_reverse_proxy api.klutch.sh localhost:3000"
return 1
fi
echo -e "${YELLOW}Adding reverse proxy: $hostname -> $upstream${NC}"
local route="{
\"match\": [{\"host\": [\"$hostname\"]}],
\"handle\": [{
\"handler\": \"reverse_proxy\",
\"upstreams\": [{\"dial\": \"$upstream\"}],
\"headers\": {
\"request\": {
\"set\": {
\"X-Forwarded-For\": [\"{http.request.remote}\"],
\"X-Forwarded-Proto\": [\"{http.request.proto}\"],
\"X-Forwarded-Host\": [\"{http.request.host}\"]
}
}
}
}]
}"
curl -s -X POST "${CADDY_API}/config/apps/http/servers/srv0/routes" \
-H "Content-Type: application/json" \
-d "$route" && echo -e "${GREEN}Route added${NC}" || echo -e "${RED}Failed to add route${NC}"
}
# Get server ID
get_id() {
echo -e "${BLUE}Server ID:${NC}"
curl -s -X GET "${CADDY_API}/id"
}
# Get version and build info
get_version() {
echo -e "${BLUE}Caddy version and build info:${NC}"
curl -s -X GET "${CADDY_API}/version"
}
# Check admin API status
check_health() {
echo -e "${YELLOW}Checking admin API status...${NC}"
if curl -s -f "${CADDY_API}/config/" > /dev/null 2>&1; then
echo -e "${GREEN}Admin API is healthy${NC}"
return 0
else
echo -e "${RED}Admin API is not responding${NC}"
return 1
fi
}
# Monitor request metrics
monitor_metrics() {
echo -e "${YELLOW}Monitoring Caddy metrics (press Ctrl+C to stop)...${NC}"
while true; do
clear
echo -e "${BLUE}=== Caddy Metrics ===${NC}"
curl -s -X GET "${CADDY_API}/metrics" | grep http_requests | head -20
sleep 5
done
}
# Main menu
case "${1:-help}" in
get)
get_config
;;
get-path)
get_config_path "$2"
;;
reload)
reload_config
;;
routes)
list_routes
;;
add-proxy)
add_reverse_proxy "$2" "$3"
;;
id)
get_id
;;
version)
get_version
;;
health)
check_health
;;
monitor)
monitor_metrics
;;
*)
echo -e "${YELLOW}Caddy Configuration Management${NC}"
echo "Usage: $0 {get|get-path|reload|routes|add-proxy|id|version|health|monitor} [args]"
echo ""
echo "Commands:"
echo " get - Get entire configuration"
echo " get-path <path> - Get config from specific path"
echo " reload - Reload configuration"
echo " routes - List all routes"
echo " add-proxy <host> <upstream> - Add reverse proxy route"
echo " id - Get server ID"
echo " version - Get version info"
echo " health - Check admin API health"
echo " monitor - Monitor metrics"
;;
esac

Shell - Caddy Server Management Script

#!/bin/sh
# Caddy Server Management Script
# Requires: curl
CADDY_HOST="example-app.klutch.sh"
CADDY_ADMIN_PORT="2019"
CADDY_API="http://${CADDY_HOST}:${CADDY_ADMIN_PORT}"
echo "=== Caddy Server Management ==="
echo "Admin API: $CADDY_API"
echo ""
# Get configuration summary
echo "Configuration Summary:"
curl -s -X GET "${CADDY_API}/config/" | grep -o '"matcher"' | wc -l | xargs echo "Routes found:"
# Get running time
echo ""
echo "Server Status:"
curl -s -X GET "${CADDY_API}/id" | head -5
# Show HTTP listener status
echo ""
echo "Checking HTTP listeners..."
curl -s -X GET "${CADDY_API}/config/apps/http/servers" | head -20
# Get load balance policies
echo ""
echo "Load Balancer Status:"
curl -s -X GET "${CADDY_API}/config/apps/http/servers/srv0/routes" | \
grep -A 5 '"policy"' || echo "No load balancing policies configured"
# Display reverse proxy upstreams
echo ""
echo "Reverse Proxy Upstreams:"
curl -s -X GET "${CADDY_API}/config/apps/http/servers/srv0/routes" | \
grep -o '"dial":[^}]*' | head -10
# Check certificate status
echo ""
echo "TLS Certificate Status:"
curl -s -X GET "${CADDY_API}/config/apps/tls" | head -20
# Get metrics if available
echo ""
echo "Request Metrics (last 10):"
curl -s -X GET "${CADDY_API}/metrics" 2>/dev/null | grep http_requests | head -10 || echo "Metrics not available"
# Validate configuration
echo ""
echo "Configuration Validation:"
curl -s -X POST "${CADDY_API}/validate/config" \
-H "Content-Type: application/json" \
-d "$(curl -s -X GET "${CADDY_API}/config/")" && \
echo "✓ Configuration is valid" || \
echo "✗ Configuration has errors"

Best Practices

  • Use Caddyfile for Simple Configs: For straightforward deployments, write configs in Caddyfile format which is more readable and maintainable
  • Implement Health Checks: Always configure health checks for upstream servers to ensure requests only go to healthy backends
  • Monitor Admin API: Regularly check admin API endpoints to monitor server status and configuration changes
  • Version Control Configuration: Keep Caddyfile and configuration in version control for easy rollbacks and auditing
  • Use Environment Variables: Leverage Nixpacks environment variables for dynamic configuration across environments
  • Implement Rate Limiting: Use Caddy’s rate limiting directives to protect upstream services from abuse
  • Compress Responses: Enable automatic compression (gzip/zstd) for text-based content to reduce bandwidth
  • Set Proper Timeouts: Configure appropriate timeouts for your use case to prevent hanging connections
  • Enable Access Logging: Configure JSON logging for better analysis and debugging of requests
  • Use Graceful Reloads: Leverage admin API for configuration updates to avoid downtime
  • Certificate Management: Monitor certificate renewal status and ensure ACME challenge endpoints are accessible
  • TLS Customization: Review and customize TLS protocols and cipher suites for your security requirements

Troubleshooting

Issue: 502 Bad Gateway errors when proxying

  • Verify upstream service is accessible from the container: test with curl from inside container
  • Check upstream address format: should be host:port for TCP connections
  • Review health check configuration: ensure health_uri endpoint returns 200 OK
  • Check container networking: verify upstream service is reachable on specified port
  • Review Caddy logs: check admin API logs for connection errors

Issue: HTTPS certificate not provisioning automatically

  • Verify DNS is properly configured: domain must resolve to deployment IP
  • Check outbound connectivity: ensure container can reach ACME challenge endpoints
  • Review certificate logs: check /config/ for certificate status and errors
  • Verify port accessibility: ensure ports 80 and 443 are properly exposed and accessible
  • Check domain validation: confirm DNS records or ACME challenge responses are working

Issue: Admin API not responding

  • Verify admin port mapping: ensure port 2019 is accessible from your location
  • Check firewall rules: confirm no firewall is blocking admin API connections
  • Review authentication: if authentication is enabled, verify credentials are correct
  • Monitor admin process: restart container if admin API becomes unresponsive
  • Check logs: review deployment logs for admin API startup errors

Issue: High memory usage

  • Review loaded modules: disable unused modules if custom builds are used
  • Monitor certificate storage: large numbers of certificates can increase memory usage
  • Check logging level: reduce log level from debug to info for production
  • Monitor connection count: high concurrent connections increase memory usage
  • Review configuration: ensure no infinite loops or problematic middleware stacks

Issue: Slow performance or timeouts

  • Check upstream health: verify upstream services are responding quickly
  • Review network latency: measure network latency between Caddy and upstreams
  • Optimize TLS settings: consider adjusting TLS session timeout and cache settings
  • Monitor CPU usage: high CPU usage indicates load or configuration issues
  • Check request logging: review slow requests in access logs to identify bottlenecks

Update Instructions

To update Caddy to the latest version:

  1. Update Dockerfile: Modify your Dockerfile to use the latest Caddy image tag
  2. Test Locally: Build and test the updated image locally with docker-compose
  3. Push Changes: Commit and push the updated Dockerfile to GitHub
  4. Redeploy: In the Klutch.sh dashboard, trigger a manual redeploy or wait for automatic updates
  5. Verify: After deployment, test critical endpoints and monitor logs for any issues

Example Dockerfile update:

# From current version
FROM caddy:latest
# To specific version (recommended for production)
FROM caddy:2.10.2-alpine

Use Cases

  • API Gateway: Central entry point for microservices with load balancing and routing
  • Reverse Proxy: Front-end for legacy applications with automatic HTTPS and modern protocols
  • Static Site Hosting: Serve static sites with compression and caching headers
  • SaaS Custom Domains: Provision SSL certificates on-demand for customer-owned domains
  • Load Balancer: Distribute traffic across multiple backend servers with health checking
  • FastCGI Frontend: Serve PHP applications with Caddy as FastCGI reverse proxy
  • WebSocket Proxy: Forward WebSocket connections for real-time applications
  • gRPC Gateway: Bridge between gRPC services and HTTP clients
  • API Rate Limiting: Protect backend services from overwhelming request traffic
  • Multi-Tenant Hosting: Host multiple applications with automatic certificate management per domain

Additional Resources

Conclusion

Deploying Caddy on Klutch.sh provides a modern, secure, and easy-to-manage HTTP infrastructure with automatic HTTPS, powerful reverse proxy capabilities, and dynamic configuration. By following this guide, you’ve set up a production-ready web server with best-in-class security defaults and flexible configuration options.

Your Caddy deployment is now ready to serve your applications with automatic certificate management, load balancing, and advanced routing capabilities. Whether you’re using it as a static file server, reverse proxy, or API gateway, Caddy’s simple configuration and powerful features make it an ideal choice for modern web infrastructure.

For advanced configurations, monitoring requirements, or troubleshooting assistance, refer to the official Caddy documentation and community resources linked above. Regular monitoring and maintenance of your deployment will ensure optimal performance, security, and reliability for your applications.