{"id":1271,"date":"2017-11-22T06:27:23","date_gmt":"2017-11-22T06:27:23","guid":{"rendered":"https:\/\/blog.ssdnodes.com\/blog\/?p=1271"},"modified":"2025-05-18T19:41:21","modified_gmt":"2025-05-18T19:41:21","slug":"host-multiple-ssl-websites-docker-nginx","status":"publish","type":"post","link":"https:\/\/www.ssdnodes.com\/blog\/host-multiple-ssl-websites-docker-nginx\/","title":{"rendered":"Hosting multiple SSL-enabled sites with Docker and Nginx"},"content":{"rendered":"<p>In one of our most popular tutorials\u2014<a href=\"https:\/\/www.ssdnodes.com\/blog\/host-multiple-websites-docker-nginx\/\" target=\"_blank\" rel=\"noopener\">Host multiple websites on one VPS with Docker and Nginx<\/a>\u2014I covered how you can use the <code>nginx-proxy<\/code> Docker container to host multiple websites or web apps on a single VPS using different containers.<\/p>\n<p>As I was looking to enable HTTPS on some of my self-hosted services recently, I thought it was about time to take that tutorial a step further and show you how to set it up to request <a href=\"https:\/\/letsencrypt.org\/\" target=\"_blank\" rel=\"noopener\">Let's Encrypt<\/a> HTTPS certificates.<\/p>\n<p>With the help of <code>docker-letsencrypt-nginx-proxy-companion<\/code> (<a href=\"https:\/\/github.com\/JrCs\/docker-letsencrypt-nginx-proxy-companion\" target=\"_blank\" rel=\"noopener\">Github<\/a>), we'll be able to have SSL automatically enabled on any new website or app we deploy with Docker containers.<\/p>\n<h2>Prerequisites<\/h2>\n<ul>\n<li>Any of our OS options\u2014Ubuntu, Debian, or CentOS. Just a note: we've only tested Ubuntu 16.04 as of now.<\/li>\n<li>A running Docker installation, plus <code>docker-compose<\/code>\u2014see <a href=\"https:\/\/www.ssdnodes.com\/blog\/tutorial-getting-started-with-docker-on-your-vps\/\">our <em>Getting Started with Docker<\/em> guide<\/a> for more information.<\/li>\n<\/ul>\n<div class=\"cta-inline\"><\/div>\n<h2>Step 1. Getting set up, and a quick note<\/h2>\n<p>I'm a rather big fan of using <code>docker-compose<\/code> whenever possible, as it seems to simplify troubleshooting containers that are giving you trouble. Instead of parsing a long terminal command, you can simply edit the <code>docker-compose.yml<\/code> file, re-run <code>docker-compose up -d<\/code>, and see the results.<\/p>\n<p>As a result, this tutorial will be heavily biased toward using <code>docker-compose<\/code> over <code>docker<\/code> commands, particularly when it comes to setting up the <code>docker-letsencrypt-nginx-proxy-companion<\/code> service.<\/p>\n<p>If you're interested creating these containers via <code>docker<\/code> commands, check out the <a href=\"https:\/\/github.com\/JrCs\/docker-letsencrypt-nginx-proxy-companion#separate-containers-recommended-method\" target=\"_blank\" rel=\"noopener\">docker-letsencrypt-nginx-proxy-companion documentation<\/a>.<\/p>\n<p><em>Already have <code>nginx-proxy<\/code> set up via our previous tutorial? You can simply overwrite your existing <code>docker-compose.yml<\/code> file with the file you'll find in Step 2.<\/em><\/p>\n<p>If you're just getting started with <code>nginx-proxy<\/code>, start here. You'll want to start by creating a separate directory to contain your <code>docker-compose.yml<\/code> file.<\/p>\n<pre><code>$ mkdir nginx-proxy &amp;&amp; cd nginx-proxy\n<\/code><\/pre>\n<p>Once that's finished, issue the following command to create a unique network for <code>nginx-proxy<\/code> and other Docker containers to communicate through.<\/p>\n<pre><code>docker network create nginx-proxy\n<\/code><\/pre>\n<h2>Step 2. Creating the docker-compose.yml file<\/h2>\n<p>In the <code>nginx-proxy<\/code> directory, create a new file named <code>docker-compose.yml<\/code> and paste in the following text:<\/p>\n<pre><code>version: '3'\n\nservices:\n  nginx:\n    image: nginx:1.13.1\n    container_name: nginx-proxy\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - conf:\/etc\/nginx\/conf.d\n      - vhost:\/etc\/nginx\/vhost.d\n      - html:\/usr\/share\/nginx\/html\n      - certs:\/etc\/nginx\/certs\n    labels:\n      - \"com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true\"\n\n  dockergen:\n    image: jwilder\/docker-gen:0.7.3\n    container_name: nginx-proxy-gen\n    depends_on:\n      - nginx\n    command: -notify-sighup nginx-proxy -watch -wait 5s:30s \/etc\/docker-gen\/templates\/nginx.tmpl \/etc\/nginx\/conf.d\/default.conf\n    volumes:\n      - conf:\/etc\/nginx\/conf.d\n      - vhost:\/etc\/nginx\/vhost.d\n      - html:\/usr\/share\/nginx\/html\n      - certs:\/etc\/nginx\/certs\n      - \/var\/run\/docker.sock:\/tmp\/docker.sock:ro\n      - .\/nginx.tmpl:\/etc\/docker-gen\/templates\/nginx.tmpl:ro\n\n  letsencrypt:\n    image: jrcs\/letsencrypt-nginx-proxy-companion\n    container_name: nginx-proxy-le\n    depends_on:\n      - nginx\n      - dockergen\n    environment:\n      NGINX_PROXY_CONTAINER: nginx-proxy\n      NGINX_DOCKER_GEN_CONTAINER: nginx-proxy-gen\n    volumes:\n      - conf:\/etc\/nginx\/conf.d\n      - vhost:\/etc\/nginx\/vhost.d\n      - html:\/usr\/share\/nginx\/html\n      - certs:\/etc\/nginx\/certs\n      - \/var\/run\/docker.sock:\/var\/run\/docker.sock:ro\n\nvolumes:\n  conf:\n  vhost:\n  html:\n  certs:\n\n# Do not forget to 'docker network create nginx-proxy' before launch, and to add '--network nginx-proxy' to proxied containers. \n\nnetworks:\n  default:\n    external:\n      name: nginx-proxy\n<\/code><\/pre>\n<p>Special thanks to Github user <a href=\"https:\/\/github.com\/buchdag\" target=\"_blank\" rel=\"noopener\">buchdag<\/a> for this all-in-one configuration that works out of the box (most of the time).<\/p>\n<p>After you copy, make sure that the formatting looks the same\u2014the <code>yml<\/code> parser isn't kind, let's say, to syntax errors.<\/p>\n<p>Now, take a closer look at line 30, which contains <code>- .\/nginx.tmpl:\/etc\/docker-gen\/templates\/nginx.tmpl:ro<\/code>. This line creates a Docker volume between a file on your host filesystem (in this case, in the <code>nginx-proxy<\/code> directory) and a file inside one of the Docker containers. For that volume to work, we need to supply the <code>nginx.tmpl<\/code> file.<\/p>\n<p>Inside of the <code>nginx-proxy<\/code> directory, use the following <code>curl<\/code> command to copy the developer's sample <code>nginx.tmpl<\/code> file to your VPS.<\/p>\n<pre><code>$ curl https:\/\/raw.githubusercontent.com\/jwilder\/nginx-proxy\/master\/nginx.tmpl &gt; nginx.tmpl\n<\/code><\/pre>\n<h2>Step 3. Running nginx-proxy for the first time.<\/h2>\n<p>If you have an <code>nginx-proxy<\/code> container running already from the previous tutorial, you'll need to stop it before moving forward.<\/p>\n<p>You can now run the <code>docker-compose<\/code> command that will kick this all off.<\/p>\n<pre><code>$ docker-compose up -d\n<\/code><\/pre>\n<p>The process will start by downloading a few Docker images, and if things finish successfully, the output will end with the following:<\/p>\n<pre><code>Creating nginx-proxy ...\nCreating nginx-proxy ... done\nCreating nginx-proxy-gen ...\nCreating nginx-proxy-gen ... done\nCreating nginx-proxy-le ...\nCreating nginx-proxy-le ... done\n<\/code><\/pre>\n<p>To confirm this, run <code>docker ps<\/code>. You should see three running containers, named <code>nginx-proxy<\/code>, <code>nginx-proxy-gen<\/code>, and <code>nginx-proxy-le<\/code>, like this:<\/p>\n<pre><code>CONTAINER ID        IMAGE                                    COMMAND                  CREATED             STATUS              PORTS                                      NAMES\n9ea5fffc24dd        jrcs\/letsencrypt-nginx-proxy-companion   \"\/bin\/bash \/app\/en...\"   4 minutes ago       Up 4 minutes                                                   nginx-proxy-le\ne2288dfc3c5c        jwilder\/docker-gen:0.7.3                 \"\/usr\/local\/bin\/do...\"   4 minutes ago       Up 3 seconds                                                   nginx-proxy-gen\neda8f12bd829        nginx:1.13.1                             \"nginx -g 'daemon ...\"   4 minutes ago       Up 4 minutes        0.0.0.0:80-&gt;80\/tcp, 0.0.0.0:443-&gt;443\/tcp   nginx-proxy\n<\/code><\/pre>\n<p>If any of those aren't running, you should check their logs. You can do that with <code>docker logs<\/code>.<\/p>\n<pre><code>$ docker logs nginx-proxy\n$ docker logs nginx-proxy-gen\n$ docker logs nginx-proxy-le\n<\/code><\/pre>\n<p>I've found that most issues arise from <code>nginx-proxy-gen<\/code>. If you see an error about the <code>nginx-proxy<\/code> network, try creating the network again with <code>docker network create nginx-proxy<\/code>. If there are issues with the <code>nginx.tmpl<\/code> file, double check that it's the same as the <a href=\"https:\/\/raw.githubusercontent.com\/jwilder\/nginx-proxy\/master\/nginx.tmpl\" target=\"_blank\" rel=\"noopener\">one on Github<\/a>.<\/p>\n<hr \/>\n<p>If your three containers are running smoothly, then you're ready to start deploying other SSL-enabled containers behind the proxy! At this point, we're shifting away from configuring <code>nginx-proxy<\/code> and toward the ways, you should configure your apps to run behind it.<\/p>\n<h2>Step 4. Set up your DNS<\/h2>\n<p>Let's Encrypt will only issue certificates to real domains, not IPs. In order to receive valid certificates, you <em>must<\/em> set up your DNS correctly. Most likely, you will find your DNS settings with the company from which you bought your domain.<\/p>\n<p>Let's say I want to run <a href=\"https:\/\/github.com\/miniflux\/miniflux\" target=\"_blank\" rel=\"noopener\">Miniflux<\/a>, a self-hosted RSS reader, on the <code>feeds.example.com<\/code> domain. In my DNS settings, I would create a new A record that directs <code>feeds.example.com<\/code> to the public IP of the VPS where I set up my <code>nginx-proxy<\/code>.<\/p>\n<p><em>This is not an optional step<\/em>. Valid, reachable domains are required for SSL to work.<\/p>\n<p>Quick tip: to find your VPSs' public IP, use the following: <code>dig +short myip.opendns.com @resolver1.opendns.com<\/code>.<\/p>\n<h2>Step 5. The configuration basics<\/h2>\n<p>In order to set up any containerized app to work with this beautiful proxy we've now set up, you <em>must<\/em> configure the following:<\/p>\n<ul>\n<li>Three environment variables: <code>VIRTUAL_HOST<\/code>, <code>LETSENCRYPT_HOST<\/code>, <code>LETSENCRYPT_EMAIL<\/code><\/li>\n<li>The Docker network (nginx-proxy)<\/li>\n<li>Exposing port 80\/443<\/li>\n<\/ul>\n<h3>Environment variables<\/h3>\n<p>Here are the environment variables:<\/p>\n<pre><code>VIRTUAL_HOST\nLETSENCRYPT_HOST\nLETSENCRYPT_EMAIL\n<\/code><\/pre>\n<p>The <code>VIRTUAL_HOST<\/code> and <code>LETSENCRYPT_HOST<\/code> variables will be <em>the same<\/em> for almost all applications, and will correspond to the domain you used in the previous step to set up DNS.<\/p>\n<p>The <code>LETSENCRYPT_EMAIL<\/code> variable is self-explanatory: use the email address of your choosing.<\/p>\n<p>Let's extend our example from the last step. Here's the <code>docker-compose<\/code> settings I would use for that Miniflux installation, given that my email is <a href=\"mailto:code&gt;foo@example.com&lt;\/code\">code>foo@example.com<\/code<\/a>:<\/p>\n<pre><code>environment:\n    VIRTUAL_HOST: feeds.example.com\n    LETSENCRYPT_HOST: feeds.example.com\n    LETSENCRYPT_EMAIL: foo@example.com\n<\/code><\/pre>\n<h3>Docker network<\/h3>\n<p>This will be <code>nginx-proxy<\/code>, unless you changed it. Here's how it looks in a <code>docker-compose.yml<\/code> file:<\/p>\n<pre><code>networks:\n  default:\n    external:\n      name: nginx-proxy\n<\/code><\/pre>\n<h3>Expose port(s)<\/h3>\n<p>Any container you create must expose the port on which it listens to traffic. That will be 80, 443, or both. Here's how it looks in a <code>docker-compose.yml<\/code> file:<\/p>\n<pre><code>expose:\n  - 80\n<\/code><\/pre>\n<h3>A example configuration<\/h3>\n<p>And here's how all those different variables and configurations look like in a very basic <code>docker-compose.yml<\/code> file.<\/p>\n<pre><code>version: '3'\n\nservices:\n  example-app:\n    image: example\/example-app\n    expose:\n      - 80\n    environment:\n      VIRTUAL_HOST: app.example.com\n      LETSENCRYPT_HOST: app.example.com\n      LETSENCRYPT_EMAIL: foo@example.com\n\nnetworks:\n    default:\n        external:\n            name: nginx-proxy\n<\/code><\/pre>\n<hr \/>\n<p>At this point, you should have everything you need to know to deploy all kinds of Docker containers under this SSL-enabled proxy. Let's look at a few examples to show you how it works in the real world.<\/p>\n<h2>Leading by example: Miniflux<\/h2>\n<p>Remember how I wanted to self-host Miniflux at the <code>feeds.example.com<\/code> domain? Here's the <code>docker-compose.yml<\/code> that I came up with. Note the <code>expose<\/code>, <code>environment<\/code>, and <code>networks<\/code> configurations.<\/p>\n<pre><code>version: '2'\n\nservices:\n  miniflux:\n    image: miniflux\/miniflux:latest\n    expose:\n      - 80\n    volumes:\n      - miniflux_data:\/var\/www\/html\/data\n    environment:\n      VIRTUAL_HOST: feeds.example.com\n      LETSENCRYPT_HOST: feeds.example.com\n      LETSENCRYPT_EMAIL: foo@example.com\n\nvolumes:\n  miniflux_data:\n        driver: local\n\nnetworks:\n    default:\n        external:\n            name: nginx-proxy\n<\/code><\/pre>\n<p>Initialize the container with <code>docker-compose up -d<\/code> and you'll have an SSL-enabled feed reader in about 30 seconds!<\/p>\n<h2>Leading by example, take 2: WordPress<\/h2>\n<p>In my <a href=\"https:\/\/www.ssdnodes.com\/blog\/tutorial-using-docker-and-nginx-to-host-multiple-websites\/\">previous tutorial on <code>nginx-proxy<\/code><\/a>, I showed off how easy it is to launch a WordPress instance running behind the proxy.<\/p>\n<p>It's just as easy to do it with SSL. I took the <em>exact same<\/em> <code>docker-compose.yml<\/code> file and simply added the <code>LETSENCRYPT_HOST<\/code> and <code>LETSENCRYPT_EMAIL<\/code> environment variables. You have to change those two fields according to your needs, in addition to <code>VIRTUAL_HOST<\/code>.<\/p>\n<p>Take note of the <code>db_node_domain<\/code> field below. This is where you specify the name of the database that WordPress will connect to. Keep in mind that these names should be <em>unique<\/em> for each instance of WordPress, and you need to also change the <code>depends_on:<\/code> and <code>WORDPRESS_DB_HOST:<\/code> fields accordingly.<\/p>\n<pre><code>version: \"3\"\n\nservices:\n   db_node_domain:\n     image: mysql:5.7\n     volumes:\n        - db_data:\/var\/lib\/mysql\n     restart: always\n     environment:\n        MYSQL_ROOT_PASSWORD: somewordpress\n        MYSQL_DATABASE: wordpress\n        MYSQL_USER: wordpress\n        MYSQL_PASSWORD: wordpress\n     container_name: wp_test_db\n\n   wordpress:\n     depends_on:\n        - db_node_domain\n     image: wordpress:latest\n     expose:\n        - 80\n     restart: always\n     environment:\n        VIRTUAL_HOST: blog.example.com\n        LETSENCRYPT_HOST: blog.example.com\n        LETSENCRYPT_EMAIL: foo@example.com\n        WORDPRESS_DB_HOST: db_node_domain:3306\n        WORDPRESS_DB_USER: wordpress\n        WORDPRESS_DB_PASSWORD: wordpress\n     container_name: wp_test\nvolumes:\n  db_data:\n\nnetworks:\n  default:\n    external:\n      name: nginx-proxy\n<\/code><\/pre>\n<p>Run it with <code>docker-compose up -d<\/code> and then visit your domain\u2014there's that beautiful WordPress installation page <em>with<\/em> SSL encryption.<\/p>\n<p>Sweet.<\/p>\n<h2>Wrapping up<\/h2>\n<p>This has been a pretty extensive tutorial, so I understand if you're feeling a little overwhelmed. My goal here isn't to guide you toward any specific end goal, but rather to give you the tools to leverage the fantastic <code>nginx-proxy<\/code> project to your unique needs.<\/p>\n<p>If you keep the required settings in mind, you should be able to put almost any containerized app behind this proxy. And that gives you a ton of power to get the most out of your VPS.<\/p>\n<p>Best of all, <code>docker-letsencrypt-nginx-proxy-companion<\/code> will automatically renew your certificates for you, so there's no need to check in or do any manual interventions.<\/p>\n<p>As far as I can tell, this is <em>the best<\/em> way to serve many, if not dozens of SSL-encrypted websites and apps via a single proxy and a single VPS.<\/p>\n<p>[cta text=&quot;Run Docker for $1.11\/mo and get 16GB RAM!&quot; text2=&quot;You're 90 seconds away from running Docker with multiple websites on SSD Nodes!&quot; button=&quot;Start Dockerizing your apps&quot;]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It&#8217;s the ultimate combination: using Docker and Nginx to host SSL-protected multiple sites on a single VPS. This tutorial will show you how.<\/p>\n","protected":false},"author":20,"featured_media":2133,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[18,30],"tags":[182,211],"class_list":["post-1271","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-tutorials","tag-docker","tag-nginx"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/1271","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/comments?post=1271"}],"version-history":[{"count":3,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/1271\/revisions"}],"predecessor-version":[{"id":13065,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/1271\/revisions\/13065"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media\/2133"}],"wp:attachment":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media?parent=1271"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/categories?post=1271"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/tags?post=1271"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}