{"id":2244,"date":"2018-07-10T07:00:50","date_gmt":"2018-07-10T07:00:50","guid":{"rendered":"https:\/\/blog.ssdnodes.com\/blog\/?p=2244"},"modified":"2025-05-18T18:58:24","modified_gmt":"2025-05-18T18:58:24","slug":"using-docker-compose-self-hosting","status":"publish","type":"post","link":"https:\/\/www.ssdnodes.com\/blog\/using-docker-compose-self-hosting\/","title":{"rendered":"Using docker-compose to add web apps: The self-hosting handbook"},"content":{"rendered":"<p>Welcome to the third page of a handbook on self-hosting. Begin <a href=\"https:\/\/www.ssdnodes.com\/blog\/self-hosting-handbook\/\">here<\/a>. On this page, we'll be using docker-compose to add web apps and containers to our self-hosting stack.<\/p>\n<hr \/>\n<h2 id=\"topics-covered-on-this-page\">Topics covered on this page<\/h2>\n<ol>\n<li>A recap of environment variables and ports<\/li>\n<li>Example: Adding Gitea to your self-hosting stack<\/li>\n<li>Example: Adding FreshRSS to your self-hosting stack<\/li>\n<li>How does this actually work?<\/li>\n<\/ol>\n<div class=\"cta-inline\"><\/div>\n<h2 id=\"a-recap-of-environment-variables-and-ports\">A recap of environment variables and ports<\/h2>\n<p>On the previous page of this self-hosting handbook, we walked through your <code>docker-compose.yml<\/code> file and I tried to explain how to specify different environment variables that will both allow our orchestration containers to grab SSL certificates from Let's Encrypt, and allow all the containers to communicate with each other as needed.<\/p>\n<p>I have a feeling some of these variables and configurations might still be a little confusing, so let's take a moment to recap the important ones and how to set them up correctly in the future. Throughout the remainder of this handbook, I will include <code>docker-compose<\/code> service blocks that I've tested to work with this self-hosting stack, but there are likely other containers you will want to add to your stack, and you'll only be able to do that if you understand how these environment variables work.<\/p>\n<p>As I mentioned on the <a href=\"https:\/\/www.ssdnodes.com\/blog\/self-hosting-handbook-a-docker-compose-tutorial\/\">previous page<\/a>, there are three critical environment variables: <code>VIRTUAL_HOST<\/code>, <code>LETSENCRYPT_HOST<\/code>, and <code>LETSENCRYPT_EMAIL<\/code>. Let me quote myself from that page.<\/p>\n<p><em>There are three essential environment variables to set here: the <code>VIRTUAL_HOST<\/code>, the <code>LETSENCRYPT_HOST<\/code>, and the <code>LETSENCRYPT_EMAIL<\/code>.<\/em><\/p>\n<p><em>The <code>LETSENCRYPT_EMAIL<\/code> variable is simplest to explain: Replace the <code>EMAIL<\/code> variable with your email.<\/em><\/p>\n<p><em>The <code>VIRTUAL_HOST<\/code> and <code>LETSENCRYPT_HOST<\/code> both reference the domain at which you'll access the Portainer service using your browser. Both these variables <strong>must<\/strong> be set on <strong>every<\/strong> Docker container you want to access via the reverse proxy.<\/em><\/p>\n<p>There are a few other environment variables that you may need to employ.<\/p>\n<h3 id=\"virtual_port\">VIRTUAL_PORT<\/h3>\n<p>To get started, here's what the <code>nginx-proxy<\/code> documentation says about <code>VIRTUAL_PORT<\/code>:<\/p>\n<pre>If your container exposes multiple ports, nginx-proxy will default to the service running on port 80. If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one. If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected.\n<\/pre>\n<p>By default, <code>nginx-proxy<\/code> will assume that your container will be listening for traffic on port 80. If the <code>ports<\/code> section in the <code>docker-compose<\/code> configuration reads something like <code>- &quot;667:80&quot;<\/code>, then you don't need to use <code>VIRTUAL_PORT<\/code>. In that case, the Docker containing will be listening to traffic on port 80, and <code>nginx-proxy<\/code> will take care of the rest.<\/p>\n<p>But, what if the <code>ports<\/code> section was something like <code>- &quot;3000:5000<\/code>? This means that the Docker container is listening for traffic on port <code>5000<\/code>. You must use <code>VIRTUAL_PORT<\/code> in this case to force <code>nginx-proxy<\/code> to send traffic to the correct port, and not the <code>80<\/code> default.<\/p>\n<p>If this doesn't quite make sense yet, we'll see this <code>VIRTUAL_PORT<\/code> variable in action when adding Gitea to our stack in the next step.<\/p>\n<h2 id=\"example-adding-gitea-to-your-self-hosting-stack\">Example: Adding Gitea to your self-hosting stack using docker-compose<\/h2>\n<p>Your Portainer container should be working just fine, but you didn't get all this way to have a Docker monitoring solution and no Docker containers! Let's get started with a popular choice for self-hosting: an alternative to hosted (and often paid) Git hosting services like GitHub.<\/p>\n<p>By adding Gitea to your self-hosting stack, you'll be able to host your own version-controlled software, whether you keep it private or share it with the world!<\/p>\n<h3 id=\"add-a-new-dns-record\">Add a new DNS record<\/h3>\n<p>Gitea will require its own subdomain, just as Portainer does at <code>docker.DOMAIN.TLD<\/code>. Head back into your DNS configuration and create a new A record at the subdomain of your choosing. I tend to pick <code>gitea<\/code>, but it's entirely up to you.<\/p>\n<h3 id=\"add-to-the-docker-composeyml-file\">Add to the docker-compose.yml file<\/h3>\n<p>Let's get right into it. Here's the entire block to add to the <code>services<\/code> section of your <code>docker-compose.yml<\/code> file.<\/p>\n<pre><code>  gitea:\n    image: gitea\/gitea:latest\n    container_name: gitea\n    restart: unless-stopped\n    environment:\n      - VIRTUAL_HOST=gitea.DOMAIN.TLD\n      - LETSENCRYPT_HOST=gitea.DOMAIN.TLD\n      - LETSENCRYPT_EMAIL=EMAIL\n      - VIRTUAL_PORT=3000\n      - ROOT_URL=https:\/\/gitea.DOMAIN.TLD\n      - DOMAIN=gitea.DOMAIN.TLD\n      - PROTOCOL=http\n      - USER_UID=1000\n      - USER_GID=1000\n    volumes:\n      - .\/gitea:\/data\n    ports:\n      - \"5000:3000\"\n      - \"222:22\"\n    networks:\n      - proxy-tier\n      - default<\/code><\/pre>\n<p>At first glance, this configuration is far more complicated than Portainer. While that may be true to an extent, much of this should be familiar to you. The first three environment variables are expected, and you can see the <code>VIRTUAL_PORT<\/code> variable as well. We're telling <code>nginx-proxy<\/code> that Gitea will be listening for traffic on port 3000, which you can see clarified down in the <code>ports<\/code> section as well: `- &quot;5000:3000&quot;.<\/p>\n<p>The remainder of the environment variables\u2014<code>ROOT_URL<\/code>, <code>DOMAIN<\/code>, <code>PROTOCOL<\/code>, <code>USER_UID<\/code>, and <code>USER_GID<\/code>\u2014are Gitea-specific. Instead of informing <code>nginx-proxy<\/code>, these variables tell Gitea how it should run. Simply the other <code>DOMAIN<\/code> and <code>TLD<\/code> variables with your domain and TLD, and leave the rest as-is.<\/p>\n<h3 id=\"run-docker-compose-up--d\">Run docker-compose up -d<\/h3>\n<p>Whenever you add to or otherwise change up your <code>docker-compose.yml<\/code> file, all you need to do to deploy these changes is run <code>docker-compose up -d<\/code> again. You should see output similar to the following:<\/p>\n<pre><code>$ docker-compose up -d\nPulling gitea (gitea\/gitea:latest)...\nlatest: Pulling from gitea\/gitea\n911c6d0c7995: Pull complete\nf36b1d28f2ac: Pull complete\nd73c2cfa601e: Pull complete\n744db10b7a3b: Pull complete\n03cb1114429b: Pull complete\na28e30c2301a: Pull complete\nDigest: sha256:464544cf5a7e8adf1430002a6b38076d5b5be2563363c3227144b2ae68b9a7e3\nStatus: Downloaded newer image for gitea\/gitea:latest\nCreating gitea ...\nproxy is up-to-date\nportainer is up-to-date\nCreating gitea ... done<\/code><\/pre>\n<p>Hop over to your browser and navigate to the URL you specified via the DNS record and in your <code>docker-compose.yml<\/code> file. You'll see a fully-encrypted, self-hosted Gitea installation waiting for you! You can create your user and get started right away.<\/p>\n<h2 id=\"example-adding-freshrss-to-your-self-hosting-stack\">Example: Adding FreshRSS to your self-hosting stack using docker-compose<\/h2>\n<p>Another widespread use of self-hosting is an RSS reader. I've covered this topic <a href=\"https:\/\/www.ssdnodes.com\/blog\/three-ways-to-hop-aboard-the-rss-revival-with-self-hosting\/\">in the past<\/a>, and have since figured out an effortless way to add FreshRSS to this stack.<\/p>\n<p>As with the previous example, start by adding a new A record to your DNS. Then, add the following to your <code>docker-compose.yml<\/code> file:<\/p>\n<pre><code>  freshrss:\n    image: linuxserver\/freshrss\n    container_name: freshrss\n    restart: unless-stopped\n    environment:\n      - VIRTUAL_HOST=feed.DOMAIN.TLD\n      - LETSENCRYPT_HOST=feed.DOMAIN.TLD\n      - LETSENCRYPT_EMAIL=EMAIL@DOMAIN.TLD\n      - PGID=1001\n      - PUID=1000\n    volumes:\n      - .\/freshrss:\/config\n    ports:\n      - \"667:80\"\n    networks:\n      - proxy-tier\n      - default<\/code><\/pre>\n<p>Run <code>docker-compose up -d<\/code> again to refresh your self-hosting stack.<\/p>\n<pre><code>$ docker-compose up -d\nPulling freshrss (linuxserver\/freshrss:)...\nlatest: Pulling from linuxserver\/freshrss\n94c34ef7d9ee: Pull complete\ne169d46472b9: Pull complete\n6f85777a9276: Pull complete\n6359afc496ca: Pull complete\n945860b71d75: Pull complete\ndb1f674ef8a6: Pull complete\n6e560e43af06: Pull complete\nDigest: sha256:7be62e52282273f672f28dec26979ac64f53a4d14b244c67875b2c7bb8010278\nStatus: Downloaded newer image for linuxserver\/freshrss:latest\ngitea is up-to-date\nCreating freshrss ...\nproxy is up-to-date\nportainer is up-to-date\nCreating freshrss ... done<\/code><\/pre>\n<p>Head over to the URL you specified, and a new FreshRSS installation will be waiting for your favorite feeds!<\/p>\n<h2 id=\"how-does-this-actually-work\">How does this actually work?<\/h2>\n<p>Now that you have seen some examples of how to add more Docker containers to this self-hosting stack, let's take a moment to examine precisely <em>how<\/em> this system works together. Here's the process, simplified into a handful of steps:<\/p>\n<ol>\n<li>You add a new service to <code>docker-compose.yml<\/code> file.<\/li>\n<li>You then run <code>docker-compose up -d<\/code>.<\/li>\n<li>The new Docker container(s) are downloaded and started.<\/li>\n<li>A service called <code>docker-gen<\/code>, running inside of <code>nginx-proxy<\/code>, recognizes that a new Docker container has started via the Docker sockets file (<code>\/var\/run\/docker.sock<\/code>). Because this container has the <code>VIRTUAL_HOST<\/code> environment variable set, <code>nginx-proxy<\/code> knows it should initialize it as part of the reverse proxy network.<\/li>\n<li>The <code>nginx-proxy<\/code> container creates new reverse proxy Nginx rules to route traffic coming into the VPS to the appropriate place.<\/li>\n<li>The <code>letsencrypt-nginx-proxy-companion<\/code> also recognizes that a new Docker container has started via Docker sockets, and because it has the <code>LETSENCRYPT_HOST<\/code> environment variable set to a real domain, <code>letsencrypt-nginx-proxy-companion<\/code> knows to request a new SSL certificate from Let's Encrypt.<\/li>\n<li><code>letsencrypt-nginx-proxy-companion<\/code> and <code>nginx-proxy<\/code> coordinate to install the new SSL certificate and enable HTTPS traffic.<\/li>\n<li><code>letsencrypt-nginx-proxy-companion<\/code> checks the existing SSL certificates every 3600 seconds (every 60 minutes) to verify if any are expiring soon. If so, it will register the SSL certificate again, ensuring traffic is always encrypted.<\/li>\n<\/ol>\n<p>The underlying functionality is far, far more complicated than this numbered list suggests, but the beauty of this setup is that we don't have to worry about it. The developers have abstracted much of the headache and complexity away from us, giving us full control of our self-hosting stack while making it relatively easy to use.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Quickly add web apps to your self-hosting Docker stack using docker-compose. Start with Gitea and FreshRSS and get the tools to self-host almost anything.<\/p>\n","protected":false},"author":20,"featured_media":2955,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[18],"tags":[182],"class_list":["post-2244","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-docker"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/2244","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=2244"}],"version-history":[{"count":3,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/2244\/revisions"}],"predecessor-version":[{"id":13037,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/2244\/revisions\/13037"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media\/2955"}],"wp:attachment":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media?parent=2244"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/categories?post=2244"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/tags?post=2244"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}