Search code examples
dockerdocker-composeyamldocker-swarmportainer

Docker Swarm Secrets Not Evaluating Correctly in Compose on Portainer


I'm trying to deploy a Discord Bot I've written in python to a VM (running Portainer) on my home server. However, I'm running into the issue of having Portainer correctly evaluate the bot token secret. I have defined the secret with the name HU3BOT_DISCORD_TOKEN, and am using the following compose file:

version: "3.9"
services:
  hu3bot:
    image:  drak3/hu3bot:latest
    environment:
     - DISCORD_TOKEN=/run/secrets/HU3BOT_DISCORD_TOKEN
     - DISCORD_CHANNEL="3d_printing"
     - PRINTER_HOST=voron.srv
     - CAM_PORT_MAIN=8081
     - CAM_PORT_ALT=8080
     - MOONRAKER_API_PORT=7125
     - WEB_URL='https://fluidd.drak3.io'
    secrets:
    - HU3BOT_DISCORD_TOKEN

# the secret is a discord bot token
secrets:
  HU3BOT_DISCORD_TOKEN:
    external: true

Locally, I can run the script making use of a .env file w/o issue. I can do the same as a local container. However, when I attempt to use the secret I've defined, I cannot get it to be properly evaluated. I've added some print statements to my code, and depending on how i format the DISCORD_TOKEN=/run/secrets/HU3BOT_DISCORD_TOKEN line, the token will either be evaluated as a Null (None of type <class 'NoneType'>) or as a string with the content /run/secrets/HU3BOT_DISCORD_TOKEN.

I have tried all the following combinations of formatting, but they'll all either be the name of the secret, or Null:

  • DISCORD_TOKEN=/run/secrets/HU3BOT_DISCORD_TOKEN
  • DISCORD_TOKEN= /run/secrets/HU3BOT_DISCORD_TOKEN
  • DISCORD_TOKEN:/run/secrets/HU3BOT_DISCORD_TOKEN
  • DISCORD_TOKEN: /run/secrets/HU3BOT_DISCORD_TOKEN
  • "DISCORD_TOKEN=/run/secrets/HU3BOT_DISCORD_TOKEN"
  • "DISCORD_TOKEN= /run/secrets/HU3BOT_DISCORD_TOKEN"
  • "DISCORD_TOKEN:/run/secrets/HU3BOT_DISCORD_TOKEN"
  • "DISCORD_TOKEN: /run/secrets/HU3BOT_DISCORD_TOKEN"

To add insult to injury, I also have a different stack that pulls a webhook from a secret (like this: WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=/run/secrets/DISCORD_WEBHOOK_WATCHTOWER) which works fine.

Could the content of the secret have any bearing on this? I'm honestly at a loss to explain this, and would greatly appreciate any help or suggestions.

UPDATE:

For those coming along after me, having a similar issue, thanks to Chris Becke, I've realized I must have been misunderstanding how docker secrets actually work. I previously thought the string /run/secrets/secret_name could be used as a literal stand in for the secret value itself, and that by passing them in to environment variables, it would be equivalent of just having the raw secret there. This misunderstanding is most likely due to my having only used secrets in compose files using other people's images. I'm guessing they had some kind of code to distinguish between the value being an environment variable and a docker secret path. After adding such code to my bot, the value is evaluated as expected. This was a useful answer for doing this.


Solution

  • Setting an environment variable just sets it to the literal value you provide.

    Handling secrets properly entails injecting a script to read the secret into an environment variable. Something like this:

    services:
      some-service:
        image: whatever
        entrypoint:
        - /bin/sh
        - -c
        - |
          read -r DISCORD_TOKEN < /run/secrets/HU3BOT_DISCORD_TOKEN
          exec $$0 "$$@"
        command: previous command
    

    You need to look up the containers previous CMD and ENTRYPOINT values and make sure the new flow chains to the old flow. Setting entrypoint automatically clears any existing CMD so you need to fully specify that if it existed.

    It is also worth noting that a lot of Docker images (Postgres etc) support initialising environment variables from a file, by processing variables with a _FILE suffix.

    e.g. The Docker Postgres image will read the file specified by "POSTGRES_PASSWORD_FILE" into "POSTGRES_PASSWORD"

    If the Hu3Bot image has been built with this usage in mind, then setting DISCORD_TOKEN_FILE to the secret path might work.