I’ve always wanted a way to run various docker apps with actual HTTPS certificates internally. Playing with DNS zones and reverse proxies can work well enough, but can be complex. If you are already using Tailscale and are a bit familiar with Docker, there is a way to cheat.

I came across this post/video from Alex Kretzschmar of Tailscale and the Self-Hosted podcast that shows you how this sorcery works. Only difference for me was that I run all my docker containers on my Synology NAS, so the instructions didn’t quite add up as all the containers are hosted from the same IP with different port numbers. After while keeping track of the ports/logins can be a pain.

To understand how it all works with the Docker network modes and service linking, Alex goes into detail in the link above for the curious. Well worth the read and view of his video.

For this post I am just going to go through an example of how to set up an app called Sterling PDF using Container Manager on Synology.

As Alex notes - you’ll need to decide between Auth keys and OAuth clients for getting the Tailscale container authenticated to your tailnet. I will be using OAuth clients as they are bit more set and forget, but your threat model may be different. image from Tailscale blog

One thing to note about this type of setup is it’s not ideal for every situation or application. You are relying on access to your tailnet for access to the application. If another device not connected to your tailnet needs access to that app, you’ll have to add them. Also, an application like PiAlert needs access to your local network to notify you when devices connect. Using this setup with that app will by default not allow that access to that network and you won’t see any devices apart from the gateway. There might be a good way to configure that access, but that is out of scope for this.

Some prerequisites to make sure you are using for the OAuth client setup are:

  • Create/uncomment out your Access Controls section in your Tailscale admin portal so that the container can authenticate.
    // Define the tags which can be applied to devices and by which users.
    "tagOwners": {
    	"tag:container": ["autogroup:admin"],
    },
    
  • Turn on MagicDNS in your DNS section of your Tailscale admin portal.
  • Enable HTTPS in the same DNS section.
    • (Optionally) - rename you tailnet if you want a more easily typed domain name to use.
  • Generate an OAuth client under Settings —> OAuth clients with at least “Devices - Write” permissions and set the tag as you specified in your ACL above.

I’ll be using Stirling PDF as an example of taking my previous container that was running on http://192.168.1.50:8080. It’s a pretty cool app that lets you do all sorts of stuff with PDFs if you have that need. When we’re done it will run on https://stirling-pdf.penguin-lion.ts.net (I made the domain name up - but play around with the tailnet name generator on the site).

On your Synology, make sure you have the folder structure setup like the instructions suggest. Some of those are optional, but won’t hurt to create them for now.

For the Tailscale folder structure, I’m using the same naming method Alex used in his post with “ts-“ and the name of the other docker container folder for the app we’re setting up.

In the above “config” folder for tailscale, you’ll want to create a stirling-pdf.json file like below. This is what will help the HTTPS part work for us.

~Note:~ When I tried using Alex’s example json from his post it had an AllowFunnel: false section which should turn off Tailscale Funnel service (expose the app to the internet), but that didn’t work for me in that it said funnel was on, so I just took out that section.

{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "${TS_CERT_DOMAIN}:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://127.0.0.1:8080"
        }
      }
    }
  }
}

Here is the whole docker-compase.yml file when everything is done. I’ll go through and explain a few items. Note that Synology default is “volume1” for the local paths.

version: '3.3'
services:
  ts-stirling-pdf:
    image: tailscale/tailscale:latest
    container_name: ts-stirling-pdf
    hostname: stirling-pdf
    environment:
      - TS_AUTHKEY=tskey-client-xyzclientid-xyz123secret?ephemeral=false
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_SERVE_CONFIG=/config/stirling-pdf.json
      - TS_EXTRA_ARGS=--advertise-tags=tag:container
    volumes:
      - /volume2/docker/ts-stirling-pdf/state:/var/lib/tailscale
      - /volume2/docker/ts-stirling-pdf/config:/config
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    restart: unless-stopped
  stirling-pdf:
    image: frooodle/s-pdf:latest
    container_name: stirling-pdf
    network_mode: service:ts-stirling-pdf
    depends_on:
      - ts-stirling-pdf
    volumes:
      - /volume2/docker/stirling-pdf/data:/usr/share/tessdata
      - /volume2/docker/stirling-pdf/config:/configs
      - /volume2/docker/stirling-pdf/customFiles:/customFiles/
      - /volume2/docker/stirling-pdf/logs:/logs/
    environment:
      - DOCKER_ENABLE_SECURITY=false
    restart: unless-stopped
  • There are two containers/services here: stirling-pdf and ts-stirling-pdf. The stirling-pdf container will depend on the ts-stirling-pdf container for its network (tailscale). You’ll notice that second container name is the same as the host name of the first container.
  • The ?ephemeral=false part at the end of the OAuth client string will allow the device to persist on the tailnet.
  • That - TS_SERVE_CONFIG=/config/stirling-pdf.json is the file I mentioned above.
  • The - TS_EXTRA_ARGS=--advertise-tags=tag:container line is the container tag that the OAuth client will use to authenticate.
  • There are other items that you might need explanations for, but those are either personal preferences (restart: unless-stopped), better described in Alex’s post, or easily found with a quick search.

Save this docker-compose.yml file with your edits (namely the authkey and path names) and place it your stirling-pdf folder. When you step through the Synology Container Manager UI Project setup, it will pick the file up automatically. Go ahead and let it build at the end of the wizard and it will pull the images and setup the containers.

If you go into your Tailscale admin console under Machines you should see the stirling-pdf container. Now browse to https://stirling-pdf.(yourtailnetdomain).ts.net and you should now have an HTTPS docker app running on your tailnet!

As a bonus, here is another app called n8n to demonstrate adding a third (database) container to the mix. This one was setup with an Auth key so that is another difference. This is an app that really needs a domain to run properly for some features, so it’s a good use case for the Tailscale HTTPS method. Credit Marius for the basic layout of how to set this up.

version: "3.9"
services:
  ts-n8n:
    image: tailscale/tailscale:latest
    container_name: ts-n8n
    hostname: n8n
    environment:
      - TS_AUTHKEY=tskey-auth-xyz123-xyz123secret
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_SERVE_CONFIG=/config/n8n.json
    volumes:
      - /volume2/docker/ts-n8n/state:/var/lib/tailscale
      - /volume2/docker/ts-n8n/config:/config
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    restart: unless-stopped
  db:
    image: postgres
    container_name: n8n-DB
    network_mode: service:ts-n8n
    mem_limit: 512m
    cpu_shares: 768
    security_opt:
      - no-new-privileges:true
    user: 1026:100
    healthcheck:
      test: ["CMD", "pg_isready", "-q", "-d", "n8n", "-U", "n8nuser"]
      timeout: 45s
      interval: 10s
      retries: 10
    volumes:
      - /volume2/docker/n8n-new/db:/var/lib/postgresql/data:rw
    environment:
      TZ: America/Chicago
      POSTGRES_DB: n8n
      POSTGRES_USER: n8nuser
      POSTGRES_PASSWORD: n8npass
    restart: on-failure:5
    depends_on:
      - ts-n8n
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    network_mode: service:ts-n8n
    mem_limit: 1g
    cpu_shares: 768
    security_opt:
      - no-new-privileges:true
    volumes:
      - /volume2/docker/n8n-new/data:/home/node/.n8n:rw
      - /volume2/docker/n8n-new/files:/files:rw
    environment:
      N8N_HOST: n8n.<tailnet dns name>.ts.net
      N8N_PORT: 5678
      N8N_PROTOCOL: https
      NODE_ENV: production
      WEBHOOK_URL: https://n8n.<tailnet dns name>.ts.net
      GENERIC_TIMEZONE: America/Chicago
      TZ: America/Chicago
      DB_TYPE: postgresdb
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_HOST: n8n
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_USER: n8nuser
      DB_POSTGRESDB_PASSWORD: n8npass
    restart: on-failure:5
    depends_on:
      db:
        condition: service_healthy