Portainer for your Homelab
A step-by-step guide to getting started with Docker, Portainer, and securely accessing the Portainer container via a Tailscale companion container running Tailscale Serve for reverse-proxy.
This post is going to cover quite a bit. I'm going to walkthrough getting Docker onto a fresh Ubuntu install, utilizing Portainer to manage our Docker containers, and using Tailscale to connect to it so that only our private tailnet can manage it.
Files used can be found here.
I'm going to be using a completely fresh install of Ubuntu Desktop 24.04 with installation defaults as the host.
Looks like by default docker isn't there. I'm not a huge fan of snap
and the other two are "unofficial". So instead we follow docker documentation. First we add the repository to apt
.
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
Now we do the install.
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
We try the docker ps
command again.
It works, but our user isn't added to the docker
group, requiring us to use sudo
. So if we want to run docker commands without it, let's add ourself to the docker
group and try again.
First sudo usermod -aG docker $USER
adds the current user you're logged in with to the group, then newgrp docker
to let the permissions apply without having to logout/login. Third time's the charm, we run docker ps
again to verify.
If you haven't created a Tailscale account yet, do so. Once you login to the account, we need to do a couple steps. Download and install the agent on the PC you'll be using to visit web urls, otherwise when we set up Portainer to use Tailscale, we won't be able to access it.
Under the DNS tab of the Admin console, down at the bottom, you'll want to enable the MagicDNS and HTTPS Certificate settings if you haven't used Tailscale Serve before.
Go to the Access controls tab and modify your file to make sure the line for tagOwners
is uncommented (including closing bracket) and include "tag:container": ["autogroup:admin"],
within it. This tells us that anything with the tag container
can be added by those users within the automatically created admin
group, which you as the owner will be placed into already.
Navigate next to Settings > Keys > Generate auth key.
Fill out the fields similar to below and make sure you add the tag you just created.
Copy the code that is generated somewhere securely as someone with this password will be able to onboard new devices to your tailnet. You'll need it for the next step as well as any other containers you'd like to put behind Tailscale.
To make Tailscale Serve work with Portainer, we'll need the container to reference a configuration json. Make a directory via mkdir -p ~\appdata\ts-portainer\config
and then nano ~\appdata\ts-portainer\config\portainer.json
and copy/paste the following. CTRL-O
to write the file and CTRL-X
to exit if this is your first time with nano.
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "https+insecure://localhost:9443"
}
}
}
}
}
This tells Tailscale to forward to the Portainer's webui on port 9443 and ${TS_CERT_DOMAIN}
is Tailscale magic that pulls from the container itself with the hostname and MagicDNS url.
Let's get into the Portainer bits. Fill out this to get your free license that allows up to 3 nodes aka the hosts we install it on, with all their features. With that license we modify their deployment guide for docker standalone.
If you want to run Portainer off the host without a Tailscale container, you can run docker run -d -p 8000:8000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v ~/appdata/portainer:/data portainer/portainer-ee
directly and skip over the next bit.
A little break down of the above. -d
tells it to run in the background so it's not taking over your terminal session. -p
are the ports that the host listens and passes to the container, in host:container
format. --name
is the name of the container shown in docker ps
results and referenced by other containers. --restart
with the always
option tells the container to restart – aka if it crashes for some reason, or if the host reboots. The -v
is the volume mapping, where we are mapping the hosts docker.sock to the container, and a new folder we made to where the container stores it's data in the user's home directory. The final bit of portainer/portainer-ee
is telling docker to pull that image for the container's use.
However, I'm a fan of docker compose
so I'm going to go differently from what the portainer steps decree. We need to make a compose.yml
and a .env
for use in the container creation. This way it's a file you can revisit if you forget how you created and it falls off your terminal history. Create the .env
to store the tailscale secret and the appdata path as we'll be referencing it multiple times. Make sure you are in the home directory ( cd ~
if unsure) nano .etc
and fill it out similar:
TSKEY_AUTH=tskey-auth-blahblahblah-blahblahblah
APPDATA=~/appdata
Then nano compose.yml
and we paste the following and save it.
services:
ts-portainer:
image: tailscale/tailscale:latest
container_name: ts-portainer
hostname: portainer
environment:
- TS_AUTHKEY=${TSKEY_AUTH}
- TS_EXTRA_ARGS=--advertise-tags=tag:container
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_SERVE_CONFIG=/config/portainer.json
volumes:
- ${APPDATA}/ts-portainer/state:/var/lib/tailscale
- ${APPDATA}/ts-portainer/config:/config
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
portainer:
image: portainer/portainer-ee
container_name: portainer
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${APPDATA}/portainer:/data
depends_on:
- ts-portainer
network_mode: service:ts-portainer
volumes:
ts-portainer:
driver: local
Breaking the compose down. We have two services ts-portainer
which we're using for Tailscale and portainer
the actual Portainer container.
In ts-portainer
image
is used to pull the actual container image, container_name
is used so we have consistency and not adding a new device to our tailnet everytime a container get's spun up, and hostname
is used to pass the hostname for our MagicDNS url.
For the environment we're passing a couple things, TS_AUTHKEY
reads the .env
file we put in our auth key, so it can be onboarded to the tailnet. TS_EXTRA_ARGS
gives it the container tag since that's needed for our onboarding. TS_STATE_DIR
is a mandatory flag tailscale needs to be persistent for checks, we point it to where to look in the container and then use the ${APPDATA}/ts-portainer/state:/var/lib/tailscale
to tell the container where to map on the host. TS_USERSPACE
is another mandatory argument since we're running this without a user logged in. TS_SERVE_CONFIG
tells the container where to find it's the configuration for Tailscale Serve, which is the other mapping we have in volumes
. Review this page for additional environment variables that can be used.
The rest of the configuration is explained in the docker run
section above, with the additional depends on
tells Portainer to not start until Tailscale is running, and network_mode
where we tell it to use ts-portainer service for it's networking.
Now we docker compose up -d
which automatically targets the compose.yml
to build the containers and pulls in variables stored in the .env
. Watch as everything gets downloaded and created. When it's complete we visit the https://portainer.<yourmagicdns>.ts.net
we should now see the following, success!
Enter your desired password, and then your license key on the next page. Then "Get Started", your "local" environment, and containers.
We see both our containers here running, with the option to inspect, see logs, etc. Of course, this can be done without the gui – feel free to dig around and see what other Portainer features exist!
With our environment set up successfully, I'll be writing another article about some Portainer features and containers that can be useful for the homelab.