TeslaLogger in the Cloud

For the last few months, I've been running TeslaLogger to log my car's journeys and charge statistics, with the aim of tracking battery health and charge performance over the long run. I've initially been running this on my Mac Mini at home, but thought I'd take advantage of Oracle's "Always Free Resources" and run it on a 4 core, 24GB RAM ARM64 instance in the cloud.

The goal here is to run Traefik 2 and TeslaLogger and it's Grafana instance on a Docker server, with Traefik 2 providing reverse proxying and SSL for TeslaLogger and Grafana without exposing the services to the internet directly - with Traefik 2 being the gatekeeper to them all. As a bonus, we'll add IPv6 support!

We'll assume you have Docker up and running on your machine. We'll also be using separate docker-compose files to keep TeslaLogger somewhat separate from Traefik, as you may be using Traefik for more services than just TeslaLogger.

Traefik 2 configuration

Files & Folders

Let's start with creating a folder in which you'll house your Traefik configuration. In my case that would be: mkdir -p ~/docker/traefik/

Next we'll create a folder for file based configuration of Traefik (we won't use this today but it's nice to have): mkdir -p ~/docker/traefik/dynamic_conf/

Lastly, we'll create a file for our certificate storage: touch ~/docker/traefik/acme.json and change it's permissions: chmod 600 ~/docker/traefik/acme.json

Network

Next up, we create a docker network on which traefik and the services will be connected to, allowing them to communicate with eachother.

sudo docker network create traefik_default

docker-compose.yml

We'll move to the folder for Traefik: mv ~/docker/traefik/

Now, we need to create our service definition. The finaldocker-compose.yml looks like this:

version: "2.1"
services:
  ipv6:
    image: robbertkl/ipv6nat
    restart: unless-stopped
    network_mode: "host"
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /lib/modules:/lib/modules:ro

  traefik:
    image: "traefik:latest"
    restart: unless-stopped
    command:
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      - "--api"
      # Generic Cert Resolver
      - "--certificatesresolvers.leresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.leresolver.acme.email=your@email.here"
      - "--certificatesresolvers.leresolver.acme.storage=/acme.json"
      - "--certificatesresolvers.leresolver.acme.tlschallenge=true"
     # Dynamic Configuration File
      - "--providers.file.directory=/dynamic_conf"
      # Stats
      - "--accesslog=true"
    ports:
      - "80:80"
      - "443:443"
    networks:
      - 'traefik_default'
      - 'fd01'
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./acme.json:/acme.json"
      - "./dynamic_conf:/dynamic_conf"
    labels:
      - "traefik.enable=true"
      # Dashboard
      - "traefik.http.routers.traefik.rule=Host(`traefik.foo.bar`)"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.tls.certresolver=leresolver"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.middlewares=authtraefik"
      - "traefik.http.middlewares.authtraefik.basicauth.users=florian:HTPASSWD" # user/password
      # global redirect to https
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

networks:
  fd01:
    enable_ipv6: true
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "true"
    ipam:
      driver: default
      config:
       - subnet: fd01::/80
  traefik_default:
    external: true

Let's go over the diferent sections.

IPv6 & Networks

  ipv6:
    image: robbertkl/ipv6nat
    restart: unless-stopped
    network_mode: "host"
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /lib/modules:/lib/modules:ro
      
[snip]

networks:
  fd01:
    enable_ipv6: true
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "true"
    ipam:
      driver: default
      config:
       - subnet: fd01::/80
  traefik_default:
    external: true
IPv6 and Networks configuration

At the top, we're defining a service called ipv6. This service will allow us to do NATing over IPv6, meaning that our Traefik instance will be accessible via IPv6. This is optional and you can just not have that service at all if you don't want IPv6.

In the networks, we create 2 networks. One called fd01, the other traefik_default. If you removed the IPv6 service at the top, you won't need fd01 here, so just remove that config. Be sure to also remove the fd01 network from the traefik service later on. The traefik_default network will be the network that we use for traefik to communicate to our TeslaLogger service - and any other services you may want to have in the future.

Traefik Service

  traefik:
    image: "traefik:latest"
    restart: unless-stopped
    command:
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      - "--api"
      # Generic Cert Resolver
      - "--certificatesresolvers.leresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.leresolver.acme.email=your@email.here"
      - "--certificatesresolvers.leresolver.acme.storage=/acme.json"
      - "--certificatesresolvers.leresolver.acme.tlschallenge=true"
     # Dynamic Configuration File
      - "--providers.file.directory=/dynamic_conf"
      # Stats
      - "--accesslog=true"
    ports:
      - "80:80"
      - "443:443"
    networks:
      - 'traefik_default'
      - 'fd01'
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./acme.json:/acme.json"
      - "./dynamic_conf:/dynamic_conf"
    labels:
      - "traefik.enable=true"
      # Dashboard
      - "traefik.http.routers.traefik.rule=Host(`traefik.foo.bar`)"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.tls.certresolver=leresolver"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.middlewares=authtraefik"
      - "traefik.http.middlewares.authtraefik.basicauth.users=florian:HTPASSWD" # user/password
      # global redirect to https
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

This is really our core section. We create 2 entrypoints, web and websecure on 80 and 443 respectively. We also make sure Docker containers are not exposed by default through Traefik, meaning they only get exposed if you add the corresponding labels: --providers.docker.exposedbydefault=false

Next we configure the certificate resolver, calling it leresolver, just a generic one for LetsEncrypt. Be sure to configure your email address!

Lastly we enable the file configuration directory and enable the access log.

The ports and networks configuration is pretty straightforward as is the volume one. We created the files and folders that we're using for the volumes before, so be sure you use the right folder mapping here.

When it comes to labels, we'll configure 2 things here:

  1. The Traefik dashboard
  2. Global HTTP->HTTPS redirect

First, we need to enable traefik for this service, as we don't expose services by default, using traefik.enable=true.

For the dashboard, be sure to set your rule correctly, defining where you want your Traefik dashboard to be accessible: traefik.http.routers.traefik.rule=Host(`traefik.foo.bar`)

Please also configure  your user and use htpasswd to generate yourself a password for the basicauth middleware: traefik.http.middlewares.authtraefik.basicauth.users=florian:HTPASSWD

You could also not add this section if you don't want to expose the Traefik dashboard.

Lastly, we add a middleware to redirect all HTTP to HTTPS.

That's it for the traefik configuration.

You should now be able to bring up traefik using: docker-compose up -d.

TeslaLogger configuration

Now that we have Traefik up and running, let's configure TeslaLogger. While we generally follow the instructions for the Docker setup, we use a slightly modified docker-compose.yml to allow for everything to run behind the reverse proxy.

We'll start with creating a folder for all our TeslaLogger stuff: mkdir -p ~/docker/teslalogger/ and then move into it: cd ~/docker/teslalogger/

Let's start with cloning the git repo into our folder: git clone https://github.com/bassmaster187/TeslaLogger

As per the global instructions, we'll copy over the config file and configure it: cp TeslaLogger/TeslaLogger/App.config TeslaLogger/TeslaLogger/bin/TeslaLogger.exe.config

And then we edit it: vi TeslaLogger/TeslaLogger/bin/TeslaLogger.exe.config

We configure the DBConnectionstring to look something like: <value>Server=database;Database=teslalogger;Uid=root;Password=teslalogger;CharSet=utf8;</value>

Now, this is where we deviate from the official guide, as we'll use a slightly modified docker-compose file.

Our docker-compose.yml

version: "3"
services:

  teslalogger:
    build: TeslaLogger/docker/teslalogger/.
    restart: always
    volumes:
      - ./TeslaLogger/TeslaLogger/www:/var/www/html
      - ./TeslaLogger/TeslaLogger/bin:/etc/teslalogger
      - ./TeslaLogger/TeslaLogger/GrafanaDashboards/:/var/lib/grafana/dashboards/
      - ./TeslaLogger/TeslaLogger/GrafanaPlugins/:/var/lib/grafana/plugins
      - ./TeslaLogger/docker/teslalogger/Dockerfile:/tmp/teslalogger-DOCKER
      - teslalogger-tmp:/tmp/
    depends_on:
      - database
    environment:
      - TZ=Europe/Berlin
    labels:
       - "com.centurylinklabs.watchtower.enable=false"
    networks:
      - internal
      - traefik_default

  database:
    image: mariadb:10.4.7
    restart: always
    env_file:
      - ./TeslaLogger/.env
    volumes:
      - ./TeslaLogger/TeslaLogger/sqlschema.sql:/docker-entrypoint-initdb.d/sqlschema.sql
      - ./TeslaLogger/TeslaLogger/mysql:/var/lib/mysql
    environment:
      - TZ=Europe/Berlin
    networks:
      - internal

  grafana:
    image: grafana/grafana:8.3.2
    restart: always
    environment:
      - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=natel-discrete-panel,pr0ps-trackmap-panel,teslalogger-timeline-panel
      - TZ=Europe/Berlin
      - GF_SERVER_ROOT_URL=http://tesla.foo.bar:3000/grafana/
      - GF_SERVER_SERVE_FROM_SUB_PATH=true
    volumes:
      - ./TeslaLogger/TeslaLogger/bin:/etc/teslalogger
      - ./TeslaLogger/TeslaLogger/GrafanaDashboards/:/var/lib/grafana/dashboards/
      - ./TeslaLogger/TeslaLogger/GrafanaPlugins/:/var/lib/grafana/plugins
      - ./TeslaLogger/TeslaLogger/GrafanaConfig/datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml
      - ./TeslaLogger/TeslaLogger/GrafanaConfig/sample.yaml:/etc/grafana/provisioning/dashboards/dashboards.yml
    depends_on:
      - database
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tl_grafana.rule=Host(`tesla.foo.bar`) && PathPrefix(`/grafana`)"
      - "traefik.http.routers.tl_grafana.entrypoints=websecure"
      - "traefik.http.routers.tl_grafana.tls=true"
      - "traefik.http.routers.tl_grafana.tls.certresolver=leresolver"
      - "traefik.http.services.tl_grafana.loadbalancer.server.port=3000"
      - "traefik.docker.network=traefik_default"
    networks:
      - traefik_default
      - internal

  webserver:
    build: TeslaLogger/docker/webserver/.
    restart: always
    volumes:
      - ./TeslaLogger/docker/webserver/php.ini:/usr/local/etc/php/php.ini
      - ./TeslaLogger/TeslaLogger/www:/var/www/html
      - ./TeslaLogger/TeslaLogger/bin:/etc/teslalogger
      - ./TeslaLogger/docker/teslalogger/Dockerfile:/tmp/teslalogger-DOCKER
      - ./TeslaLogger/TeslaLogger/GrafanaConfig/datasource.yaml:/tmp/datasource-DOCKER
      - teslalogger-tmp:/tmp/
    labels:
       - "com.centurylinklabs.watchtower.enable=false"
       - "traefik.enable=true"
       - "traefik.http.routers.teslalogger.rule=Host(`tesla.foo.bar`) && PathPrefix(`/admin`)"
       - "traefik.http.routers.teslalogger.entrypoints=websecure"
       - "traefik.http.routers.teslalogger.tls=true"
       - "traefik.http.routers.teslalogger.tls.certresolver=leresolver"
       - "traefik.http.services.teslalogger.loadbalancer.server.port=80"
       - "traefik.docker.network=traefik_default"
    networks:
      - traefik_default
      - internal
    environment:
      - TZ=Europe/Berlin

volumes:
    teslalogger-tmp:
networks:
  traefik_default:
    external: true
  internal:
    internal: true

teslalogger serivce

We make sure we build the code from the TeslaLogger directory, not our current one. We're also mapping the volumes to that subdirectory.

We'll also connect the instance to our traefik_default and internal network. That's it for this service.

database service

For the database, we'll again change the mount folders, and then connect it only to our internal network.

grafana service

This is the service where we'll need to make some changes. We're adding a few environment variables, the key ones being:

      - GF_SERVER_ROOT_URL=http://tesla.foo.bar:3000/grafana/
      - GF_SERVER_SERVE_FROM_SUB_PATH=true

We'll also correct the folder mapping in the volumes section as per the other ones and then we add some labels for traefik to pick up the instance:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tl_grafana.rule=Host(`tesla.foo.bar`) && PathPrefix(`/grafana`)"
      - "traefik.http.routers.tl_grafana.entrypoints=websecure"
      - "traefik.http.routers.tl_grafana.tls=true"
      - "traefik.http.routers.tl_grafana.tls.certresolver=leresolver"
      - "traefik.http.services.tl_grafana.loadbalancer.server.port=3000"
      - "traefik.docker.network=traefik_default"

Be sure to configure your rule correctly to match your domain name you want to serve it on.

Lastly, we'll connect grafana to both the internal and traefik_default network

webserver service

Again, we'll adjust the folder mappings here, and then we need to add the labels for traefik.

    labels:
       - "com.centurylinklabs.watchtower.enable=false"
       - "traefik.enable=true"
       - "traefik.http.routers.teslalogger.rule=Host(`tesla.foo.bar`) && PathPrefix(`/admin`)"
       - "traefik.http.routers.teslalogger.entrypoints=websecure"
       - "traefik.http.routers.teslalogger.tls=true"
       - "traefik.http.routers.teslalogger.tls.certresolver=leresolver"
       - "traefik.http.services.teslalogger.loadbalancer.server.port=80"
       - "traefik.docker.network=traefik_default"

Lastly, same as the grafana service, we connect it to both traefik_default and internal under networks.

Launching TeslaLogger

Now that we have configured it all, let's simply launch it with docker-compose up -d

You should be able to access your TeslaLogger instance by visiting https://tesla.foo.bar/admin/ - of course using your configured domain.

Grafana will be available under https://tesla.foo.bar/grafana/.

Please make sure you configure a TeslaLogger password, as otherwise your instance will be accessible by anyone and change your Grafana user password.

Summary

That's it. You're now running TeslaLogger securely in the cloud, behind Traefik without the need for any VPN.

You can find a follow up post on how to run TeslaETA alongside this over here.