Using docker-compose to add web apps: The self-hosting handbook
Welcome to the third page of a handbook on self-hosting. Begin here. On this page, we’ll be using docker-compose to add web apps and containers to our self-hosting stack.
Topics covered on this page
- A recap of environment variables and ports
- Example: Adding Gitea to your self-hosting stack
- Example: Adding FreshRSS to your self-hosting stack
- How does this actually work?
Save 90% on cloud hosting vs Digital Ocean, Vultr & Linode!
Kick off the new year with high-performance cloud hosting for unheard of prices. Get 32GB RAM + 320GB of high-speed SSD storage starting at just $99 per year!
A recap of environment variables and ports
On the previous page of this self-hosting handbook, we walked through your
docker-compose.yml 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.
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
docker-compose 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.
As I mentioned on the previous page, there are three critical environment variables:
LETSENCRYPT_EMAIL. Let me quote myself from that page.
There are three essential environment variables to set here: the
LETSENCRYPT_HOST, and the
LETSENCRYPT_EMAIL variable is simplest to explain: Replace the
LETSENCRYPT_HOST both reference the domain at which you’ll access the Portainer service using your browser. Both these variables must be set on every Docker container you want to access via the reverse proxy.
There are a few other environment variables that you may need to employ.
To get started, here’s what the
nginx-proxy documentation says about
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.
nginx-proxy will assume that your container will be listening for traffic on port 80. If the
ports section in the
docker-compose configuration reads something like
- "667:80", then you don’t need to use
VIRTUAL_PORT. In that case, the Docker containing will be listening to traffic on port 80, and
nginx-proxy will take care of the rest.
But, what if the
ports section was something like
- "3000:5000? This means that the Docker container is listening for traffic on port
5000. You must use
VIRTUAL_PORT in this case to force
nginx-proxy to send traffic to the correct port, and not the
If this doesn’t quite make sense yet, we’ll see this
VIRTUAL_PORT variable in action when adding Gitea to our stack in the next step.
Example: Adding Gitea to your self-hosting stack using docker-compose
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.
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!
Add a new DNS record
Gitea will require its own subdomain, just as Portainer does at
docker.DOMAIN.TLD. Head back into your DNS configuration and create a new A record at the subdomain of your choosing. I tend to pick
gitea, but it’s entirely up to you.
Add to the docker-compose.yml file
Let’s get right into it. Here’s the entire block to add to the
services section of your
gitea: image: gitea/gitea:latest container_name: gitea restart: unless-stopped environment: - VIRTUAL_HOST=gitea.DOMAIN.TLD - LETSENCRYPT_HOST=gitea.DOMAIN.TLD - LETSENCRYPT_EMAIL=EMAIL - VIRTUAL_PORT=3000 - ROOT_URL=https://gitea.DOMAIN.TLD - DOMAIN=gitea.DOMAIN.TLD - PROTOCOL=http - USER_UID=1000 - USER_GID=1000 volumes: - ./gitea:/data ports: - "5000:3000" - "222:22" networks: - proxy-tier - default
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
VIRTUAL_PORT variable as well. We’re telling
nginx-proxy that Gitea will be listening for traffic on port 3000, which you can see clarified down in the
ports section as well: `- “5000:3000”.
The remainder of the environment variables—
USER_GID—are Gitea-specific. Instead of informing
nginx-proxy, these variables tell Gitea how it should run. Simply the other
TLD variables with your domain and TLD, and leave the rest as-is.
Run docker-compose up -d
Whenever you add to or otherwise change up your
docker-compose.yml file, all you need to do to deploy these changes is run
docker-compose up -d again. You should see output similar to the following:
$ docker-compose up -d Pulling gitea (gitea/gitea:latest)... latest: Pulling from gitea/gitea 911c6d0c7995: Pull complete f36b1d28f2ac: Pull complete d73c2cfa601e: Pull complete 744db10b7a3b: Pull complete 03cb1114429b: Pull complete a28e30c2301a: Pull complete Digest: sha256:464544cf5a7e8adf1430002a6b38076d5b5be2563363c3227144b2ae68b9a7e3 Status: Downloaded newer image for gitea/gitea:latest Creating gitea ... proxy is up-to-date portainer is up-to-date Creating gitea ... done
Hop over to your browser and navigate to the URL you specified via the DNS record and in your
docker-compose.yml file. You’ll see a fully-encrypted, self-hosted Gitea installation waiting for you! You can create your user and get started right away.
Example: Adding FreshRSS to your self-hosting stack using docker-compose
Another widespread use of self-hosting is an RSS reader. I’ve covered this topic in the past, and have since figured out an effortless way to add FreshRSS to this stack.
As with the previous example, start by adding a new A record to your DNS. Then, add the following to your
freshrss: image: linuxserver/freshrss container_name: freshrss restart: unless-stopped environment: - VIRTUAL_HOST=feed.DOMAIN.TLD - LETSENCRYPT_HOST=feed.DOMAIN.TLD - [email protected] - PGID=1001 - PUID=1000 volumes: - ./freshrss:/config ports: - "667:80" networks: - proxy-tier - default
docker-compose up -d again to refresh your self-hosting stack.
$ docker-compose up -d Pulling freshrss (linuxserver/freshrss:)... latest: Pulling from linuxserver/freshrss 94c34ef7d9ee: Pull complete e169d46472b9: Pull complete 6f85777a9276: Pull complete 6359afc496ca: Pull complete 945860b71d75: Pull complete db1f674ef8a6: Pull complete 6e560e43af06: Pull complete Digest: sha256:7be62e52282273f672f28dec26979ac64f53a4d14b244c67875b2c7bb8010278 Status: Downloaded newer image for linuxserver/freshrss:latest gitea is up-to-date Creating freshrss ... proxy is up-to-date portainer is up-to-date Creating freshrss ... done
Head over to the URL you specified, and a new FreshRSS installation will be waiting for your favorite feeds!
How does this actually work?
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 how this system works together. Here’s the process, simplified into a handful of steps:
- You add a new service to
- You then run
docker-compose up -d.
- The new Docker container(s) are downloaded and started.
- A service called
docker-gen, running inside of
nginx-proxy, recognizes that a new Docker container has started via the Docker sockets file (
/var/run/docker.sock). Because this container has the
VIRTUAL_HOSTenvironment variable set,
nginx-proxyknows it should initialize it as part of the reverse proxy network.
nginx-proxycontainer creates new reverse proxy Nginx rules to route traffic coming into the VPS to the appropriate place.
letsencrypt-nginx-proxy-companionalso recognizes that a new Docker container has started via Docker sockets, and because it has the
LETSENCRYPT_HOSTenvironment variable set to a real domain,
letsencrypt-nginx-proxy-companionknows to request a new SSL certificate from Let’s Encrypt.
nginx-proxycoordinate to install the new SSL certificate and enable HTTPS traffic.
letsencrypt-nginx-proxy-companionchecks 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.
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.
Table of contents
Like what you saw? Subscribe to our weekly newsletter.