You found me!
  • About
  • All posts

My docker playground - Wed, Mar 25, 2020

For quick experiments, like trying out new software, and also for easily managing static website services for sites like this one, I run docker on a number of hosts (usually VM’s or bare metal servers). It’s quick and dirty, but it does the job, so a collegue asked me to write up a bit about this docker playground. Here it goes.

NB. This step by step guide assumes we’re using Debian 10 Linux on amd64 architecture. It may also be applicable on other systems, but I have not tested it. Let me know what you find!

The nessecary ingredients

  • A linux host with root access
  • DNS to add some records for this host
  • Public IP
  • docker
  • portainer
  • traefik

Let’s get cooking…

Install docker-ce

This is (almost) exactly as described at https://docs.docker.com/install/linux/docker-ce/debian/ but I am assuming you are in a root shell (i.e. not using sudo in this case)

Install required packages for using an https repo (why this isn’t available by default these days…)

apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common

Install the repository key

curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -

Add the docker-ce repository. I’ve chosen not to take the standard install practice here because I like custom added repo’s to not be in source.list but in sources.list.d with a separate file. This makes things like housekeeping and ansible scripting a bit easier IMO.

echo \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable" > /etc/apt/sources.list.d/docker-ce.list

Update package cache and install docker-ce

apt-get update && apt-get install docker-ce docker-ce-cli containerd.io

And this step is done. If all is well, you can now run the docker ps command to see if the docker environment is up and running.

Make it a swarm

I want this approach to be able to scale over multiple hosts if needed, so let’s make this machine it’s own Swarm. I’m assuming you know how to find your host’s IP address, you need to enter it here.

docker swarm init --advertise-addr <Host-IP>

In case you might want to add docker nodes to this swarm later on, it is usefull to save the token that the command above prints out. It looks something like this:

Swarm initialized: current node (89n3329ctbpbad0qqaw5udtnp) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-3wuam4bzp5h9yvd1elbabgh16vg86tok2qommjbwqfmif7ds4d-bs1go083dm20rylz2ur1g3fid <Host-IP>:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Install portainer

The nice folks at Portainer have decent getting-started docs at https://www.portainer.io/installation/ however, I wanted a little something special. I’ve taken their portainer-agent-stack.yml and adjusted it in order to prepare for using Traefik further down the road.

Here is what it looks like now:

version: '3.2'

services:
  agent:
    image: portainer/agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9000:9000"
    volumes:
      - /opt/portainer:/data
    networks:
      - agent_network
#      - proxy
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
      labels:
        traefik.enable: "true"
        traefik.http.routers.portainer.rule: Host(`<myhostname>`)
        traefik.http.routers.portainer.entrypoints: https
        traefik.http.routers.portainer.tls.certresolver: letsencrypt
        traefik.http.services.portainer.loadbalancer.server.port: 9000
        traefik.docker.network: proxy

networks:
  agent_network:
    driver: overlay
    attachable: true
#  proxy:
#    external: true

There are two things to mind:

  1. Make sure the volume exists (in this example /opt/portainer)
  2. Enter the <myhostname>. This should be a resolvable name, because in the next steps Traefik will be trying to generate Letsencrypt certificates for your system, including the Portainer WebUI.

As you can see I’ve left port 9000 open so we can access the system before Traefik is up and running. Once we’re done, we can comment that out. Also, the network port is commented out, once Traefik is up and running, we will enable it for this stack too.

Save the contents to a file, let’s call it portainer-agent-stack.yml also lets create a little script to pull the images and (re)start Portainer, we’ll call upgrade_portainer.sh

Here is what that looks like:

#!/bin/bash

# Upgrade local portainer image
docker pull portainer/portainer
docker pull portainer/agent

# Stop portainer
docker stack rm portainer

# Start upgraded portainer
docker stack deploy --compose-file=portainer-agent-stack.yml portainer

Make it executable and run it

chmod +x upgrade_portainer.sh && ./upgrade_portainer.sh

If all is well you now have an operational Portainer stack which can be accessed via http://myhostname:9000 *** Insert image ***

Create your admin user now, and log in. You can now open the primary swarm and view the running stacks. There should be one: portainer

Install Traefik

We will now add Traefik as a second stack in the swarm. First create the proxy overlay network

docker network create -d overlay proxy

Also create some directories for storage.

mkdir -p /opt/traefik/letsencrypt
mkdir -p /opt/traefik/etc/conf.d

You can configure Traefik almost entirely with environment variables, but I’ve chosen not to do so because it gives me options to proxy for other stuff than just docker. You might not need to, but it won’t hurt.

Create a basic configuration in /opt/traefik/etc/traefik.yml:

# /etc/traefik/traefik.yml

global:
  checkNewVersion: true
  sendAnonymousUsage: true

log:
  level: DEBUG

api:
  insecure: true

ping: {}
  
entryPoints:
  http:
    address: :80
  https:
    address: :443

certificatesResolvers:
  letsencrypt:
    acme:
      email: <admin-email>
      storage: /letsencrypt/acme.json
      tlschallenge: true

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
    swarmMode: true
  file:
    directory: /etc/traefik/conf.d/
    watch: true

And add the ‘redirect to https’ file in /opt/traefik/etc/conf.d/redirect.yml:

# /etc/traefik/dynamic/redirect.yml
http:
  routers:
    redirect-to-https:
      entryPoints:
        - http
      middlewares:
        - redirect-to-https
      rule: HostRegexp(`{any:.+}`)
      service: redirect-to-https

  services:
    # bogus service, the URL will be never called
    redirect-to-https:
      loadBalancer:
        servers:
          - url: http://192.168.0.1

  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true

Almost there!

In the Stacks list, click the + Add stack button. You’ll need to give your stack a name, I suggest ’traefik’. In the Web editor, paste the compose file below.

version: "3.7"

networks:
  proxy:
    external: true

services:
  traefik:
    image: "traefik:latest"
    command:
      - "--configFile=/etc/traefik/traefik.yml"
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
    volumes:
      - "/opt/traefik/etc:/etc/traefik"
      - "/opt/traefik/letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    deploy:
      placement:
        constraints:
          - node.role == manager
      labels:
        traefik.enable: "true"
        traefik.http.routers.traefik.rule: Host(`traefik.<myhostname>`)
#        traefik.http.routers.traefik.middlewares: auth
#        traefik.http.middlewares.auth.basicAuth.users: "florian:$$apr1$$9<hash>"
        traefik.http.routers.traefik.entrypoints: https
        traefik.http.routers.traefik.tls.certresolver: letsencrypt
        traefik.http.services.traefik.loadbalancer.server.port: 8080
        traefik.docker.network: proxy

When done, click the Deploy stack button, and if all is well, Traefik will start up and generate the first Letsencrypt certs for the Portainer admin UI and for it’s own dashboard.

Wrapping up

Now we can do a few closing operations:

Lock down the unsecure Portainer port

In the portainer stack file, comment out the port snippet:

#    ports:
#      - "9000:9000"

and uncomment the proxy network snippets

    networks:
      - agent_network
#      - proxy
    deploy:

...

networks:
  agent_network:
    driver: overlay
    attachable: true
  proxy:
    external: true

then call the upgrade_portainer.sh script.

Lock down the unsecured Traefik dashboard

The Traefik dashboard is available but unprotected. This is clearly not ideal, so you might want to set a password for it. Luckily this can be done with Traefik middlewares, as documented at https://docs.traefik.io/v2.0/middlewares/basicauth/ You need to create an htpasswd entry:

echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g

then add it to the Traefik stack by uncommenting the lines to look something like this:

        traefik.http.routers.traefik.middlewares: auth
        traefik.http.middlewares.auth.basicAuth.users: "florian:$$apr1$$9<hash>"

and click Update the stack

Closing thoughts

As said this method can also work across a swarm with multiple nodes, but then you might need to arrange some form of shared storage. I’ve done this with an NFS mount that was available in the same place on all docker nodes. Simply adjust the path you’re using in your stack file appropriately.

I’m sure this approach can still be improved in a myriad of ways. If you have any suggestions, please let me know :-)

\O/

That’s it! You should now have a fully functional docker/portainer/traefik box to tinker with. Enjoy!

Back to Home


© Florian Overkamp 2025 | Just some personal ramblings. | Socials: Mastodon LinkedIn