1. The problem I’m having:
DNS Challenge (Digital Ocean) for my Caddy server (v2.9.1) with a wildcard domain doesn’t work.
I use Caddy through FrankenPhp builder.
That worked few months ago but I destroyed and recreate my digital ocean droplet and all networking config.
I configured DNS records for wildcard ( A *.aboulbox.com directs to XXX.XXX.XXX.XXX).
When I test Digital Ocean API to access domain config, it works:
curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer api_token" "https://api.digitalocean.com/v2/domains/aboulbox.com/records"
I’m sure it’s a stupid thing that I miss but I don’t know what…
2. Error messages and/or full log output:
2025/04/29 18:12:06.563 ERROR cleaning up solver {"identifier": "*.aboulbox.com", "challenge_type": "dns-01", "error": "no memory of presenting a DNS record for \"_acme-challenge.aboulbox.com\" (usually OK if presenting also failed)"}
github.com/mholt/acmez/v3.(*Client).solveChallenges.func1
/root/go/pkg/mod/github.com/mholt/acmez/v3@v3.1.1/client.go:318
github.com/mholt/acmez/v3.(*Client).solveChallenges
/root/go/pkg/mod/github.com/mholt/acmez/v3@v3.1.1/client.go:363
github.com/mholt/acmez/v3.(*Client).ObtainCertificate
/root/go/pkg/mod/github.com/mholt/acmez/v3@v3.1.1/client.go:136
github.com/caddyserver/certmagic.(*ACMEIssuer).doIssue
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/acmeissuer.go:489
github.com/caddyserver/certmagic.(*ACMEIssuer).Issue
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/acmeissuer.go:382
github.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue
/root/go/pkg/mod/github.com/caddyserver/caddy/v2@v2.9.1/modules/caddytls/acmeissuer.go:249
github.com/caddyserver/certmagic.(*Config).obtainCert.func2
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/config.go:626
github.com/caddyserver/certmagic.doWithRetry
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/async.go:104
github.com/caddyserver/certmagic.(*Config).obtainCert
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/config.go:700
github.com/caddyserver/certmagic.(*Config).ObtainCertAsync
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/config.go:505
github.com/caddyserver/certmagic.(*Config).manageOne.func1
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/config.go:415
github.com/caddyserver/certmagic.(*jobManager).worker
/root/go/pkg/mod/github.com/caddyserver/certmagic@v0.22.2/async.go:73
2025/04/29 18:12:06.699 ERROR tls.obtain could not get certificate from issuer {"identifier": "*.aboulbox.com", "issuer": "acme-v02.api.letsencrypt.org-directory", "error": "[*.aboulbox.com] solving challenges: presenting for challenge: adding temporary record for zone \"aboulbox.com.\": Post \"https://api.digitalocean.com/v2/domains/aboulbox.com/records\": oauth2: token expired and refresh token is not set (order=https://acme-staging-v02.api.letsencrypt.org/acme/order/197355664/24222599644) (ca=https://acme-staging-v02.api.letsencrypt.org/directory)"}
2025/04/29 18:12:06.699 INFO tls.issuance.acme using ACME account {"account_id": "https://acme.zerossl.com/v2/DV90/account/0f_nCQmPJCl7VvF_sqstrA", "account_contact": ["mailto:mymail@gmail.com"]}
2025/04/29 18:12:06.911 INFO trying to solve challenge {"identifier": "aboulbox.com", "challenge_type": "dns-01", "ca": "https://acme.zerossl.com/v2/DV90"}
2025/04/29 18:12:06.912 ERROR cleaning up solver {"identifier": "aboulbox.com", "challenge_type": "dns-01", "error": "no memory of presenting a DNS record for \"_acme-challenge.aboulbox.com\" (usually OK if presenting also failed)"}
3. Caddy version:
v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
4. How I installed and ran Caddy:
Build from FrankenPhp Dockerfile, using xcaddy.
Dockerfile:
#syntax=docker/dockerfile:1.4
# Versions
FROM dunglas/frankenphp:1.5-builder-php8.3 AS builder
# Copy xcaddy in the builder image
COPY --from=caddy:2.9.1-builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
RUN xcaddy build v2.9.1 \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/caddy-dns/digitalocean \
--with github.com/dunglas/caddy-cbrotli \
--with github.com/greenpau/caddy-security
# Mercure and Vulcain are included in the official build, but feel free to remove them
# Add extra Caddy modules here
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
# Replace the official binary by the one contained your custom modules
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
# Base FrankenPHP image
FROM frankenphp_upstream AS frankenphp_base
WORKDIR /app
VOLUME /app/var/
# persistent / runtime deps
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
acl \
file \
gettext \
git \
nano \
locales \
iputils-ping \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
install-php-extensions \
@composer \
apcu \
intl \
opcache \
zip \
gd \
;
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
###> recipes ###
###> doctrine/doctrine-bundle ###
RUN install-php-extensions pdo_pgsql
###< doctrine/doctrine-bundle ###
###< recipes ###
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile
ENTRYPOINT ["docker-entrypoint"]
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ]
# Dev FrankenPHP image
FROM frankenphp_base AS frankenphp_dev
ENV APP_ENV=dev XDEBUG_MODE=off
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
RUN set -eux; \
install-php-extensions \
xdebug \
;
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
# Prod FrankenPHP image
FROM frankenphp_base AS frankenphp_prod
ENV APP_ENV=prod
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
# prevent the reinstallation of vendors at every changes in the source code
COPY --link composer.* symfony.* ./
RUN set -eux; \
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
# copy sources
COPY --link . ./
RUN rm -Rf frankenphp/
RUN set -eux; \
mkdir -p var/cache var/log; \
composer dump-autoload --classmap-authoritative --no-dev --optimize; \
composer dump-env prod; \
composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync;
a. System environment:
root@2261eaef1f85:/app# frankenphp environ
caddy.HomeDir=/root
caddy.AppDataDir=/data/caddy
caddy.AppConfigDir=/config/caddy
caddy.ConfigAutosavePath=/config/caddy/autosave.json
caddy.Version=v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=
runtime.GOOS=linux
runtime.GOARCH=amd64
runtime.Compiler=gc
runtime.NumCPU=2
runtime.GOMAXPROCS=2
runtime.Version=go1.24.2
os.Getwd=/app
SERVER_NAME=aboulbox.com
APP_CATCHA_TOKEN=
DO_API_TOKEN=api_token
CADDY_GLOBAL_OPTIONS=acme_dns digitalocean {api_token}
ADMIN_PASSWORD_HASH=password
STABILITY=stable
DATABASE_URL=postgresql://main:main@database:5432/main?serverVersion=15&charset=utf8
HOSTNAME=2261eaef1f85
LANGUAGE=en_US:en
PHP_VERSION=8.3.20
PHP_INI_DIR=/usr/local/etc/php
GPG_KEYS=
XDG_DATA_HOME=/data
GODEBUG=cgocheck=0
XDG_CONFIG_HOME=/config
APP_CONSTRUCTION=1
PHP_LDFLAGS=-Wl,-O1 -pie
PWD=/app
HOME=/root
LANG=en_US.UTF-8
APP_ENV=prod
ADMIN_PASSWORD=admin
PHP_SHA256=f15914e071b5bddaf1475b5f2ba68107e8b8846655f9e89690fb7cd410b0db6c
PHPIZE_DEPS=autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c
TERM=xterm
PHP_URL=https://www.php.net/distributions/php-8.3.20.tar.xz
ADMIN_USER=admin
SHLVL=1
COMPOSER_ALLOW_SUPERUSER=1
PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
UMAMI_WEBSITE_ID=
FRANKENPHP_CONFIG=import worker.Caddyfile
LC_ALL=en_US.UTF-8
MAILER_DSN=
HMAC_KEY=
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PHP_INI_SCAN_DIR=:/usr/local/etc/php/app.conf.d
SYMFONY_VERSION=
PHP_ASC_URL=https://www.php.net/distributions/php-8.3.20.tar.xz.asc
PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
APP_SECRET=
_=/usr/local/bin/frankenphp
c. Service/unit/compose file:
docker-compose.yml:
services:
php:
image: ${IMAGES_PREFIX:-}app-php
restart: unless-stopped
container_name: php
environment:
SERVER_NAME: ${SERVER_NAME:-localhost}
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
DATABASE_URL: postgresql://${POSTGRES_USER:-main}:${POSTGRES_PASSWORD:-main}@database:5432/${POSTGRES_DB:-main}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
STABILITY: ${STABILITY:-stable}
ADMIN_USER: ${ADMIN_USER:-admin}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
ADMIN_PASSWORD_HASH: ${ADMIN_PASSWORD_HASH:-password}
DO_API_TOKEN: ${DO_API_TOKEN}
volumes:
- caddy_data:/data
- caddy_config:/config
- ./caddy-data/caddy/auth/:/etc/gatekeeper/auth
ports:
- "80:80"
- "443:443"
- "443:443/udp"
networks:
- monitor-net
otterwiki:
image: redimp/otterwiki:2
container_name: otterwiki
restart: unless-stopped
volumes:
- ./otterwiki:/app-data
networks:
- monitor-net
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
environment:
DATABASE_TYPE: postgresql
APP_SECRET: ${APP_SECRET}
restart: unless-stopped
env_file: ".umami.env"
depends_on:
- database
- php
labels:
org.label-schema.group: "analytics"
networks:
- monitor-net
dashy:
image: lissy93/dashy
container_name: dashy
restart: unless-stopped
volumes:
- ./dashy/user-data:/app/user-data
environment:
- NODE_ENV=production
depends_on:
- php
networks:
- monitor-net
database:
image: tensorchord/pgvecto-rs:pg16-v0.2.1
container_name: database
environment:
POSTGRES_PASSWORD: main
POSTGRES_USER: main
POSTGRES_DB: main
ports:
- "5432"
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- monitor-net
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###
volumes:
postgres_data: {}
caddy_config:
caddy_data:
networks:
monitor-net:
driver: bridge
d. My complete Caddy config:
Caddyfile:
############
# Globals #
############
{
{$CADDY_GLOBAL_OPTIONS}
grace_period 10s
email mymail@gmail.com
default_sni {$SERVER_NAME}
order authenticate before respond
order authorize before reverse_proxy
security {
local identity store localdb {
realm local
path /data/caddy/.local/users.json
}
authentication portal myportal {
enable identity store localdb
crypto default token lifetime 86400
cookie lifetime 86400
crypto key verify from file /etc/gatekeeper/auth/jwt/sign_key.pem
ui {
links {
"Dashboard" https://dash.{$SERVER_NAME:localhost}
"Monitoring" https://monitor.{$SERVER_NAME:localhost}
"Wiki" https://wiki.{$SERVER_NAME:localhost}
"Analytics" https://analytics.{$SERVER_NAME:localhost}
"Immich" https://dash.{$SERVER_NAME:localhost}
"Tandoor" https://tandoor.{$SERVER_NAME:localhost}
}
}
}
authorization policy users_policy {
set auth url https://auth.{$SERVER_NAME:localhost}
bypass uri prefix /api
# Verify signed key
crypto key sign-verify from file /etc/gatekeeper/auth/jwt/verify_key.pem
## Inject header to downstream
inject headers with claims
inject header "X-JWT-Assertion" from {http.request.cookie.access_token}
allow roles authp/admin authp/user
}
}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
############
# Snippets #
############
(main) {
log {
format filter {
wrap json
fields {
common_log delete
request>tls delete
}
}
level error
}
respond /robots.txt 200 {
body "User-agent: *
Disallow: /
User-agent: AdsBot-Google
Disallow: /
User-agent: AdsBot-Google-Mobile
Disallow: /"
close
}
}
###############
# aboulbox.com #
###############
https://{$SERVER_NAME:localhost} {
import main
root * /app/public
encode zstd br gzip
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
php_server
}
https://*.{$SERVER_NAME:localhost} {
import main
@auth host auth.{$SERVER_NAME:localhost}
@monitor host monitor.{$SERVER_NAME:localhost}
@mail host mail.{$SERVER_NAME:localhost}
@wiki host wiki.{$SERVER_NAME:localhost}
@analytics host analytics.{$SERVER_NAME:localhost}
@dash host dash.{$SERVER_NAME:localhost}
@gallery host gallery.{$SERVER_NAME:localhost}
@tandoor host tandoor.{$SERVER_NAME:localhost}
handle @auth {
authenticate with myportal
}
handle @wiki {
authorize with users_policy
reverse_proxy otterwiki {
header_up x-otterwiki-name "Aboul"
header_up x-otterwiki-email "mymail@gmail.com"
header_up x-otterwiki-permissions "READ,WRITE,UPLOAD,ADMIN"
}
}
handle @analytics {
authorize with users_policy
reverse_proxy umami:3000
}
handle @dash {
authorize with users_policy
reverse_proxy dashy:8080
}
handle {
redir https://www.youtube.com/watch?v=xm3YgoEiEDc 302
}
}