Using Caddy Server with Ghost CMS: A Modern Alternative to Nginx
Host your Ghost CMS with Caddy for automatic HTTPS, efficient static file handling, and improved security. With minimal configuration, Caddy makes hosting Ghost easier. If you’re looking for a fully managed hosting solution, we offer expert support and are ready to help you get started.
If you’re self-hosting a Ghost CMS site, you’re probably familiar with Nginx as the go-to reverse proxy and static file server. It’s reliable, battle-tested, and deeply integrated in the web hosting world.
But what if you could simplify your configuration, get automatic HTTPS out of the box, and serve both static files and dynamic content—all from a single, elegant config file? Enter Caddy Server.
At our agency, we host Ghost and WordPress for many clients, and we’re always experimenting with newer, better tools that make our lives easier. Nginx is awesome—but it’s also showing its age. SSL setup with certbot or acme.sh works, but it’s still a chore. And let’s be honest: no matter how many scripts you write, things can and do break.
In this article, we’ll walk you through configuring Ghost CMS with Caddy, highlight the pros and cons of using Caddy vs. Nginx, and share a full working Caddyfile that supports static files and secure headers.
Why Consider Caddy Instead of Nginx?
✅ Core Advantages of Caddy
- Automatic HTTPS with built-in Let's Encrypt support—no need for certbot.
- Simplified configuration syntax with the human-friendly
Caddyfile
. - Static file serving built in.
- Security headers, compression, and reverse proxying all in one config.
- Hot reload support for configuration changes.
🚀 Advanced Capabilities
- Dynamic Configuration via API: Reconfigure routes, proxies, or headers on the fly using Caddy's powerful REST/JSON API—no need to restart the server.
- Built-in Metrics: Exposes Prometheus-compatible metrics for real-time observability and monitoring.
- Plugin Ecosystem: Modular builds and a growing list of plugins allow you to extend Caddy for your use case.
- HTTP/3 + QUIC Support: Cutting-edge protocol support gives you faster and more reliable connections for clients that support them
⚠️ Disadvantages of Caddy
- Smaller community and fewer tutorials compared to Nginx.
- Slightly higher memory usage in some cases.
- Some advanced features may require plugins or workarounds.
Ghost CMS + Caddy configuration
Now, let’s assume you already have a running Ghost instance on your server, installed using Ghost-cli (not Docker). In that case, we can expect a few things to be true by default:
- Ghost is running as a system service
- It’s listening on
localhost:2368
- Your site files are located under /var/www/ghost (or a similar directory)
- Installed Caddy on your Ubuntu Linux - install manual can be found here
- Pointed domain to your server (usually A record to your server IP)
CaddyFile configuration ( usually located in /etc/caddy/CaddyFile
)
yourdomain.com {
reverse_proxy localhost:2368
encode gzip zstd
}
Yes, that’s it—this is a working Caddyfile configuration for your Ghost website. If you compare it with a typical Nginx config file, it becomes immediately clear why Caddy feels like such a breeze to work with.

Okay, maybe it’s a bit over the top—but you get the idea. Despite how minimal the Caddy config looks, it is doing a lot of heavy lifting behind the scenes. It automatically fetches SSL certificates for your domain—no need to mess with scripts or tools like Certbot. It also handles HTTP to HTTPS redirection out of the box, proxies all requests to your Ghost app, and even applies compression to reduce data transfer.
With this minimal config, you’re good to go—so if you’re itching to get hands-on, feel free to start fiddling with your server now. But if you’re like me and not quite satisfied with just the basics, stick around—we’ll go a bit deeper in the next section.
Let Web Servers Do What They Do Best: Serve Static Files
Our simple Caddy example works great—but it can work even better. Instead of letting the Ghost app handle static files, we can delegate that job to Caddy. Web servers are generally better equipped for serving static assets than web apps, and by doing so, we also gain more control. For example, we can set custom cache expiration headers to improve performance and caching behavior.
yourdomain.com {
handle_path /assets/* {
root * /var/www/ghost/content/themes/journal/assets
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
reverse_proxy localhost:2368
encode gzip zstd
}
- handle_path /assets/* { ... }
- Matches all requests starting with /assets/ (like /assets/style.css or /assets/images/logo.png).
- Automatically strips the /assets part from the URL path before processing it inside the block.
- root * /var/www/ghost/content/themes/journal/assets
- Sets the file lookup root for matching requests.
- After stripping /assets, the file is looked up inside this directory.
- For example:
- /assets/images/logo.png → looks for/var/www/ghost/content/themes/journal/assets/images/logo.png
- file_server
- Tells Caddy to serve files directly from the filesystem (no backend proxying).
- If the file exists, it gets served as-is with default MIME types and status codes.
- header Cache-Control "public, max-age=31536000, immutable"
- Adds a Cache-Control header to every file served from this block.
- This enables long-term caching:
- public: allows shared caches (e.g., CDNs) to store the file.
- max-age=31536000: instructs clients to cache the file for one year (in seconds).
- immutable: signals that the file will never change, so the browser doesn’t need to revalidate it.
With this setup, Caddy now serves all the static files from our Journal theme, giving Ghost more breathing room to focus on dynamic content. But we can still take it a step further—theme files aren’t the only assets that are, well… static.
When you upload an image to an article through the Ghost Admin panel, Ghost stores the original file on disk. However, the image displayed in the article is often a resized version, optimized for layout and performance. The size and resolution of this version depend on your theme’s configuration. In general, most content management systems aim to serve smaller, optimized images when the full-size version isn’t necessary—reducing bandwidth and improving load times.
However, there’s a small caveat when it comes to serving resized images directly from Caddy: these optimized image versions don’t exist until someone visits the article. Ghost generates the smaller, optimized versions of uploaded images on-demand, the first time the article is viewed. That means on the very first visit, the requested image doesn’t exist on disk yet—so Caddy can’t serve it as a static file. We need some conditional matching to handle this right.
yourdomain.com {
handle_path /assets/* {
root * /var/www/ghost/content/themes/journal/assets
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
handle_path /content/images/* {
root * /var/www/ghost/content/images
@exists file
handle @exists {
file_server
}
@missing not file
handle @missing {
rewrite * /content/images{path} # Restore full path before proxying
reverse_proxy localhost:2368
}
}
reverse_proxy localhost:2368
encode gzip zstd
}
- Caddy first checks if the requested image file exists in the /var/www/ghost/content/images folder (where Ghost stores uploaded images).
- If the file exists, Caddy serves it directly — no need to bother Ghost. This makes things faster and reduces load on the app.
- If the file doesn’t exist yet (for example, a resized image that hasn’t been generated yet), Caddy:
- Restores the original URL path.
- Proxies the request to Ghost running at 127.0.0.1:2368, so Ghost can generate and return the image.
This way, Ghost only handles images once — after that, Caddy takes over serving them.
Now we have support even for uploaded images. Same approach can be used for media files.
Built-In Logging in Caddy: Just Point It Where You Want
Caddy supports logging to files like any mature web server, and it offers a surprisingly flexible range of logging options. You can fine-tune what gets logged, how it’s formatted, and where it’s stored. For a full breakdown, check out the official Caddy logging documentation.
yourdomain.com {
log {
output file /var/log/caddy/myghost-access.log
}
handle_path /assets/* {
root * /var/www/ghost/content/themes/journal/assets
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
handle_path /content/images/* {
root * /var/www/ghost/content/images
@exists file
handle @exists {
file_server
}
@missing not file
handle @missing {
rewrite * /content/images{path} # Restore full path before proxying
reverse_proxy localhost:2368
}
}
reverse_proxy localhost:2368
encode gzip zstd
}
Hardening Your Ghost Site with HTTP Security Headers
While Caddy does a great job of serving content and handling HTTPS by default, adding extra security headers can further protect your site from a range of common attacks. These headers can help prevent things like cross-site scripting (XSS), clickjacking, and information leaks, all with minimal configuration. In this section, we’ll go over the essential headers to add to your Caddyfile, ensuring your site is secure and your users are protected.
yourdomain.com {
log {
output file /var/log/caddy/myghost-access.log
}
# Security Headers
header {
Strict-Transport-Security "max-age=31537777"
Referrer-Policy "origin-when-cross-origin"
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
X-Content-Type-Options "nosniff"
X-Frame-Options "sameorigin"
X-Xss-Protection "1; mode=block"
}
handle_path /assets/* {
root * /var/www/ghost/content/themes/journal/assets
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
handle_path /content/images/* {
root * /var/www/ghost/content/images
@exists file
handle @exists {
file_server
}
@missing not file
handle @missing {
rewrite * /content/images{path} # Restore full path before proxying
reverse_proxy localhost:2368
}
}
reverse_proxy localhost:2368
encode gzip zstd
}
- Strict-Transport-Security:Forces browsers to always use HTTPS when communicating with your site.max-age=31537777 tells the browser to remember this rule for about a year.
- Referrer-Policy: origin-when-cross-origin:Controls how much referrer information is sent when navigating away. This setting sends the full referrer for same-origin requests, but only the origin (domain) for cross-origin ones — a good privacy/security balance.
- Permissions-Policy: Disables browser features that are rarely needed and can be abused, like access to the camera, mic, geolocation, etc.accelerometer=(), camera=(), ... explicitly denies these features.
- X-Content-Type-Options: nosniff: Prevents the browser from guessing (“sniffing”) the content type. This reduces the risk of certain kinds of attacks, like executing uploaded scripts as HTML.
- X-Frame-Options: sameorigin:Prevents your site from being embedded in an iframe on another domain — which protects against clickjacking.
- X-XSS-Protection: 1; mode=block (mostly legacy):Tells older browsers to block rendering if a cross-site scripting (XSS) attack is detected.
To wrap it up
Caddy simplifies server management for Ghost CMS, offering automatic HTTPS, efficient static file serving, and powerful security headers. It reduces complexity while delivering excellent performance. Whether you’re a developer or a content creator, Caddy’s ease of use and flexibility make it an ideal choice for hosting Ghost. We’ve barely scratched the surface here, so I recommend diving into the documentation to learn more about this amazing piece of tech.
If you’re looking for a reliable, fully managed hosting solution for your Ghost site, we’re here to help. With years of experience hosting numerous clients, we ensure high uptime, optimal performance, and ongoing support. Reach out to us today, and let us handle the technical details so you can focus on creating great content.