I’ve been getting into self hosting a lot more services over the past few weeks and months as I’ve been on a quest to actively get rid of subscriptions and cloud services and hosting the services myself. At the start, I just made these services available over VPN because I didn’t feel comfortable exposing them to the internet. But as time went on, I noticed I was really missing the public access. So I decided to figure out how to safely expose services publicly.
In this post, I will go through the setup that I went with. For context, I run everything as docker containers on my Truenas box so your mileage may vary.
Traefik
The thing that took me the longest to understand was Traefik, but now that I’ve gotten a hang of it, it’s actually pretty straight forward!
Traefik is an open-source application proxy. It takes in all requests it receives and based on your configuration decides where to send the request off to. If you want to read up more on the architecture, their documentation is pretty extensive!
For the purposes of this guide, you need to understand a few concepts:
- Entrypoints: as you may have guessed, these are the entry points into Traefik, you use entrypoints to define the ports that will be receiving packets and whether it should be TCP or UDP.
- Routers: move the traffic from the entrypoints to the backend services. You can configure a router to also use different pieces of middleware along the way
- Services: services define how to reach the actual backend service you’re publishing
Pre-requisites
Because I didn’t want to expose every single container in a stack, I decided to create a dedicated proxy network. The traefik container lives in this network, as well as any other public facing container. If a stack has multiple containers, I would only put the container which hosts the web server on this network.
To create the network, run the command sudo docker network create proxy
. The network can be a simple bridge network.
Once that’s done, it’s time to bring traefik online. I use compose files for everything in my setup, it just makes my life easier and I can bring up complete application stacks at the same time. For context, every stack runs in it’s own folder. My traefik folder looks like this. Keep this in mind when you’re copying code for your own setup if your layout is different.
|
|
Here’s what my compose file looks like, I’ve replaced my domain name with domain.tld, replace that with your domain. Whenever you spin up a new compose file, docker will create a network for that stack. Since we created the proxy network manually, we need to tell docker to use it. We tell docker to use the proxy network, and at the bottom of the file we tell docker that it was already created.
You’ll also notice that I’m mounting a separate log folder. This is required for the configuration of Crowdsec. I will publish a follow-up post about that part later.
|
|
In this file we define some additional confiruation for traefik. The first part defines some more secure headers and adds in a redirect scheme for http to https. The second part is where we add in the Crowdsec plugin. As I was writing this, it already felt a bit long so I will publish a second post on this soon! config.yml
|
|
The final configuration file contains the entrypoints, providers and more importantly, certificate resolvers. In my case, I use letsencrypt certificates. Whenever I spin up a new service with Traefik, a new certificate will be requested for that CN, and written into the acme.json file.
traefik.yaml
|
|
In the .env file I have a single entry, TRAEFIK_DASHBOARD_CREDENTIALS=username:hashedpassword where I generated the hashed password using the command echo $(htpasswd -nB maarten) | sed -e s/\\$/\\$\\$/g
.
This password is only required to authenticate to the Traefik dashboard.
PocketID
Just presenting everything to the internet over a reverse proxy doesn’t exactly secure it very much. I wanted to avoid having a million different logins and ideally I would move to a modern form of authentication. Initially, I had chosen Authentik, since it was widely adopted and got a lot of praise. The learning curve was a bit too steep for my liking at the time, so I looked for something simpler and came across PocketID
So first things first, I have to spin up PocketID. You can find my compose file below. A lot of things should look familiar already.
|
|
PocketID is tied to the proxy network, since it needs to be available from Traefik. To tell Traefik how it needs to behave, we use the labels construct. Let’s go over every label and explain what it does:
- “traefik.enable=true” –> enables traefik for this service
- “traefik.http.routers.pocketid-secure.entrypoints=https” –> tells traefik to use the https entrypoint
- “traefik.http.routers.pocketid-secure.rule=Host(
pocketid.domain.tld
)” –> the hostname traefik looks for. It will also use this to request a certificate - “traefik.http.routers.pocketid-secure.tls=true” –> Tells traefik to enable TLS
- “traefik.http.routers.pocketid.tls.certresolver=cloudflare” –> Use this certresolver to request a certificate
- “traefik.http.routers.pocketid-secure.service=pocketid” –> create a pocketid service
- “traefik.http.services.pocketid.loadbalancer.server.port=1411” –> The backend port of the service
- “traefik.docker.network=proxy” –> what network traefik should use
After you’ve spun up PocketID, you can go to the https://pocketid.domain.tld/setup you specified earlier and create your first account. Create a passkey (or better multiple passkeys) and you’re good to tie in other applications!
Connecting applications
Now that the base setup is done, we can start tying applications into PocketID and start using Oauth. For the purposes of this guide, I’ll talk about applications that have OIDC support natively. You can also hide apps that don’t support OAuth out of the box behind PocketID, but that’s an article for another day.
For the sake of this guide, I’ll use Immich as an example, but the steps are the same for all applications. There’s 4 things you need:
- issuer URL
- client ID
- client secret
- callback URL
Depending on the app you’re deploying, you can configure this natively in the admin UI, or you have to define these things in an .env file. If you’re deploying Karakeep for example, you’ll have to use an .env file to get OAuth working.
Creating OIDC Client
In the PocketID interface, go to OIDC Clients and click on Add OIDC Client. Here you give the client a descriptive name, this name will also be shown when a user logs in. Next, you click on the + sign under Callback URLs Add in the URLs that can be found in the application documentation. For Immich I have the following URLs configured.

You can upload a logo if you want, this makes it esier for your users to identify what service they’re logging into. After that just click save. At the top of the screen you will now see the Client ID and Client secret.
Copy those files and enter them in Immich. For Immich this configuration is in the administration menu, settings, Authentication settings. Enter the details you copied over from PocketID and enter the URL you defined in your traefik config for PocketID as the issuer URL.
Verifying everything works
As a final step, we’re going to verify everything works as intended. Open up a new InPrivate/Incognito window and go to the public URL of your Immich. You should be greeted by this login screen

Press on the Login with OAuth button. This will redirect you to your PocketID instance. Once there you will be presented with the following screen

As soon as you click on the Sign In button, you will be prompted to provide your passkey. Once that’s done, a green checkmark will appear and you’ll be redirected to your Immich instance where you’ll be logged in! Initially when I set it up, I did run into a few issues. But all these issues turned out to be a wrong URL or a wrong secret that I had pasted. Double check all the URLs and IDs/secrets if you run into issues and you should be good.