Caddy is a fast, multi-platform web server with automatic HTTPS.

To update or switch versions, run webi caddy@stable (or @v2.7, @beta, etc).

Files

These are the files / directories that are created and/or modified with this install:

~/.config/envman/PATH.env
~/.local/bin/caddy

~/.config/caddy/autosave.json
~/.config/caddy/env
~/.local/share/caddy/certificates/
<PROJECT-DIR>/Caddyfile

Cheat Sheet

Caddy makes it easy to use Let's Encrypt to handle HTTPS (TLS/SSL) and to reverse proxy APIs and WebSockets to other apps - such as those written node, Go, python, ruby, and PHP.

We've split what we find most useful into two categories:

  • Caddy for Developers (Caddyfile)
    • Serve Static Files & Directories
    • Warning-free HTTPS on localhost
    • Redirect (ex: www, https)
    • Logging
    • Compression
    • Reverse Proxy
    • Rewrite Paths
    • CORS
    • Wildcard Domain Example (with DuckDNS)
    • TLS on Private DNS (192.168.x.x)
    • Variables, Placeholders, Macros, Snippets
    • Conditional Logic (if)
    • Comprehensive Caddyfile Example
    • As a macOS service (launchd & launchctl)
    • As a Windows service (starup item)
    • As a Linux service (systemd & systemctl)
  • Caddy for DevOps (JSON Config & API)
    • JSON Config Overview
    • fmt & lint the Caddyfile
    • Caddyfile to JSON Config
    • JSON Config Admin
      • Code Editor autocomplete
      • Backup
      • Restore
      • Manage & Update Config
    • How to use ENVs
    • HTTP Basic Authorization
    • Prevent Dev Sites from Hijacking Production SEO
    • Wildcard & Private IP Certs
      • libdns DNS Providers
      • lego DNS Providers
    • Use HTTP only (no TLS/HTTPS)
    • Use Non-Standard Ports
    • Permission to Use Ports 80 & 443
    • Run with systemd (VM, VPS)
    • Run with openrc (Container, Docker)

Caddy for Developers

mkdir -p ~/.config/caddy/
touch ~/.config/caddy/env

caddy run --config ./Caddyfile --envfile ~/.config/caddy/env
  • run runs in the foreground
  • start starts a background service (daemon)

Warning: ~/.config/caddy/autosave.json is overwritten each time caddy is run with a Caddyfile!

See also:

How to Serve Files & Directories

Using the convenience file-server command:

caddy file-server --browse --listen :4040

Using Caddyfile:

localhost {
    # ...

    handle /* {
        root * ./public/
        file_server {
            browse
        }
    }
}
  • browse enables the built-in directory explorer

See also:

How to serve HTTPS on localhost

Caddy can be used to test with https on localhost.

It's fully automatic and works in your local browser without warnings, assuming you accept the prompt to add the temporary root certificate to your OS keychain.

Caddyfile:

localhost {
    handle /api/* {
        reverse_proxy localhost:3000
    }

    handle /* {
        root * ./public/
        file_server {
            # ...
        }
    }
}
caddy run --config ./Caddyfile

See also:

How to Redirect www and HTTPS

HTTPS redirects are automatic.

www redirects can be done like this:

# redirect www to apex domain
www.example.com {
    redir https://example.com{uri} permanent
}

example.com {
    # ...
}

If you have legacy systems that require the reverse, perhaps to deal with legacy cookie policies, you can do that too.

See also:

How to Log to System Logger

example.com {
    # log to stdout, which is captured by journalctl, etc
    log {
        output stdout
        format console
    }

    # ...
}

See also:

How to Enable Compression

example.com {

    # enable streaming compression
    encode gzip zstd

    handle /* {
        file_server {
            root /srv/example.com/public/

            # enable static compression
            precompressed br,gzip
        }
    }

    # ...
}
  • precompressed will serve index.html.br (or index.html.gz) instead of index.html, when available

See also:

How to Reverse Proxy

  • X-Forwarded-* are set by default:
    • X-Forwarded-For (XFR) is the Request IP
    • X-Forwarded-Proto (XFP) is set to http for plaintext or https for TLS
    • X-Forwarded-Host (XFH) is the original Host header from the client
  • trusted_proxies can be set to allow header pass thru from another proxy
    • private_ranges is a built-in alias for
      192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8 fd00::/8 ::1
  • X-Accel-Redirect can be set to allow static file passthru serving (also known as X-SendFile or X-LIGHTTPD-send-file)
{
    servers {
        trusted_proxies static private_ranges
    }
}

example.com {
    # ...

    handle /api/* {
        reverse_proxy localhost:3000 {

            @accel header X-Accel-Redirect *
            handle_response @sendfile {
                root * /srv/assets
                rewrite * {http.response.header.X-Accel-Redirect}
                file_server
            }

        }
    }
}

See also:

How to Rewrite Paths

Rather than reverse_proxy, this could just as well be handled by file_server.

handle_path eats the path, whereas handle matches without consuming.

example.com {
    # ...

    # {host}/api/oldpath/* => http://localhost:3000/api/newpath/*
    handle_path /api/oldpath/* {
        rewrite * /api/newpath{path}
        reverse_proxy localhost:3000
    }
}

How to handle CORS Preflight + Request

CORS comes in 3 basic varieties:

  • Simple Requests
  • Preflight Requests
  • Credentialed Requests
    (by Origin and/or Authentication)

"Simple Requests"

Simple Requests are those that match:

  • GET, HEAD, or POST
  • Accept, Range and traditional Content-Types, which are: \
    • application/x-www-form-urlencoded, multipart/form-data, text/plain

Typical use cases include

  • Static Files
  • Public Assets
  • Contact Request Forms
# CORS "Simple Request"
# (for Static Files & Form Posts)
(cors-simple) {
    @match-cors-request-simple {
        not header Origin "{http.request.scheme}://{http.request.host}"
        header Origin "{http.request.header.origin}"
        method GET HEAD POST
    }

    handle @match-cors-request-simple {
        header {
            Access-Control-Allow-Origin "*"
            Access-Control-Expose-Headers *
            defer
        }
    }
}

example.com {
    # ex: POST to unauthenticated forms
    handle /api/public/* {
        import cors-simple
        reverse_proxy localhost:3000
    }

    # ex: GET, HEAD static assets
    handle /* {
        import cors-simple
        file_server {
            /srv/public/
        }
    }
}

API Requests

Typical use cases for this are:

  • Fully Public APIs
  • APIs Authenticated by Token or username
    • Authentication: Basic <base64(api:token)>
    • Authentication: Bearer <token>
  • POST forms with non-traditional Content-Typesusing
    • application/json
    • application/graphql+json
    • etc

Important Notes:

  • * wildcards may NOT be used for authenticated API requests
  • Access-Control-Expose-Headers exposes to JavaScript, not just the browser
# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
(cors-api) {
    @match-cors-api-preflight {
        not header Origin "{http.request.scheme}://{http.request.host}"
        header Origin "{http.request.header.origin}"
        method OPTIONS
    }
    handle @match-cors-api-preflight {
        header {
            Access-Control-Allow-Origin "{http.request.header.origin}"
            Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
            Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
            Access-Control-Allow-Credentials "true"
            Access-Control-Max-Age "3600"
            defer
        }
        respond "" 204
    }

    @match-cors-api-request {
        not header Origin "{http.request.scheme}://{http.request.host}"
        header Origin "{http.request.header.origin}"
        not method OPTIONS
    }
    handle @match-cors-api-request {
        header {
            Access-Control-Allow-Origin "{http.request.header.origin}"
            Access-Control-Allow-Credentials "true"
            Access-Control-Max-Age "3600"
            defer
        }
    }
}

api.example.com {
    handle /api/* {
        import cors-api

        reverse_proxy localhost:3000
    }

    # ...
}

Restricted by Origin

Typical use cases for this are:

  • Allow access to partners or sister domains

Important Notes:

  • * wildcards can be used for unauthenticated requests
(cors-origin) {
    @match-cors-preflight-{args.0} {
        header Origin "{args.0}"
        method OPTIONS
    }
    handle @match-cors-preflight-{args.0} {
        header {
            Access-Control-Allow-Origin "{args.0}"
            Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
            Access-Control-Allow-Headers *
            Access-Control-Max-Age "3600"
            defer
        }
        respond "" 204
    }

    @match-cors-request-{args.0} {
        header Origin "{args.0}"
        not method OPTIONS
    }
    handle @match-cors-request-{args.0} {
        header {
            Access-Control-Allow-Origin "{http.request.header.origin}"
            Access-Control-Expose-Headers *
            defer
        }
    }
}

partners.example.com {
    import cors-origin https://member.example.com
    import cors-origin https://whatever.com

    file_server {
        root /srv/public/
    }
}

See also:

How to Wildcards & Private DNS

DNS Providers are required for

  • wildcards (*.example.com)
  • Private IPs / Private DNS (192.168.x.x)
  • Running Caddy directly on non-standard ports (3000, 8443)

Example with DuckDNS:

  1. Put the credentials in your dotenv (the name is arbitrary):
    caddy.env:

    MY_DUCKDNS_TOKEN=xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx
    
  2. Add the tls directive in the format of dns <provider> [documented params]:

    # a wildcard domain
    *.example.duckdns.org {
        tls {
            dns duckdns {env.MY_DUCKDNS_TOKEN}
        }
    
        # ...
    }
    
    # an intranet domain (on a private network, such as 192.168.x.x)
    local.example.duckdns.org {
        tls {
            dns duckdns {env.MY_DUCKDNS_TOKEN}
        }
    
        # ...
    }
    

For more information see How to use libdns providers below in the DevOps section.

See also:

How to use Caddyfile Meta Variables

  • "Placeholders" and "Shorthand" are the variables that look like:

    • {http.request.uri}
    • {request.uri}
    • {uri}
    • {path}
    • {host}
    • {http.response.header}
    • {args[0]}
  • Environment Variables come in Parse-time and Runtime variety:

    • {$DUCKDNS_API_TOKEN}, {$BASIC_AUTH_DIGEST} (parse-time)
    • {env.DUCKDNS_API_TOKEN}, {env.BASIC_AUTH_DIGEST} (runtime)
  • "Named Matchers" can substitute paths in most places:

    # match this secret path to find hidden treasures
    handle_path /easter-eggs/* {
        root * /srv/my-eggs
        file_server
    }
    
    # match this secret header to find hidden treasures
    @my-easter-egg {
        header X-Magic-Word "Easter-Egg"
    }
    handle @my-easter-egg {
        root * /srv/my-eggs
        file_server
    }
    
  • "Imports" and "Snippets" are the macro templates that look like:

    # (template-name)
    (my-no-plaintext) {
    
      # @matcher-name
      @my-plaintext {
          protocol http
      }
    
      # use of matcher
      redir @my-plaintext https://{host}{uri}
    }
    
    example.com {
        # import the snippet
        import my-no-plaintext
    }
    

See also:

Placeholder Hierarchy

Path                                        # Shorthand
├── args[]                                  # in snippets (template functions)
├── env.*
├── http
│   ├── error.+                             # {err.+}
│   ├── matchers
│   │   ├── file.+                          # {file_match.+}
│   │   ├── header_regexp.?
│   │   ├── path_regexp.?
│   │   └── vars_regexp.?
│   ├── regexp.*[]                          # {re.*.1}
│   ├── request
│   │   ├── cookie.*                        # {cookie.*}
│   │   ├── header.*                        # {header.*}
│   │   ├── host
│   │   │   └── labels[]                    # {labels.0} (as rDNS: com.example)
│   │   ├── hostport                        # {hostport}
│   │   ├── method                          # {method}
│   │   ├── port                            # {port}
│   │   ├── remote                          # {remote}
│   │   │   ├── host                        # {remote_host}
│   │   │   └── port                        # {remote_port}
│   │   ├── scheme                          # {scheme}
│   │   ├── tls
│   │   │   ├── cipher_suite                # {tls_cipher}
│   │   │   ├── client
│   │   │   │   ├── certificate_der_base64  # {tls_client_certificate_der_base64}
│   │   │   │   ├── certificate_pem         # {tls_client_certificate_pem}
│   │   │   │   ├── fingerprint             # {tls_client_fingerprint}
│   │   │   │   ├── issuer                  # {tls_client_issuer}
│   │   │   │   ├── serial                  # {tls_client_serial}
│   │   │   │   └── subject                 # {tls_client_subject}
│   │   │   └── version                     # {tls_version}
│   │   ├── uri                             # {uri}
│   │   │   ├── path.+                      # {path.+}
│   │   │   │   ├── dir                     # {dir}
│   │   │   │   └── file.+                  # {file}
│   │   │   │       ├── base                # {file.base}
│   │   │   │       └── ext                 # {file.ext}
│   │   │   └── query.*                     # {query.*}
│   ├── reverse_proxy.+                     # {rp.+}
│   │   └── upstream                        # {upstream}
│   │   │   └── hostport                    # {upstream_hostport}
│   └── vars.*                              # {vars.*}
│       └── client_ip                       # {client_ip}
├── system
│   ├── hostname
│   ├── slash
│   ├── os
│   ├── arch
│   └── wd
└── time
    └── now
        ├── common_log
        ├── http
        ├── unix
        ├── unix_ms
        └── year
  • [] signifies a list accessible by index, such as labels.0
  • .+ signifies more pre-defined keys, see docs (linked below) for specifics
  • .* signifies that the keys are arbitrary per the config or the request
  • .? signifies that we didn't understand the documentation

See also:

How to Conditional ENVs

There is no if in Caddy, but a matcher with "CEL" does the same thing.

Ex: I only want to enforce HTTP Basic Auth if it's enabled:

localhost {
    @match-enforce-auth `"{$HTTP_BASIC_AUTH_ENABLED}".size() > 0`
    basicauth @match-enforce-auth {
        {$HTTP_BASIC_AUTH_USERNAME} {$HTTP_BASIC_AUTH_PASSWORD_DIGEST}
    }

    # ...
}

You can do slightly more complex expressions on the variety of variables (placeholders), but you'd have to look up the CEL docs.

However, you can only do these expressions in things that have a matcher.

See also:

Putting it All Together

Here's what a fairly basic, but comprehensive and complete Caddyfile looks like:

Caddyfile:

# redirect www to bare domain
www.example.com {
    redir https://example.com{uri} permanent
}

example.com {
    ###########
    # Logging #
    ###########

    # log to stdout, which is captured by journalctl
    log {
        output stdout
        format console
    }

    ###############
    # Compression #
    ###############

    # turn on standard streaming compression
    encode gzip zstd

    ####################
    # Reverse Proxying #
    ####################

    # reverse proxy /api to :3000
    handle /api/* {
        reverse_proxy localhost:3000
    }

    # reverse proxy some "well known" APIs
    handle /.well-known/openid-configuration {
        reverse_proxy localhost:3000
    }
    handle /.well-known/jwks.json {
        reverse_proxy  localhost:3000
    }

    ##################
    # Path Rewriting #
    ##################

    # reverse proxy and rewrite path /api/oldpath/* => /api/newpath/*
    handle_path /api/oldpath/* {
        rewrite * /api/newpath{path}
        reverse_proxy localhost:3000
    }

    ###############
    # File Server #
    ###############

    # serve static files
    handle /* {
        root * /srv/example.com/public/
        file_server {
            precompressed br,gzip
        }
    }
}

How to run Caddy as a macOS Service

To avoid the nitty-gritty details of launchd plist files, you can use serviceman to template out the plist file for you:

  1. Install serviceman

    webi serviceman
    
  2. Use Serviceman to create a launchd plist file

    my_username="$( id -u -n )"
    
    serviceman add --user --name caddy -- \
        caddy run --config ./Caddyfile --envfile ~/.config/caddy/env
    

    (this will create ~/Library/LaunchAgents/caddy.plist)

  3. Manage the service with launchctl

    launchctl unload -w ~/Library/LaunchAgents/caddy.plist
    launchctl load -w ~/Library/LaunchAgents/caddy.plist
    

This process creates a User-Level service in ~/Library/LaunchAgents. To create a System-Level service in /Library/LaunchDaemons/ instead:

sudo serviceman add --system --name caddy -- \
   caddy run --config ./Caddyfile --envfile ~/.config/caddy/env

How to run Caddy as a Windows Service

  1. Set a Windows Firewall Rule to allow traffic to Caddy.
    You can do this with PowerShell by changing YOUR_USER in the script below and running it in cmd.exe as Administrator:
    powershell.exe -WindowStyle Hidden -Command $r = Get-NetFirewallRule -DisplayName 'Caddy Web Server' 2> $null; if ($r) {write-host 'found rule';} else {New-NetFirewallRule -DisplayName 'Caddy Web Server' -Direction Inbound $HOME\\.local\\bin\\caddy.exe -Action Allow}
    
  2. Install serviceman
    webi serviceman
    
  3. Create a Startup Registry Entry with Serviceman.
    serviceman.exe add --name caddy -- \
        caddy run --config ./Caddyfile --envfile ~/.config/caddy/env
    
  4. You can manage the service directly with Serviceman. For example:
    serviceman stop caddy
    serviceman start caddy
    

This will run caddy as a Startup Item. To run as a true system service see https://caddyserver.com/docs/running#windows-service.

How to run Caddy as a Linux service

This will create a System Service using Caddyfile.
See the notes below to run as a User Service or use the JSON Config.

  1. If you haven't already, create a non-root user. You can use ssh-adduser for this:

    curl -fsS https://webi.sh/ssh-adduser | sh
    

    (this will follow the common industry convention of naming the user app)

  2. Give caddy port-binding privileges. You can use setcap-netbind for this:

    webi setcap-netbind
    setcap-netbind caddy
    

    (or you can use setcap directly)

    my_caddy_path="$( command -v caddy )"
    my_caddy_absolute="$( readlink -f "${my_caddy_path}" )"
    
    sudo setcap cap_net_bind_service=+ep "${my_caddy_absolute}"
    
  3. Install serviceman to template a systemd service unit

    webi serviceman
    
  4. Use Serviceman to create a systemd config file.

    my_username="$( id -u -n )"
    sudo env PATH="$PATH" \
        serviceman add --system --username "${my_username}" --name caddy -- \
            caddy run --config ./Caddyfile --envfile ~/.config/caddy/env
    

    (this will create /etc/systemd/system/caddy.service)

  5. Manage the service with systemctl and journalctl:

    sudo systemctl restart caddy
    sudo journalctl -xefu caddy
    

To create a User Service instead:

  • don't use sudo, but do use --user when running serviceman:
    serviceman add --user --name caddy -- \
       caddy run --config ./Caddyfile --envfile ~/.config/caddy/env
    
    (this will create ~/.config/systemd/user/)
  • user the --user flag to manage services and logs:
    systemctl --user enable caddy
    systemctl --user restart caddy
    journalctl --user -xef -u caddy
    

To use the JSON Config:

  • use --resume rather than --config ./Caddyfile
    caddy run --resume --envfile ~/.config/caddy/env
    

Caddy for DevOps

touch ./config.env

caddy run --resume --envfile ./caddy.env
# (resumes from ~/.config/caddy/autosave.json)
  • --resume overrides --config
  • the save file is hard coded to ~/.config/caddy/autosave.json
  • only a single API-enabled instance can resumed at a time
    (the workaround is to not use resume, but replace the config file and restart)

To create and load the initial JSON Config, see the Caddyfile to JSON section below.

Where to learn about the JSON config

The best way to learn is to create a Caddyfile and

  • run caddy adapt ./Caddyfile
  • or see ~/.config/caddy/autosave.json after any caddy run

Then it's also helpful to read the general overview:

The key things you'll need to learn:

  • which modules can be nested within others (handle, routes)
  • which keys are arbitrary (srv0) and which are pre-defined (group, match)
  • which structures are core to caddy vs which are specific to a module
  • which structures you can eliminate or deneste (Caddyfile conversion is messy)

How to fmt & lint Caddyfiles

Both caddy fmt and caddy adapt can be used to lint.

caddy fmt --overwrite ./Caddyfile
caddy adapt --config ./Caddyfile

How to convert Caddyfile to JSON

Shown with jq (yq also works well) because it makes the output readable.

caddy adapt --config ./Caddyfile |
    jq > ./caddy.json

You can then load the JSON Config to a live server:

my_config="./caddy.json"

curl -X POST "http://localhost:2019/load" \
    -H "Content-Type: application/json" \
    -d @"${my_config}"

This will immediately overwrite ~/.config/caddy/autosave.json.

Code Editor support for Caddy's JSON API

VS Code and Vim / NeoVim are supported.

See https://github.com/abiosoft/caddy-json-schema.

How to Backup the JSON config

my_date="$( date '+%F_%H.%M.%S' )"

curl "http://localhost:2019/config" -o ./caddy."${my_date}".json

Or copy from ~/.config/caddy/autosave.json

Warning: ~/.config/caddy/autosave.json is overwritten each time caddy is run with a Caddyfile!

How to Restore via the API

This will effectively gracefully restart caddy.

my_config="./caddy.json"

curl -X POST "http://localhost:2019/load" \
    -H "Content-Type: application/json" \
    -d @"${my_config}"

How to Update via the API

It will probably be best (and simplest) to write a new config file programmatically and then upload it whole.

Currently, there is no API to provide idempotent updates ("upsert" or "set"), and many changes that are logically a single unit (such as adding a new site), require updates among a few different structures, such as:

  • apps.https.servers["srv0"].routes[]
  • apps.tls.automation.policies[].subjects
  • apps.tls.certificates.automate[]

However, very, very large config files may benefit from the extra work required to do smaller updates rather than reload the whole config.

Here are some important notes:

  • PATCH will replace, not modify / merge as you would traditionally expect
  • PUT will NOT replace, but rather insert into a position
  • A literal ... in a path, such as POST /config/my-config/... will append
  • @id may exist as a special key on any object, but must globally unique
  • GET /id/my_object directly accesses the object with "@id": "my_object"

See also:

How to use ENVs

Caddy's --envfile ./caddy.env parser supports dotenvs in this format:

caddy.env:

FOO="one"
BAR='two'
BAZ=three

They are accessed like {env.FOO} whether in Caddyfile or caddy.json:

example.com {
    file_server * {
        root {env.WWW_ROOT}
    }
}
{
  "apps": {
    "http": {
      "servers": {
        "my-srv0": {
          "listen": [":443"],
          "routes": [
            {
              "match": [{ "host": ["example.com"] }],
              "handle": [
                {
                  "handler": "file_server",
                  "root": "{env.WWW_ROOT}"
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    }
  }
}

Conventionally, the dotenv file should be placed in one of the following locations:

  • ~/.config/caddy/env
  • <PROJECT-DIR>/caddy.env
  • <PROJECT-DIR>/.env

It does NOT follow the dotenv spec, in particular:

  • does not support export prefix
  • does not interpolate variables in double-quoted " strings

Consider dotenv for better compatibility.

See also:

How to add HTTP Basic Authorization

  1. Digest a password with random salt
    cat ./password.txt |
        caddy hash-password
    
    $2a$14$QYYeOtsv0RJixoNZ5frOwuPDiUWl8QBkeMEUBbmnkOHuErlVklzTm
    
  2. Put the digest into an env file with single quotes (to escape the $s)
    caddy.env:
    BASIC_AUTH_USERNAME=my-username
    BASIC_AUTH_DIGEST='$2a$14$QYYeOtsv0RJixoNZ5frOwuPDiUWl8QBkeMEUBbmnkOHuErlVklzTm'
    
  3. Reference {env.BASIC_AUTH_DIGEST} in the Caddyfile or caddy.json
    example.com {
        handle /* {
            basicauth {
                {env.BASIC_AUTH_USERNAME} {env.BASIC_AUTH_DIGEST}
            }
            root * /home/app/srv/example.com/public/
            file_server
        }
    }
    

How to Prevent Dev Sites from Hijacking Prod

Not caddy specific, but...

By default, dev sites on dev domains will hijack the SEO and damage the reputation of your production domains.

Allowing non-production sites to be indexed may even cause browsers to issue Suspicious Site Blocking on your primary domain.

To prevent search engine and browser confusion

  • delist your demo, staging, beta, & development from indexing
  • promote your primary domain as canonical
  • DO NOT prevent crawling via robots.txt
    (counter-intuitive, but pages must be crawled for links to NOT be indexed)
  • all domains using public TLS certs will be indexed by default
    (they are all linked to and crawled from various Certificate Transparency reports)
  • follow these guidelines even if the dev sites use HTTP Basic Auth
dev.example.com {
    header {
        Link "<https://production.example.com{http.request.orig_uri}>; rel=\"canonical\""
        X-Robots-Tag noindex
    }

    # ...
}

See also:

How to DNS Providers for Wildcard Certs

You will need to use xcaddy to build caddy with DNS module support.

DNS Providers come in two flavors:

  1. libdns instances (newer, fewer providers)
  2. lego singletons (deprecated)

You can only have ONE lego instance per process, whereas libdns can support multiple providers across multiple domains.

How to use libdns providers

Look for your DNS provider in the official lists:

For this example we'll use DuckDNS (https://github.com/caddy-dns/duckdns).

  1. Put the credentials in your dotenv (the name is arbitrary):
    caddy.env:

    MY_DUCKDNS_TOKEN=xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx
    
  2. Add the tls directive in the format of dns <provider> [documented params]:

    example.duckdns.org {
        tls {
            dns duckdns {env.MY_DUCKDNS_TOKEN}
        }
    
        # ...
    }
    
    *.example.duckdns.org {
        tls {
            dns duckdns {env.MY_DUCKDNS_TOKEN}
        }
    
        # ...
    }
    

When using the JSON config the token key is instead named api_token!

You can see this by running caddy adapt ./Caddyfile on the example above.

How to use lego providers

If you can't find your DNS provider in the libdns list, check to see if it's available in the lego list:

For this example we'll use DNSimple (https://go-acme.github.io/lego/dns/dnsimple/).

  1. Put the credentials in your dotenv (which MUST match the docs):
    caddy.env:

    DNSIMPLE_OAUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
  2. Add the tls directive in the format of dns lego_deprecated <provider>:

    example.com {
        tls {
            dns lego_deprecated dnsimple
        }
    
        # ...
    }
    
    *.example.com {
        tls {
            dns lego_deprecated dnsimple
        }
    
        # ...
    }
    

How to run caddy on HTTP only (no TLS)

You must use the http:// prefix AND specify a port number:

http://localhost:3080 {
    #...
}

How to run caddy on non-standard ports

http://example.com:3080, https://example.com:3443 {
    #...
}

You cannot get TLS certificates (HTTPS) on non-standard ports unless:

  • you use a DNS Provider (see the Private IP / Wildcard section)
  • or you have some sort of special proxy in place

How to allow caddy to bind on 80 & 443

On macOS all programs are allowed to use privileged ports by default.

On Linux there are several ways to add network capabilities for privileged ports:

  1. Use setcap-netbind

    webi setcap-netbind
    setcap-netbind caddy
    
  2. Use setcap directly

    my_caddy_path="$( command -v caddy )"
    my_caddy_absolute="$( readlink -f "${my_caddy_path}" )"
    
    sudo setcap cap_net_bind_service=+ep "${my_caddy_absolute}"
    
  3. Use setcap through systemd
    (see systemd instructions below)

  4. Run as root (such as on single-user containers)

  5. Run as app, but port-forward through the container
    (you figure it out)

setcap-netbind must be run each time caddy is updated.

How to run with systemd

See also: https://caddyserver.com/docs/running

systemd is the init system used on most VPS-friendly Linuxes.

  1. Install serviceman to create the systemd config
    webi serviceman
    
  2. Generate the service file: \
    • JSON Config
      my_app_user="$( id -u -n )"
      sudo env PATH="${PATH}" \
          serviceman add --system --cap-net-bind \
              --username "${my_app_user}" --name caddy -- \
              caddy run --resume --envfile ./caddy.env
      
    • Caddyfile
      my_app_user="$( id -u -n )"
      sudo env PATH="${PATH}" \
          serviceman add --system --cap-net-bind \
              --username "${my_app_user}" --name caddy -- \
              caddy run --config ./Caddyfile --envfile ./caddy.env
      
  3. Reload systemd config files, the logging service (it may not be started on a new VPS), and caddy
    sudo systemctl daemon-reload
    sudo systemctl restart systemd-journald
    sudo systemctl restart caddy
    

If you prefer to create the service file manually, it should look something like this:

/etc/systemd/system/caddy.service:

# Generated for serviceman. Edit as you wish, but leave this line.
# Pre-req
# sudo mkdir -p ~/srv/ /var/log/caddy/
# sudo chown -R app:app /var/log/caddy
# Post-install
# sudo journalctl -xefu caddy

[Unit]
Description=caddy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
# Restart on crash (bad signal), but not on 'clean' failure (error exit code)
# Allow up to 3 restarts within 10 seconds
# (it's unlikely that a user or properly-running script will do this)
Restart=always
StartLimitInterval=10
StartLimitBurst=3

# User and group the process will run as
User=app
Group=app

WorkingDirectory=/home/app/srv/
ExecStart=/home/app/.local/bin/caddy run --resume --envfile /home/app/srv/caddy.env
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full

# These directives allow the service to gain root-like networking privileges.
# Note that you may have to add capabilities required by any plugins in use.
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true

# Caveat: Some features may need additional capabilities.
# For example an "upload" may need CAP_LEASE
; CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_LEASE
; AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_LEASE
; NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

See also:

How to run with openrc

See also: https://caddyserver.com/docs/running

openrc is the init system on Alpine and other Docker and container-friendly Linuxes.

/etc/init.d/caddy:

#!/sbin/openrc-run
supervisor=supervise-daemon

name="Caddy web server"
description="Fast, multi-platform web server with automatic HTTPS"
description_checkconfig="Check configuration"
description_reload="Reload configuration without downtime"

# for JSON Config
: ${caddy_opts:="--envfile /root/.config/caddy/env --resume"}

# for Caddyfile
#: ${caddy_opts:="--envfile /root/.config/caddy/env --config /root/srv/caddy/Caddyfile"}

command=/root/bin/caddy
command_args="run $caddy_opts"
command_user=root:root
extra_commands="checkconfig"
extra_started_commands="reload"
output_log=/var/log/caddy.log
error_log=/var/log/caddy.err

depend() {
    need net localmount
    after firewall
}

checkconfig() {
    ebegin "Checking configuration for $name"
    su ${command_user%:*} -s /bin/sh -c "$command validate $caddy_opts"
    eend $?
}

reload() {
    ebegin "Reloading $name"
    su ${command_user%:*} -s /bin/sh -c "$command reload $caddy_opts"
    eend $?
}

stop_pre() {
    if [ "$RC_CMD" = restart ]; then
        checkconfig || return $?
    fi
}

Contribute

Report an Issue Submit Installer Star on GitHub