caddyserver/caddy · error
opening log writer using %#v: %v
Error message
opening log writer using %#v: %v
What it means
Thrown during CustomLog/BaseLog provisioning when the configured log writer module cannot be opened. Caddy loads the writer module (WriterRaw, e.g. file_writer, net_writer), then calls logging.openWriter; any failure from the writer's own open logic (file permissions, bad path, unreachable socket) surfaces wrapped here with the writer value printed via %#v.
Source
Thrown at logging.go:356
levelEnabler zapcore.LevelEnabler
core zapcore.Core
}
func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error {
if cl.WriterRaw != nil {
mod, err := ctx.LoadModule(cl, "WriterRaw")
if err != nil {
return fmt.Errorf("loading log writer module: %v", err)
}
cl.writerOpener = mod.(WriterOpener)
}
if cl.writerOpener == nil {
cl.writerOpener = StderrWriter{}
}
var err error
cl.writer, _, err = logging.openWriter(cl.writerOpener)
if err != nil {
return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err)
}
// set up the log level
cl.levelEnabler, err = parseLevel(cl.Level)
if err != nil {
return err
}
if cl.EncoderRaw != nil {
mod, err := ctx.LoadModule(cl, "EncoderRaw")
if err != nil {
return fmt.Errorf("loading log encoder module: %v", err)
}
cl.encoder = mod.(zapcore.Encoder)
// if the encoder module needs the writer to determine
// the correct default to use for a nested encoder, we
// pass it down as a secondary provisioning stepView on GitHub (pinned to 50e54ee279)
Solutions
- Check the wrapped error text (%v) — it names the concrete cause (permission denied, no such file or directory, connection refused).
- Verify the log writer path is writable by the Caddy process user (touch the file as that user; fix ownership or chmod).
- Create missing parent directories or use an absolute path that exists.
- For net_writer, confirm the remote endpoint is listening and reachable from the Caddy host.
- Temporarily remove the writer block to fall back to the default StderrWriter{} and confirm the rest of the config loads.
Example fix
// before (caddyfile)
log {
output file /var/log/caddy/access.log
}
// after — directory exists and is writable, or use a writable path
log {
output file /home/caddy/logs/access.log
}
# shell: mkdir -p /var/log/caddy && chown caddy:caddy /var/log/caddy Defensive patterns
Strategy: validation
Validate before calling
// before loading config, verify writability of file log outputs
import "os"
func checkLogWritable(path string) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("log path %s not writable: %w", path, err)
}
return f.Close()
} Try / catch
// Go: treat config-load errors as fatal, but log the wrapped cause
if err := caddy.Load(cfg, true); err != nil {
if strings.Contains(err.Error(), "opening log writer") {
log.Printf("log writer unavailable: %v", err)
}
return err
} Prevention
- Pre-create log directories and chown them to the service user in Dockerfiles/systemd units.
- Run 'caddy validate --config' in CI with the same user that runs production.
- Keep log paths under directories listed as writable in systemd ReadWritePaths.
When it happens
Trigger: Provisioning a logs config whose writer fails to open: file_writer with an unwritable/read-only path or missing directory, net_writer with an unreachable address, or a custom WriterOpener whose OpenWriter returns an error.
Common situations: Running Caddy in a container as a non-root user while logs point at /var/log with root ownership; a path with a typo'd or non-existent parent directory; systemd unit with restrictive ReadWritePaths; switching log output to a socket that is not yet up.
Related errors
- checking if default Caddyfile exists: %v
- reading default Caddyfile: %v
- backing up current binary: %v
- unable to open destination file: %v
- unable to set permissions (%s) on %s: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/d0646f5467369a81.
Report an issue: GitHub.