Files
coder/docs/tutorials/reverse-proxy-caddy.md
T
Nick Vigilante c84aa564ba docs: normalize code-fence languages for Shiki compatibility (#27161)
Normalizes non-standard code-fence language tags across `docs/**` so a
strict highlighter (Shiki, used by Fumadocs) won't fail the build on an
unrecognized language, and unifies redundant synonym tags onto one
canonical form per language. The current renderer (Speed-Highlight)
detects the language from the code content, not the fence label, so this
drift wasn't visible until now.

## Changes

- `hcl` -> `tf` (199 fences, including indented ones nested in
numbered/bulleted lists). Shiki ships `hcl` and `terraform` as two
distinct grammars (not aliases); every `hcl`-tagged fence in `docs/**`
is actually Terraform resource/data/provider syntax, so the more
specific `terraform` grammar is correct for all of them. `tf` is Shiki's
own alias for that grammar, and it's also what GitHub's own markdown
renderer resolves to the same HCL/Terraform highlighting.
- `pwsh`/`powershell` -> `ps1`. Both `ps` and `ps1` are registered
PowerShell aliases in Shiki, but on GitHub's renderer only `.ps1` is a
registered file extension (`.ps` isn't), so `ps1` renders identically to
`powershell` there today while bare `ps` would silently lose
highlighting.
- `env` -> `dotenv` (a dedicated Shiki grammar for `KEY=VALUE` files)
- `text`/`output`/`none`/`url` -> `txt`. Same built-in plain-text
fallback either way, just shorter.
- `Dockerfile` -> `dockerfile` (lowercase)
- `bash`/`shell` -> `sh` (732 fences). Shiki and GitHub both alias all
three to a single shell grammar; this was already the style guide's
stated preference, just not enforced across the existing corpus until
now.
- `markdown` -> `md` (4 fences). Alias of the same grammar in both Shiki
and GitHub.
- `jsonc` -> `json` (1 fence). The block has no comments or trailing
commas, so it doesn't need the comments-capable grammar.
- `ts` -> `tsx` (2 fences, `docs/about/contributing/frontend.md`).
Verified the actual content tokenizes identically under both grammars,
and a sibling block in the same file already needs `tsx` for real JSX,
so unifying to one tag is safe for this file. Documented a caveat: `tsx`
mis-tokenizes the legacy angle-bracket type-assertion syntax
(`<Type>value`), which is invalid in real `.tsx` files anyway, so use
`value as Type` instead.
- `yml` -> `yaml` (1 fence)
- Updated `docs/.style/style-guide/formatting.md` to document all
canonical tags

`promql` (2 fences) and `caddyfile` (2 fences) are left as-is. Shiki
doesn't bundle a grammar for either, so they need a custom grammar
registration when the site adopts Shiki, rather than degrading to `txt`.
Tracked as follow-up work under DOCS-118 and
[DOCS-544](https://linear.app/codercom/issue/DOCS-544/vendor-a-local-promql-grammar-for-shiki-syntax-highlighting)
(promql).

Does not touch `offlinedocs/`.

Linear:
[DOCS-476](https://linear.app/codercom/issue/DOCS-476/normalize-docs-code-fence-languages-de-risk-shikifumadocs)

<details>
<summary>How the fence tags were verified</summary>

Each tag was tested against a real `shiki@latest` highlighter instance
(`codeToHtml`/`codeToTokens`) and cross-checked against GitHub's
`@wooorm/starry-night` grammar sources (the renderer that actually
displays these `.md` files today, in repo browsing and PR diffs), since
that's what determines whether brevity is safe before Shiki adoption:

```text
FAIL  env        -- Language `env` is not included in this bundle.
FAIL  Dockerfile -- Language `Dockerfile` is not included in this bundle.
FAIL  promql     -- Language `promql` is not included in this bundle.
FAIL  caddyfile  -- Language `caddyfile` is not included in this bundle.
FAIL  pwsh       -- Language `pwsh` is not included in this bundle.
FAIL  output     -- Language `output` is not included in this bundle.
```

`hcl` doesn't error in Shiki, since it's a real grammar, but that's
exactly the trap: it was silently rendering every fence with the generic
HCL grammar instead of the Terraform-specific one. Every `hcl`-tagged
fence in `docs/**` was manually checked against `origin/main` and is
genuinely Terraform content.

For `ts`/`tsx`, tokenizing the actual doc content confirmed identical
output under both grammars; a synthetic test with the legacy
angle-bracket cast syntax confirmed `tsx` degrades on that specific
construct, which the style guide now calls out.

The first normalization pass only matched fence tags at column 0
(`^```tag$`), missing tags indented inside numbered/bulleted lists. A
follow-up pass caught the remaining occurrences at any indentation
level.

</details>


---

*This PR description and the underlying changes were prepared with Coder
Agents assistance.*
2026-07-15 14:07:09 -04:00

8.2 KiB

Caddy

This is an example configuration of how to use Coder with caddy. To use Caddy to generate TLS certificates, you'll need a domain name that resolves to your Caddy server.

Getting started

With docker compose

  1. Install Docker and Docker Compose

  2. Create a compose.yaml file and add the following:

    services:
    coder:
        image: ghcr.io/coder/coder:${CODER_VERSION:-latest}
        environment:
            CODER_PG_CONNECTION_URL: "postgresql://${POSTGRES_USER:-username}:${POSTGRES_PASSWORD:-password}@database/${POSTGRES_DB:-coder}?sslmode=disable"
            CODER_HTTP_ADDRESS: "0.0.0.0:7080"
            # You'll need to set CODER_ACCESS_URL to an IP or domain
            # that workspaces can reach. This cannot be localhost
            # or 127.0.0.1 for non-Docker templates!
            CODER_ACCESS_URL: "${CODER_ACCESS_URL}"
            # Optional) Enable wildcard apps/dashboard port forwarding
            CODER_WILDCARD_ACCESS_URL: "${CODER_WILDCARD_ACCESS_URL}"
            # If the coder user does not have write permissions on
            # the docker socket, you can uncomment the following
            # lines and set the group ID to one that has write
            # permissions on the docker socket.
            #group_add:
            #  - "998" # docker group on host
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        depends_on:
            database:
            condition: service_healthy
    
    database:
        image: "postgres:17"
        ports:
            - "5432:5432"
        environment:
            POSTGRES_USER: ${POSTGRES_USER:-username} # The PostgreSQL user (useful to connect to the database)
            POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} # The PostgreSQL password (useful to connect to the database)
            POSTGRES_DB: ${POSTGRES_DB:-coder} # The PostgreSQL default database (automatically created at first launch)
        volumes:
            - coder_data:/var/lib/postgresql/data # Use "docker volume rm coder_coder_data" to reset Coder
        healthcheck:
            test:
            [
                "CMD-SHELL",
                "pg_isready -U ${POSTGRES_USER:-username} -d ${POSTGRES_DB:-coder}",
            ]
            interval: 5s
            timeout: 5s
            retries: 5
    
    caddy:
        image: caddy:2.6.2
        ports:
            - "80:80"
            - "443:443"
            - "443:443/udp"
        volumes:
            - $PWD/Caddyfile:/etc/caddy/Caddyfile
            - caddy_data:/data
            - caddy_config:/config
    
    volumes:
        coder_data:
        caddy_data:
        caddy_config:
    
  3. Create a Caddyfile and add the following:

    {
        on_demand_tls {
            ask http://example.com
        }
    }
    
    coder.example.com, *.coder.example.com {
      reverse_proxy coder:7080
      tls {
            on_demand
          issuer acme {
             email email@example.com
          }
          }
    }
    

    Here;

    • coder:7080 is the address of the Coder container on the Docker network.
    • coder.example.com is the domain name you're using for Coder.
    • *.coder.example.com is the domain name for wildcard apps, commonly used for dashboard port forwarding. This is optional and can be removed.
    • email@example.com: Email to request certificates from LetsEncrypt/ZeroSSL (does not have to be Coder admin email)
  4. Start Coder. Set CODER_ACCESS_URL and CODER_WILDCARD_ACCESS_URL to the domain you're using in your Caddyfile.

    export CODER_ACCESS_URL=https://coder.example.com
    export CODER_WILDCARD_ACCESS_URL=*.coder.example.com
    docker compose up -d # Run on startup
    

Standalone

  1. If you haven't already, install Coder

  2. Install Caddy Server

  3. Copy our sample Caddyfile and change the following values:

    {
        on_demand_tls {
            ask http://example.com
        }
    }
    
    coder.example.com, *.coder.example.com {
      reverse_proxy coder:7080
    }
    

    If you're installed Caddy as a system package, update the default Caddyfile with vim /etc/caddy/Caddyfile

    • email@example.com: Email to request certificates from LetsEncrypt/ZeroSSL (does not have to be Coder admin email)
    • coder.example.com: Domain name you're using for Coder.
    • *.coder.example.com: Domain name for wildcard apps, commonly used for dashboard port forwarding. This is optional and can be removed.
    • localhost:3000: Address Coder is running on. Modify this if you changed CODER_HTTP_ADDRESS in the Coder configuration.
    • DO NOT CHANGE the ask http://example.com line! Doing so will result in your certs potentially not being generated.
  4. Configure Coder and change the following values:

    • CODER_ACCESS_URL: root domain (e.g. https://coder.example.com)
    • CODER_WILDCARD_ACCESS_URL: wildcard domain (e.g. *.example.com).
  5. Start the Caddy server:

    If you're keeping Caddy running via a system service:

    sudo systemctl restart caddy
    

    Or run a standalone server:

    caddy run
    
  6. Optionally, use ufw or another firewall to disable external traffic outside of Caddy.

    # Check status of UncomplicatedFirewall
    sudo ufw status
    
    # Allow SSH
    sudo ufw allow 22
    
    # Allow HTTP, HTTPS (Caddy)
    sudo ufw allow 80
    sudo ufw allow 443
    
    # Deny direct access to Coder server
    sudo ufw deny 3000
    
    # Enable UncomplicatedFirewall
    sudo ufw enable
    
  7. Navigate to your Coder URL! A TLS certificate should be auto-generated on your first visit.

Generating wildcard certificates

By default, this configuration uses Caddy's on-demand TLS to generate a certificate for each subdomain (e.g. app1.coder.example.com, app2.coder.example.com). When users visit new subdomains, such as accessing ports on a workspace, the request will take an additional 5-30 seconds since a new certificate is being generated.

For production deployments, we recommend configuring Caddy to generate a wildcard certificate, which requires an explicit DNS challenge and additional Caddy modules.

  1. Install a custom Caddy build that includes the caddy-dns module for your DNS provider (e.g. CloudFlare, Route53).

  2. Edit your Caddyfile and add the necessary credentials/API tokens to solve the DNS challenge for wildcard certificates.

    For example, for AWS Route53:

    tls {
    -  on_demand
    -  issuer acme {
    -      email email@example.com
    -  }
    
    +  dns route53 {
    +     max_retries 10
    +     aws_profile "real-profile"
    +     access_key_id "AKI..."
    +     secret_access_key "wJa..."
    +     token "TOKEN..."
    +     region "us-east-1"
    +  }
    }
    

    Configuration reference from caddy-dns/route53.

    And for CloudFlare:

    Generate a token with the following permissions:

    • Zone:Zone:Edit
    tls {
    -  on_demand
    -  issuer acme {
    -      email email@example.com
    -  }
    
    +  dns cloudflare CLOUDFLARE_API_TOKEN
    }
    

    Configuration reference from caddy-dns/cloudflare.