Why teams put EspoCRM behind Caddy with automatic TLS
A team running EspoCRM on its own server usually needs HTTPS that renews itself without someone tracking expiry dates by hand. Caddy can obtain and renew certificates automatically for the hostname it serves, so the web tier handles TLS while EspoCRM stays behind it on a private network. That removes a recurring operational task, but it does not remove the need to plan the certificate source, the hostname and the ports that must be reachable from the internet before go-live.
Caddy fits best when the team wants a small, readable configuration file, automatic certificates and a single proxy in front of the EspoCRM container. Nginx, Apache and IIS remain the choices when the organization already runs them, has standard hardening baselines for them, or needs their modules. Traefik is the usual alternative when several containers on one host are discovered through labels. Whichever proxy you choose, the EspoCRM server requirements still apply: PHP 8.3 to 8.5, MySQL 8.0 or newer, or MariaDB 10.6 or newer or PostgreSQL 15, and cron or a scheduled task for background jobs. Confirm those against the EspoCRM server configuration docs for the version you deploy.
This topic describes one deployment path. The official installation script for Ubuntu, Debian and Linux Mint deploys a Docker stack with Nginx and MariaDB, so teams that want the vendor's default layout should start there and treat a Caddy setup as a deliberate change. Before you choose, confirm which proxy your security team can monitor and patch, who owns certificate issuance if you use your own certificates, and whether the server can reach the certificate authority on the ports it needs.
Which TLS approach fits your environment
Use this approach when the CRM hostname is a public domain that your users reach from the internet or from networks that trust public certificate authorities. Caddy requests and renews the certificate automatically from a public certificate authority over the ACME protocol, so the Caddyfile only needs the site address, and no certificate files are stored by your team. The work sits in DNS and network access, which must be right before the first start.
- DNS record: The hostname must have an A or AAAA record pointing at the public address of the host running Caddy.
- Inbound ports: Port 80 and port 443 must be reachable from the internet for the HTTP-01 or TLS-ALPN-01 challenge, unless your DNS provider plugin handles the DNS-01 challenge.
- Redirects: Caddy redirects HTTP to HTTPS by default when a site address is given, so confirm no other service occupies port 80 on that host.
- Rate limits: Repeated failed attempts can exhaust certificate authority limits, so test against the staging endpoint before switching to production.
- Renewal: Confirm the host can reach the certificate authority at renewal time and that Caddy's data volume persists across container recreation.
Confirm the exact ACME options and supported challenge types against the current Caddy documentation for your version before you commit to a design.
Use this approach when the CRM is reachable only on a private network, such as a hostname under an internal domain that no public certificate authority can validate. Public ACME validation will fail for names that resolve only inside your network, so Caddy must issue certificates from a certificate authority your organization controls. Caddy can run its own internal certificate authority, or you can point it at an existing enterprise CA, and every client must trust the root certificate.
- Internal CA choice: Caddy's built-in internal issuer creates a local root, while an enterprise CA requires a signing certificate and key that Caddy can load.
- Trust distribution: Browsers and operating systems on managed devices need the root certificate installed through group policy or device management.
- Naming: Use a hostname your DNS resolves internally and that matches the certificate subject alternative names.
- Renewal: Short-lived internal certificates renew automatically, but the root certificate expiry date must be tracked separately.
- Access path: Confirm which network segments can reach ports 80 and 443 on the Caddy host, since the internal issuer needs no public inbound access.
Ask your identity or infrastructure team who owns the internal CA and how root rotation is handled before you write the Caddyfile.
Use this approach when your organization buys or issues a certificate through a procurement process, such as an extended validation certificate or a wildcard certificate used across several services. Caddy loads the certificate and private key from files you provide, so it does not contact a certificate authority for that site. Your team then owns renewal, which becomes a manual or scripted task outside Caddy.
- Caddyfile setting: Use the
tlsdirective with the paths to the certificate chain and private key, for exampletls /etc/caddy/certs/crm.pem /etc/caddy/certs/crm.key. - Chain file: Include the intermediate certificates in the certificate file, in the order your authority specifies.
- Key handling: Store the private key with restricted file permissions and mount it into the container as a read-only volume.
- Reload: After renewal, replace the files and reload Caddy so it picks up the new certificate.
- Expiry tracking: Add the expiry date to your certificate inventory and alert before it passes.
Confirm with your certificate provider the chain format and key type they issue, and confirm that the subject names match the EspoCRM hostname exactly.
Caddy configuration options and when each fits
This table compares the main Caddyfile choices for putting EspoCRM behind Caddy, from automatic public certificates to internal certificates and your own files. Read each row for the situation it suits and the item to confirm in the Caddy documentation for the version you run. The main takeaway is that the hostname's reachability from the public internet decides most of the TLS choice.
| Option | What it does | When it fits | What to confirm |
|---|---|---|---|
| Automatic certificate via public hostname | Caddy obtains and renews a certificate for the site address on its own | EspoCRM is reachable on a public DNS name and ports 80 and 443 are open to the issuer | Caddy version, ACME challenge types allowed, and how the issuer is reached from your network |
| tls directive with your own certificate and key | Caddy loads a certificate and private key file you supply instead of issuing one | Your security team issues certificates from an internal or commercial CA | File paths, file permissions for the Caddy process, and the reload procedure after renewal |
| Internal certificate for private networks | Caddy issues certificates from its own local certificate authority | EspoCRM runs on an internal hostname that no public CA can validate | Whether clients will trust the Caddy root certificate and how it is distributed |
| reverse_proxy target for the EspoCRM container | Forwards requests from Caddy to the EspoCRM container on the Compose network | Caddy and EspoCRM share a Docker network and the container name resolves | Container name, internal port, and the Caddy directive syntax for your version |
| Header settings | Sets or removes response headers such as security headers at the proxy | You want consistent headers across all EspoCRM responses | Which headers EspoCRM already sends and which ones duplicate or conflict |
| Request body size limit | Caps the size of uploads accepted by the proxy | Users attach large files to records or emails | The limit in EspoCRM's own upload settings, so the two values match |
| Timeouts for long requests | Controls how long Caddy waits on upstream responses | Imports, reports or mass actions take longer than a typical page load | Default timeout values in the Caddy version you deploy |
Setting up the Compose file, Caddyfile and certificates
This sequence takes a host from DNS records to a running EspoCRM instance served over HTTPS by Caddy, with certificates, site links, background jobs and the real-time channel checked before anyone signs in. Each step closes with a check, so a fault surfaces where it was introduced.
Prepare DNS and firewall rules
The platform or network engineer creates a DNS record for the EspoCRM hostname that points to the public address of the host, then opens inbound web traffic to that host and blocks other inbound ports at the perimeter. Caddy must be reachable from the public internet on the standard web ports so the certificate issuer can complete its checks, so confirm this with your network team first. Done means the hostname resolves to the expected address from outside your network and a test connection to the web ports succeeds from an external machine.
Write the Compose file with three services
The engineer writes one Compose file with three services, Caddy, MariaDB and EspoCRM, all attached to a private network so that only Caddy publishes ports to the host. Confirm the EspoCRM image runs a PHP version in the 8.3 to 8.5 range and that the MariaDB image is version 10.6 or newer, as listed in the EspoCRM server configuration requirements. Pin image tags to versions you have tested and avoid floating latest tags. Done means the file passes validation and the rendered output shows the network and volumes you intended.
Set environment values without committing secrets
The engineer places database passwords, the EspoCRM administrator password and the public site address in an environment file kept out of version control, and restricts its permissions so only the deploying account can read it. The Compose file references variables and holds no literal credentials. For stricter environments, Docker secrets can carry the database passwords instead. Confirm the variable names expected by the EspoCRM and MariaDB images in their current documentation before writing them. Done means the repository holds only a template with placeholder values, and the variables resolve on the host.
Write the Caddyfile for your hostname
The engineer creates a Caddyfile with one site block for the EspoCRM hostname that reverse proxies all requests to the EspoCRM container on the private network. For a public hostname, Caddy obtains and renews certificates automatically, so the block needs the name and the upstream service. If you supply your own certificate instead, the block references the certificate and key files with a tls directive, and Caddy must be able to read those mounted files. Confirm the upstream matches the EspoCRM service name and port in the Compose file. Done means
caddy validatereports the configuration as valid.Start the stack and read the logs
The engineer starts the three services with Compose, lets MariaDB finish initialization before EspoCRM connects, and reads the Caddy and EspoCRM logs for errors. A failed start often traces to a wrong database variable or a service name that does not resolve on the private network, so the logs are the first place to look. Done means all three containers report running, EspoCRM reaches the database without errors, and the Caddy log shows the site is being served.
Confirm certificate issuance
The engineer checks that Caddy obtained a certificate for the hostname, then confirms its subject, issuer and expiry from an external network. Issuance depends on DNS pointing to this host and the web ports reaching it, so a failure here usually leads back to the first step. If you use your own certificate, confirm the chain is complete and record the expiry date in your renewal calendar. Done means a client outside your network reaches the hostname over HTTPS with a certificate that names it, and plain HTTP redirects to the secure address.
Verify EspoCRM generates correct links
The administrator sets the site URL in EspoCRM to the public HTTPS address, then checks that login redirects, password reset emails, notification emails and record links all use https and the expected hostname. Caddy forwards the original scheme and host, but EspoCRM must be configured to build links from the public address. Confirm the exact settings for your EspoCRM version in the server configuration documentation. Done means a test record's link and a password reset email both contain the correct public HTTPS address.
Check cron and WebSocket after the proxy change
The engineer confirms that scheduled background jobs run and that the real-time channel still connects through Caddy. EspoCRM needs cron or an equivalent scheduled task for background jobs, so the scheduler must be running and its job activity visible in the administration area. Caddy passes WebSocket upgrade requests through its reverse proxy, but the route must match the path the WebSocket service uses, so confirm that path in your WebSocket setup. Done means a scheduled job records a completed run, and a second browser session receives a live update when a record changes.
Proxy settings EspoCRM needs behind Caddy
Caddy terminates TLS and forwards requests to the EspoCRM container over plain HTTP, so EspoCRM only works correctly behind it if it can see the original scheme and host. Caddy's reverse_proxy directive sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host by default, but the PHP web server in front of EspoCRM must trust those headers from Caddy and not from arbitrary clients. Without that, login redirects, generated links and session cookies can point at ` or the internal container name. Confirm the trusted proxy and header handling in the server configuration section of the EspoCRM docs for your version, since the exact setting names and where they live are version-dependent.
The site URL is the setting most often wrong after a proxy change. EspoCRM builds links in notification emails, reset messages and API responses from its configured site URL, so it must be the public ` address that users type, not the container address. Set it once in the administration settings and test a password reset and an email link from outside the network. Also confirm the upload size and request timeout limits. Caddy does not impose a small upload cap of its own, but the PHP layer and EspoCRM's own limits do, so large attachments and CSV imports can fail with a generic error that looks like a proxy problem. Check the PHP upload and post size values and the execution time against the docs before go-live.
Finally, the EspoCRM container should not be reachable directly on a host port. Publish only Caddy on ports 80 and 443 and keep the application and database on an internal Docker network, so every request passes through TLS and any header rules you rely on. If you restrict access by IP address, as the EspoCRM security docs recommend, decide whether that check sees the client address or the Caddy address, since a misconfigured trusted-proxy setting can make every user appear to come from one IP. Test this from a machine outside your network and record the result.
Security considerations for a Caddy-fronted EspoCRM
A Caddy-fronted EspoCRM has one public entry point, and everything behind it depends on how that edge and the application are configured. The cards below cover the network boundary, the Caddy admin interface, IP restrictions, account hygiene, session settings and logs, which together decide how much an exposed login page can give away.
Keep database and application off the public network
Only Caddy should listen on ports 80 and 443. In the Compose file, publish no host ports for the EspoCRM and MariaDB containers and attach them to an internal network so Caddy reaches EspoCRM by service name. Check with a port scan from outside your network that the database port is closed, and confirm that the EspoCRM container cannot be reached directly on its own port.
Protect the Caddy admin endpoint
Caddy exposes an admin API, which by default listens on localhost inside the container. Confirm that it is not bound to a public interface, and do not add a ports mapping for it. If you use the admin API for configuration reloads, restrict it to the host and disable it in the Caddyfile if your team reloads through the container instead.
Restrict access by IP address
The EspoCRM security documentation recommends restricting access by IP address. In Caddy, a remote_ip matcher can limit the admin area or the whole site to office and VPN ranges. Decide which paths need broad access, such as customer portals, and which need narrow access, and test the rule from an address outside the list before relying on it.
Avoid admin accounts for daily work
The security documentation advises against using admin accounts for daily tasks. Create a named administrator for configuration work, and give everyday users roles scoped to their teams. Confirm that the admin account has its own strong password and that password recovery is disabled for admins, so a phished reset link cannot grant administrative access.
Set 2FA and token lifetime
Require two-factor authentication for all users, and shorten the authentication token lifetime so a stolen session expires sooner. Note that EspoCRM's own 2FA is not compatible with OpenID Connect sign-in, so if you use SSO, rely on the identity provider's MFA policy instead. Verify the setting after changes by logging in again from a second browser.
Handle access and error logs
Caddy writes access logs in JSON when configured, and EspoCRM writes its own application logs inside the container. Decide where each goes, how long they are kept, and who may read them, because access logs contain client addresses and request paths that may hold personal data. Confirm that log volumes are backed up or shipped off-host and that the retention period matches your policy.
Questions teams ask before running EspoCRM behind Caddy
Does EspoCRM officially support Caddy as a web server?
The EspoCRM server configuration docs cover Apache, Nginx and IIS, and the supplied documentation does not include a Caddy configuration. Caddy can still work as a reverse proxy in front of an EspoCRM container, but your team owns that configuration and its testing. Before go-live, check forwarded headers, the public document root, upload size limits and timeouts against the EspoCRM server configuration docs. Treat the Caddyfile as a custom deployment choice and document it as one.
How much effort does the proxy work take compared with the rest of the deployment?
For most deployments the proxy is a small part of the total work. A Caddyfile for one hostname is short, and most effort goes to the database, PHP or container setup, background jobs, backups, identity integration and data migration. The proxy still needs DNS records, certificate testing, header checks, and verification of logins, file uploads and any real-time features. No duration is set here. Ask any partner to base the estimate on a written scope that lists each of these items separately.
What makes the Caddy setup harder to get right?
Four factors add the most effort to a Caddy setup:
- Internal-only hosts, because automatic certificates usually depend on public DNS and reachable ports, so an internal name may need an internal CA or a DNS challenge. Confirm the options in Caddy's own documentation.
- Custom certificates, because you must decide who issues, stores, renews and rotates them.
- Multiple apps on one host, because each hostname needs its own routing, certificate and header rules.
- Upload and timeout limits for attachments and long imports, which must match what EspoCRM expects.
Can an internal IT team set this up on its own?
Yes, if the team already runs Linux and Docker, controls DNS for the chosen hostname, and can read both the EspoCRM and Caddy documentation carefully. The harder part is the security configuration around the proxy: restricting admin access, enforcing two-factor authentication, limiting token lifetime and deciding on IP restrictions. Those settings are listed in the EspoCRM security docs and should be reviewed by someone accountable for the system. Teams without that capacity often choose a partner for the review, the hardening or the whole build.
What do we need to provide before the work starts?
Prepare these items before any proxy work begins:
- A domain or subdomain you control, plus access to create and change its DNS records.
- Server access with Docker and a named administrator who can approve changes.
- Network rules that allow the ports your certificate method needs, and that match your firewall policy.
- A written certificate policy: public automatic certificates, an internal certificate authority, or purchased certificates, with an owner for renewal.
- A backup location and an agreed contact for incidents and expiry alerts.
If any item is missing, the scope and the quote will change.
Is automatic TLS enough for a regulated environment?
Automatic certificates handle issuance and renewal, but a regulated organization still needs decisions and evidence beyond that. You need to document who controls the private keys, which certificate authority is approved, how expiry is monitored, which TLS versions and ciphers are allowed, and how changes are recorded. DeployOpen prepares systems, controls and evidence for compliance assessments. Independent auditors and certification bodies issue any audit report or certification, so ask your assessor early which evidence they expect from the proxy layer.
When should we bring in a partner for this?
Consider a partner when the host runs several applications, when internal names need an internal certificate authority, when the environment falls under a compliance assessment, or when the team lacks Docker and PHP operations experience. A partner is also useful when the deadline is fixed and the proxy work must run alongside database, backup and identity tasks. Most DeployOpen engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts, and a quote follows from that written scope.
Does the choice of proxy change EspoCRM's license or cost?
Caddy does not change EspoCRM's license terms. EspoCRM is licensed under AGPL-3.0, and the open-source edition can be downloaded and self-hosted for free, according to the EspoCRM pricing page. Your Caddy licensing is a separate question that your team should confirm against Caddy's own license terms. Costs that do change with the proxy are staff time for configuration, testing and renewal ownership, plus any internal certificate authority or monitoring you add.
Official EspoCRM sources for the deployment
Start with the EspoCRM server configuration documentation before you write any Caddy configuration. It is the authoritative reference for the PHP versions, database engines and web server setups EspoCRM supports, and it also explains the cron job that background processing depends on. Confirm that your chosen MariaDB or MySQL version, and your PHP version, appear in that list for the EspoCRM release you plan to run, because the supported range changes between major versions. The EspoCRM installation by script documentation describes a Docker-based stack that uses Nginx rather than Caddy, so use it as a reference for container layout, environment variables and the command.sh utility, not as a template for your proxy.
The Caddy-specific parts of this deployment, including the Caddyfile directives, automatic certificate behavior, the storage location for issued certificates and the options for supplying your own certificate files, come from the Caddy documentation for the exact Caddy version your team runs. Caddy's defaults and directive syntax have changed across releases, so pin the image tag in your Compose file and read the matching documentation before you copy any example. Ask your platform team to record the Caddy version in the same change record as the EspoCRM version, so that a later image update does not silently change how TLS or request handling behaves.
EspoCRM's documentation does not cover Caddy, so the team owns the proxy configuration and its validation. Treat the EspoCRM docs as the contract for what the application expects from the proxy (the public document root, forwarded headers and a working PHP handler), and the Caddy docs as the contract for how the proxy behaves. Before go-live, check both sets of documentation against the running stack rather than trusting a single example, and record which page of which documentation you relied on for each setting.

