{"timestamp": "2026-07-10T22:10:17.210032", "data": {"url": "https://timwilson.info/posts/2023/02/hosting-multiple-sites-on-one-host-with-a-caddy-proxy-server/", "title": "Hosting Multiple Sites on One Host With a Caddy Proxy Server \u00b7 Tim Wilson", "content": "\u2193 Skip to main content As I mentioned in my recent article Choosing a CMS , I decided to trim my monthly webhosting expenditures by switching a number of Squarespace websites over to Hugo -generated static sites running inside Docker containers on a small Alpine Linux host at Linode . (That link to Linode is an affilitate link. Please consider checking it out.) When it\u2019s all said and done, the total savings could be as much as $1,000/year. If you\u2019re not familiar with Docker, please read my other recent article Embracing the Docker Lifestyle before continuing. That piece, and the links included in it, introduce the core Docker functionality that is key to the way I\u2019ve designed my hosting setup. Project Goals # None of the websites I was hosting on Squarespace were particularly complex. None required a database backend or advanced scripting. They\u2019re mostly plain vanilla HTML. So the goals were pretty simple. Make the sites fast, the hosting cheap, and the updates easy. The first two were a piece of cake; the third is a matter of opinion and may not be for everyone. Introducing Caddy # If you\u2019re familiar at all with hosting websites in the last 20+ years, you know about Apache . It was the first significant open source HTTP server and was (is?) still the dominant web server on the Internet. But Apache isn\u2019t alone in the web server marketplace. For various projects over the years I\u2019ve used nginx and lighttpd as servers, reverse proxies, and for web caching. All of them are powerful and flexible, but as an occasional programmer and web developer, I\u2019ve often found them to be more complex than I needed. I\u2019m sure if I were a professional sysadmin, configuring and running any of them would quickly become second nature, but that\u2019s not my story. I discovered Caddy somewhat randomly a year or so ago and was intrigued. It\u2019s written in Go and has some compelling features: Caddy is capable of automatically configuring itself with TLS certificates . It even automatically renews the certificates so you never have to worry about your website breaking in modern browsers when a cert expires. (See note above about not being a professional sysadmin.) The Caddy server is a self-contained binary and requires no external libraries to provide its full functionality. Go is arguably more secure becuase of the way it was designed. Configuring the server is incredibly easy with many basic setups capable of being expressed in just a line or two in the config file. Because of its light weight and self-contained design, Caddy is the perfect base on which to build a containerized static website for a low-powered webhost. The Setup # The diagram below shows the basic design of my setup. Figure 1: Configuration of Caddy-powered websites running in Docker on a small Alpine Linux host at Linode. Each website I\u2019m hosting lives in its own Caddy-powered container. There is another Caddy server running in its own container as a reverse proxy for the other containers. The capacity of this configuration is limited only by the capabilities of the webhost. If the $5/month Linode instance I\u2019m using now can\u2019t handle it, upgrading to an instance with more memory and storage is just a few clicks away in the Linode management interface. The only thing I had to do to the Linode host was to install docker. Caddy Configuration # Caddy stores its configuration in a Caddyfile . Here is the Caddyfile for the timwilson.info container: :8000 { root * /srv file_server } This configuration file tells Caddy what to do with HTTP requests that come in on port 8000. (Because only the port is specified in this way, Caddy will not try to obtain TLS certificates.) Caddy will look to the /srv directory as the \u201croot\u201d document for all (\"*\") URIs. The file_server directive puts Caddy is standard HTTP server mode. It would be difficult to express it much more simply than that. This configuration file is copied along with all the HTML, CSS, etc. files into the Docker image when it\u2019s built. The container is based on the caddy:2-alpine docker image which automatically runs the Caddy server process when it starts and loads its configuration from the included Caddyfile . The Caddyfile is exactly the same for every other website that is being hosted on this Linode instance. The configuration for the Caddy proxy looks a bit different. www.timwilson.info { redir https://timwilson.info {uri} } timwilson.info { reverse_proxy timwilson : 8000 } www.rapidsarcheryjoad.org { redir https://rapidsarcheryjoad.org {uri} } rapidsarcheryjoad.org { reverse_proxy rapidsarcheryjoad : 8000 } Notice that instead of specifying a port as before, this Caddyfile uses the full domain name. As a result, Caddy will make a request to the Let\u2019s Encrypt service for proper TLS certificates. This all happens automatically in the background. Notice that there are two sections for each website. The first one is simply a redirect for any incoming URL that has a www in front to the standard domain. In other words, a web browser request for www.timwilson.info/2023/02/blog-post-title/ will be redirected automatically to timwilson.info/2023/02/blog-post-title/ . This is a matter of preference. I find it simpler to redirect rather than mess with the www.* domain. The second section defines the reverse proxy. For example, any HTTP requests for timwilson.info will be passed transparently to port 8000 at the docker container identified as timwilson . The reponse from the timwilson container will be encrypted by the Caddy proxy and sent back to the user\u2019s web browser. Starting and Stopping the Containers # There\u2019s another tool I use to coordinate the starting, stopping, and communication among the various docker containers. It\u2019s called docker-compose and usually requires a separate download. The docker-compose.yml file for this setup follows: version : \"3.7\" services : proxy : image : caddy:2-alpine container_name : proxy restart : unless-stopped ports : - \"80:80\" - \"443:443\" - \"443:443/udp\" volumes : - ./Caddyfile:/etc/caddy/Caddyfile - ./caddy_data:/data - ./caddy_config:/config networks : - caddy timwilson : image : timothydwilson/timwilson.info:latest container_name : timwilson restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./timwilson_caddy_config:/config networks : - caddy rapidsarcheryjoad : image : timothydwilson/rapidsarcheryjoad.org:latest container_name : rapidsarcheryjoad restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./joad_caddy_config:/config networks : - caddy networks : caddy : external : true Notice that there\u2019s a separate section for each container that defines which docker image to use, the name of the container, ports to expose, volumes on the file system where the container can persist data, and the internal network the containers use to communicate. There is a ton of information available on how to create a docker-compose.yml file, so I won\u2019t go any farther here. When I\u2019ve built a new website image (after completing this blog post, for example) and uploaded it to hub.docker.com, I ssh to my webhost and pull the new image like this: $ sudo docker pull timothydwilson/timwilson.info:latest Typing $ sudo docker-compose stop stops all the docker containers, while $ sudo docker-compose up -d starts the containers is \u201cdetached\u201d mode so you get your system prompt back. $ sudo docker container ls will list all the running containers on the system. The final contents of the directory where I store all this looks like: $ tree -l -L 2 . \u251c\u2500\u2500 Caddyfile \u251c\u2500\u2500 caddy_config \u2502 \u2514\u2500\u2500 caddy \u251c\u2500\u2500 caddy_data \u2502 \u2514\u2500\u2500 caddy \u251c\u2500\u2500 docker-compose.yml \u251c\u2500\u2500 joad_caddy_config \u2502 \u2514\u2500\u2500 caddy \u2514\u2500\u2500 timwilson_caddy_config \u2514\u2500\u2500 caddy where you can see the directories that Caddy uses to store the site-specific configuration details (which are autogenerated based on each site\u2019s Caddyfile ), the Caddyfile associated with the Caddy proxy, and the docker-compose.yml file. Expansion # So what would the process look like to add another website to this Linode host? Build another docker container with the website content for the new site and upload it to hub.docker.com so it can be retrieved by the webhost. Redirect DNS to the IP address of my webhost. Add a new section to the docker-compose.yml file for the new site. The specifications for all the websites are almost identical in the file except for the image: , container_name: , and volumes: . Create a volume where caddy can store the configuration for the new website using the command sudo docker volume create <some volume name> . (Docker will stop you the first time you try to start the containers if you neglect this step.) Add entries for the new site to the Caddyfile on the Caddy proxy so the content is reverse proxied correctly. Profit!! Acknowledgments # It look me a while to get this figured out, and I want to shout out the generous members of the Caddy Community forum for their information. francislavoie and Whitestrake were especially detailed in their responses to my questions, and it helped immensely. \u2191", "lists": [["About", "Projects", "Blog", "Archery Resume", ""], ["Caddy is capable of automatically configuring itself with TLS certificates . It even automatically renews the certificates so you never have to worry about your website breaking in modern browsers when a cert expires. (See note above about not being a professional sysadmin.)", "The Caddy server is a self-contained binary and requires no external libraries to provide its full functionality.", "Go is arguably more secure becuase of the way it was designed.", "Configuring the server is incredibly easy with many basic setups capable of being expressed in just a line or two in the config file."], ["Build another docker container with the website content for the new site and upload it to hub.docker.com so it can be retrieved by the webhost.", "Redirect DNS to the IP address of my webhost.", "Add a new section to the docker-compose.yml file for the new site. The specifications for all the websites are almost identical in the file except for the image: , container_name: , and volumes: .", "Create a volume where caddy can store the configuration for the new website using the command sudo docker volume create <some volume name> . (Docker will stop you the first time you try to start the containers if you neglect this step.)", "Add entries for the new site to the Caddyfile on the Caddy proxy so the content is reverse proxied correctly.", "Profit!!"]], "tables": [], "code_blocks": ["Caddyfile", "Caddyfile", ":8000 { root * /srv file_server }", ":8000 { root * /srv file_server }", "/srv", "file_server", "caddy:2-alpine", "Caddyfile", "Caddyfile", "www.timwilson.info { redir https://timwilson.info {uri} } timwilson.info { reverse_proxy timwilson : 8000 } www.rapidsarcheryjoad.org { redir https://rapidsarcheryjoad.org {uri} } rapidsarcheryjoad.org { reverse_proxy rapidsarcheryjoad : 8000 }", "www.timwilson.info { redir https://timwilson.info {uri} } timwilson.info { reverse_proxy timwilson : 8000 } www.rapidsarcheryjoad.org { redir https://rapidsarcheryjoad.org {uri} } rapidsarcheryjoad.org { reverse_proxy rapidsarcheryjoad : 8000 }", "Caddyfile", "www.timwilson.info/2023/02/blog-post-title/", "timwilson.info/2023/02/blog-post-title/", "timwilson", "timwilson", "docker-compose", "docker-compose.yml", "version : \"3.7\" services : proxy : image : caddy:2-alpine container_name : proxy restart : unless-stopped ports : - \"80:80\" - \"443:443\" - \"443:443/udp\" volumes : - ./Caddyfile:/etc/caddy/Caddyfile - ./caddy_data:/data - ./caddy_config:/config networks : - caddy timwilson : image : timothydwilson/timwilson.info:latest container_name : timwilson restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./timwilson_caddy_config:/config networks : - caddy rapidsarcheryjoad : image : timothydwilson/rapidsarcheryjoad.org:latest container_name : rapidsarcheryjoad restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./joad_caddy_config:/config networks : - caddy networks : caddy : external : true", "version : \"3.7\" services : proxy : image : caddy:2-alpine container_name : proxy restart : unless-stopped ports : - \"80:80\" - \"443:443\" - \"443:443/udp\" volumes : - ./Caddyfile:/etc/caddy/Caddyfile - ./caddy_data:/data - ./caddy_config:/config networks : - caddy timwilson : image : timothydwilson/timwilson.info:latest container_name : timwilson restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./timwilson_caddy_config:/config networks : - caddy rapidsarcheryjoad : image : timothydwilson/rapidsarcheryjoad.org:latest container_name : rapidsarcheryjoad restart : unless-stopped depends_on : - proxy expose : - \"8000\" volumes : - ./caddy_data:/data - ./joad_caddy_config:/config networks : - caddy networks : caddy : external : true", "docker-compose.yml", "$ sudo docker pull timothydwilson/timwilson.info:latest", "$ sudo docker-compose stop", "$ sudo docker-compose up -d", "$ sudo docker container ls", "$ tree -l -L 2\n.\n\u251c\u2500\u2500 Caddyfile\n\u251c\u2500\u2500 caddy_config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u251c\u2500\u2500 caddy_data\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u251c\u2500\u2500 docker-compose.yml\n\u251c\u2500\u2500 joad_caddy_config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u2514\u2500\u2500 timwilson_caddy_config\n    \u2514\u2500\u2500 caddy", "$ tree -l -L 2\n.\n\u251c\u2500\u2500 Caddyfile\n\u251c\u2500\u2500 caddy_config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u251c\u2500\u2500 caddy_data\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u251c\u2500\u2500 docker-compose.yml\n\u251c\u2500\u2500 joad_caddy_config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 caddy\n\u2514\u2500\u2500 timwilson_caddy_config\n    \u2514\u2500\u2500 caddy", "Caddyfile", "Caddyfile", "docker-compose.yml", "docker-compose.yml", "image:", "container_name:", "volumes:", "sudo docker volume create <some volume name>", "Caddyfile"], "meta_description": "As I mentioned in my recent article Choosing a CMS, I decided to trim my monthly webhosting expenditures by switching a number of Squarespace websites over to Hugo-generated static sites running inside Docker containers on a small Alpine Linux host at Linode.", "meta_keywords": "caddy, caddy server, linode, web hosting, docker, docker-compose", "og_image": "https://timwilson.info/posts/2023/02/hosting-multiple-sites-on-one-host-with-a-caddy-proxy-server/cover.jpg", "js_rendered": false, "js_message": "", "success": true, "error": "", "truncated": false, "fetched_bytes": 53563, "total_bytes": 53563}}