Caddy inside Dockerfile

The problem I’m having:

I am building a custom docker image for my application. It only runs with webserver configured inside the image. Initially I had configured this with nginx. But now I want to switch to caddy. Please help me with this.

Caddy version:

2.6.4

How I installed and ran Caddy:

I want to integrate caddy inside my docker image

a. System environment

CentOS 7

d. My complete Caddy config:

my-domain  {
  	reverse_proxy localhost:4202  
   }

My Dockerfile with nginx config

...
...

# Build React APP
RUN yarn run build

# production environment
FROM nginx:1.23.3-alpine
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]

That’s a vague question. It’s lacking detail, so I’m not sure what you mean by that.

But we have our recommendations for running Caddy with Docker Compose here in the docs: Keep Caddy Running — Caddy Documentation

The resource you shared is good for a docker compose setup. But i want to use caddy inside my dockerfile.

Please explain in more detail exactly what you’re trying to do, because it’s not clear what your goal is, I need to make assumptions.

It sounds like you probably want a multi-stage build that ends in COPYing your static site build into the caddy image. See the docs on Docker Hub which explains how this would look.

Yes…I am trying to do a multi-stage build. This is my Dockerfile which I have modified at the end according to caddy.

FROM node:18-alpine3.15 as builder

# Create work directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY ./web/package.json /usr/src/app/package.json

RUN apk upgrade --update-cache --available
RUN npm install yarn --legacy-peer-deps
RUN yarn install --silent
COPY ./web /usr/src/app

RUN yarn run build

# ====== RUN CADDY =======
FROM caddy:2.6.4-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /usr/src/app/build /srv

Yeah, that looks fine.

Is there still a problem?

Haven’t tried yet. Will check and let you know.

the local container works on port 4202. My container is running, but I am not able to access the webpage. Is my caddyfile correct? Do I need to change something?

Or do I need to change 0.0.0.0 to 127.0.0.1

My Caddyfile

my_domain{
        header {
                Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"
                X-Content-Type-Options "nosniff"
                X-Frame-Options "DENY"
                Referrer-Policy "no-referrer-when-downgrade"
        }
        reverse_proxy 0.0.0.0:4202
        encode gzip
}

Logs

Found this line on my logs

{"level":"info","ts":1680344047.807029,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1680344047.810623,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1680344047.8112025,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000226d90"}
{"level":"info","ts":1680344047.8122997,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1680344047.8138502,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}
{"level":"info","ts":1680344047.8139448,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"info","ts":1680344047.8144386,"msg":"failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Receive-Buffer-Size for details."}

If you’re trying to serve a static build, you’re looking to use the root and file_server directives, not reverse_proxy.

The only thing running in the Caddy container is Caddy. There’s nothing to proxy to.

Oh okay… So is this fine? I tried this, but it was not working.

        header {
                Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"
                X-Content-Type-Options "nosniff"
                X-Frame-Options "DENY"
                Referrer-Policy "no-referrer-when-downgrade"
        }
        encode gzip
		file_server
        root public_html

What do you mean by “not working”? What’s in your logs? What behaviour are you seeing? Please be specific.

Since the default working directory is /srv, this would make the root /srv/public_html. Is that correct?

Show an example request with curl -v. Enable the debug global option and look at the container logs, what do you see?

I enabled debug option. The docker container is running. Here are the logs

{"level":"info","ts":1680495616.4855783,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}

{"level":"info","ts":1680495616.4956822,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}

{"level":"info","ts":1680495616.4980655,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}

{"level":"info","ts":1680495616.4981515,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}

{"level":"info","ts":1680495616.4982371,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000455810"}

{"level":"info","ts":1680495616.4998982,"logger":"tls","msg":"cleaning storage unit","description":"FileStorage:/data/caddy"}

{"level":"info","ts":1680495616.5002074,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}

{"level":"info","ts":1680495616.5003085,"logger":"tls","msg":"finished cleaning storage units"}

{"level":"info","ts":1680495616.5013182,"msg":"failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 2048 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Receive-Buffer-Size for details."}

{"level":"info","ts":1680495616.5016832,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}

{"level":"info","ts":1680495616.5033932,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}

{"level":"info","ts":1680495616.5035007,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["<my domain>"]}

{"level":"info","ts":1680495616.505373,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}

{"level":"info","ts":1680495616.5054524,"msg":"serving initial configuration"}

{"level":"info","ts":1680495616.508898,"logger":"tls.obtain","msg":"acquiring lock","identifier":"<my domain>"}

{"level":"info","ts":1680495616.5172265,"logger":"tls.obtain","msg":"lock acquired","identifier":"<my domain>"}

{"level":"info","ts":1680495616.5177596,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"<my domain>"}

{"level":"info","ts":1680495617.267371,"logger":"http","msg":"waiting on internal rate limiter","identifiers":["<my domain>"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":"kapil.v.k@firstqa.com"}

{"level":"info","ts":1680495617.2674541,"logger":"http","msg":"done waiting on internal rate limiter","identifiers":["<my domain>"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":"kapil.v.k@firstqa.com"}

{"level":"info","ts":1680495617.6598408,"logger":"http.acme_client","msg":"trying to solve challenge","identifier":"<my domain>","challenge_type":"http-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}

{"level":"error","ts":1680495618.5626292,"logger":"http.acme_client","msg":"challenge failed","identifier":"<my domain>","challenge_type":"http-01","problem":

{"type":"urn:ietf:params:acme:error:connection","title":"","detail":"my IP: Fetching http://<my domain>/.well-known/acme-challenge/q4Ha3lQ6kLRDKBeMgsfy7QwaX4v8_V8lRTTZ-sCouA4: Error getting validation data","instance":"","subproblems":[]}}

{"level":"error","ts":1680495618.5627105,"logger":"http.acme_client","msg":"validating authorization","identifier":"<my domain>","problem":

{"type":"urn:ietf:params:acme:error:connection","title":"","detail":"my IP: Fetching http://<my domain>/.well-known/acme-challenge/q4Ha3lQ6kLRDKBeMgsfy7QwaX4v8_V8lRTTZ-sCouA4: Error getting validation data","instance":"","subproblems":[]},"order":"https://acme-v02.api.letsencrypt.org/acme/order/1042236957/173881597387","attempt":1,"max_attempts":3}

curl -v https://:4202

* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* NSS error -12263 (SSL_ERROR_RX_RECORD_TOO_LONG)
* SSL received a record that exceeded the maximum permissible length.
* Closing connection 0
curl: (35) SSL received a record that exceeded the maximum permissible length.

Caddyfile

{
		debug
}

<my domain> {
        header {
                Strict-Transport-Security "max-age=31536000; includeSubdomains; preload;"
				X-Content-Type-Options "no sniff"
				X-Frame-Options "DENY"
				Referrer-Policy "no-referrer-when-downgrade"
        }
        encode gzip
        root * /srv/public_html
        file_server
}

Isbind 127.0.0.1 needed in caddyfile?

The problem is that Caddy wasn’t able to issue a cert for your domain. You need to make Caddy publicly accessible on ports 80 and 443 so that the ACME challenge can successfully complete to get a certificate. Your curl command used a different port, which doesn’t make sense.

Oh. So in that case I would need to make changes in my docker image…Please correct me if I am wrong.

Just getting confused…don’t know how to solve this. I am absolutely new to web servers and caddy.

The image seems fine to me, you just need to make sure you bind ports 80 and 443 to the host (when you run the container), and make sure port forwarding and your firewall if any are configured correctly for traffic on those ports to reach your server.

Getting this error in docker logs

{“level”:“error”,“ts”:1680525148.822779,“logger”:“tls.obtain”,“msg”:“could not get certificate from issuer”,“identifier”:“domain”,“issuer”:“acme-v02.api.letsencrypt.org-directory”,“error”:“HTTP 429 urn:ietf:params:acme:error:rateLimited - Error creating new order :: too many certificates (5) already issued for this exact set of domains in the last 168 hours: my-domain, retry after 2023-04-04T21:03:13Z: see Duplicate Certificate Limit - Let's Encrypt”}


Reviewing previous QA I added this in my Dockerfile for caddy config

FROM caddy:2.6.4-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /usr/src/app/build /srv

**COPY caddy_data /data**
**COPY caddy_config /config**

Should I use this inside dockerfile or docker compose?

You shouldn’t copy /data, you should persist it with a volume.

Please review the docs on Docker Hub and the Docker Compose link I sent earlier. Those cover important details to get right.

At this point you’ve hit rate limits with Let’s Encrypt, so you won’t be able to get another new cert with them. But good news is Caddy should fall back to using ZeroSSL instead which doesn’t have rate limits so it should be fine anyways.

Logs don’t have an error now. But still the site is not accessible. shows a blank page

{"level":"info","ts":1680582361.189641,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
{"level":"info","ts":1680582361.1959133,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1680582361.1984377,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00059f340"}
{"level":"info","ts":1680582361.1984413,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1680582361.1985512,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1680582361.2008052,"logger":"tls","msg":"cleaning storage unit","description":"FileStorage:/data/caddy"}
{"level":"info","ts":1680582361.203376,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"info","ts":1680582361.2040188,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"debug","ts":1680582361.2044826,"logger":"http","msg":"starting server loop","address":"[::]:443","tls":true,"http3":true}
{"level":"info","ts":1680582361.2045069,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
{"level":"debug","ts":1680582361.204625,"logger":"http","msg":"starting server loop","address":"[::]:80","tls":false,"http3":false}
{"level":"info","ts":1680582361.204633,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}
{"level":"info","ts":1680582361.2046378,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["my-domain"]}
{"level":"debug","ts":1680582361.2055016,"logger":"tls","msg":"loading managed certificate","domain":"my-domain","expiration":1688342400,"issuer_key":"acme.zerossl.com-v2-DV90","storage":"FileStorage:/data/caddy"}
{"level":"debug","ts":1680582361.2059436,"logger":"tls.cache","msg":"added certificate to cache","subjects":["my-domain"],"expiration":1688342400,"managed":true,"issuer_key":"acme.zerossl.com-v2-DV90","hash":"a6ecea2674871d6df09241a2657aa08f74e32566427cca6eaffd268bb6f5e198","cache_size":1,"cache_capacity":10000}
{"level":"debug","ts":1680582361.205982,"logger":"events","msg":"event","name":"cached_managed_cert","id":"255bfc43-0bfd-43d7-b15e-6133ac1b5ba6","origin":"tls","data":{"sans":["my-domain"]}}
{"level":"info","ts":1680582361.2064655,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1680582361.2064736,"msg":"serving initial configuration"}

Docker compose

  web_server: #webserver
    image: bwc:v1.344
    container_name: webserver
    ports:
      - "443:443" # added by me
      - "4202:80" 
    networks:
      - bmg
    volumes:
      - ./caddy_data:/data

Dockerfile

...
...
RUN yarn run build

FROM caddy:2.6.4-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /usr/src/app/build /srv

What’s your Caddyfile at this point?

A blank page implies that Caddy didn’t run any request handlers, because none of them matched.

Make a request with curl -v and show what that looks like.

curl -v

[root@eact]# curl -v https://my-domain/
* About to connect() to my-domain port 443 (#0)
* Connected to my-domain (109.123.x.x) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* SSL connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
* Server certificate:
*       subject: CN=my-domain
*       start date: Apr 03 00:00:00 2023 GMT
*       expire date: Jul 02 23:59:59 2023 GMT
*       common name: my-domain
*       issuer: CN=ZeroSSL ECC Domain Secure Site CA,O=ZeroSSL,C=AT
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: my-domain
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Alt-Svc: h3=":443"; ma=2592000
< Referrer-Policy: no-referrer-when-downgrade
< Server: Caddy
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload;
< X-Content-Type-Options: no sniff
< X-Frame-Options: DENY
< Date: Tue, 04 Apr 2023 04:30:02 GMT
< Content-Length: 0
< 
* Connection #0 to host my-domain left intact

Caddyfile

my-domain {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubdomains; preload;"
		X-Content-Type-Options "no sniff"
		X-Frame-Options "DENY"
		Referrer-Policy "no-referrer-when-downgrade"
	}
	encode gzip
	root * /srv/public_html
	file_server
}