Recently I have been refreshing my various Telegram bots - I had 3 tiny go apps lying around and I have decided it’s time to put them into a single app. I was just done with all that boilerplate and it just made more sense.

While doing so I thought it would be cool to finally push docker containers I was already building using Github Actions directly to my Synology - without any human interaction. The thing is I didn’t want to expose the whole Synology to the Evil Internet. So I have decided to use Tailscale which is a truly magical product. It’s built on top of WireGuard but removes all the manual fiddling/configuration process. It’s install & forget.

Below is a quick-ish recipe I have decided to follow.

  1. You can install Tailscale on your NAS using this guide. Minor disclaimer here - I had a problem with re-authenticating the device on DMS7 and found the solution here - running sudo tailscale up after going to the Tailscale on the NAS (via web panel) solved the problem.

  2. For the Github actions you can use this official action.

  3. In the tailscale you need to generate Auth key and make it both Reusable and Ephemeral. In the DNS settings I recommend enabling MagicDNS so you can access your NAS via the <hostname>.<tailnet> instead of an IP address.

  4. To glue it all together I chose the webhook approach - I didn’t want to connect via SSH from GH actions to my NAS or setup something too fancy.

One-time steps I did on my Synology:

Save this Dockerfile somewhere and built it via docker build . -t webhook:2.8.0

FROM almir/webhook:2.8.0
RUN  apk --update --upgrade add docker curl bash && \
     rm -rf /var/cache/apk/*

I decided to put webhook config files in /volume1/docker/webhook on my NAS.

The content of the /volume1/docker/webhook/hooks.json looks something like this:

    "id": "my-app",
    "execute-command": "/etc/webhook/my-app",
    "response-message": "Restarting container",
    "pass-environment-to-command": [
        "envname": "DOCKER_PASSWORD",
        "source": "payload",
        "name": "DOCKER_PASSWORD"
        "envname": "DOCKER_TAG",
        "source": "payload",
        "name": "DOCKER_TAG"
    "trigger-rule-mismatch-http-response-code": 411,
    "trigger-rule": {
      "match": {
        "type": "value",
        "value": "SUPER-SECRET-TOKEN",
        "parameter": {
          "source": "header",
          "name": "X-Token"

I set the “SUPER-SECRET-TOKEN” as WEBHOOK_TOKEN (along with the TAILSCALE_AUTHKEY generated before) to in the GitHub repo secrets (you can use the github CLI and check gh secret set command).

And the content of the /volume1/docker/webhook/my-app looks something like this:


set -e

echo "Login into ghcr"
docker login -u your-username -p $DOCKER_PASSWORD
docker pull$DOCKER_TAG

echo "Stop running container"
docker stop my-app || true

echo "Remove old container"
docker rm my-app || true

# here I set ENVs, container network, labels etc.
echo "Run new container"
docker run \
  -d \
  --name=my-app \
  --restart unless-stopped \
  --log-opt max-size=5m \
  --log-opt max-file=3 \$DOCKER_TAG

echo "Logout from ghcr"
docker logout

Having those two files and a custom-built image you can now run (still on your Synology):

docker run -d \
  -p 9000:9000 \
  -v /volume1/docker/webhook:/etc/webhook:ro \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --log-opt max-size=10m \
  --log-opt max-file=5 \
  --restart unless-stopped \
  --name=webhook \
  webhook:2.8.0 -verbose -hooks=/etc/webhook/hooks.json -hotreload

At this point, you should have a Docker container running on your NAS called webhook exposed on port 9000 that will try to handle my-app that is assumed to be built on as your-username/my-app (which should correspond to the the repo location/url on Github). It will try to log into ghcr using provided password & fetch provided image tag (we will pass those using Github workflow). Hopefully, you will never have to touch those settings again.

Here is the GitHub actions workflow that does rest of the work:

name: build

      - main

    name: Build image
    runs-on: ubuntu-latest

      - uses: actions/[email protected]

      - uses: docker/[email protected]
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/[email protected]

      # caching docker layer ommited for brevity

      - name: Build and push
        uses: docker/[email protected]
          context: .
          push: true
          tags:${{ github.repository }}:${{ github.sha }}

      - name: Connect to tailscale
        uses: tailscale/[email protected]
          authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
          hostname: 'github-actions-runner'

      # with MagicDNS you should be now able to connect to your NAS using
      # tailscale network + hostname of the nas
      - name: Deploy docker container webhook
        uses: joelwmale/[email protected]
          url: http://<nas hostname>.<tailscale network>
          headers: '{"X-Token": "${{ secrets.WEBHOOK_TOKEN  }}"}'
          body: >-
            { "DOCKER_PASSWORD": "${{ secrets.GITHUB_TOKEN }}",
            "DOCKER_TAG": "${{ github.sha }}" }

      # Synology can be slow-ish when it comes to pulling new image
      # GITHUB_TOKEN might expire before NAS will manage to pull the
      # new image (that token expires after action is done)
      # You might want to do some fancy pooling or just slap some
      # sleep if that's good enough for you
      # As we're not building a production system here I recommend the latter
      # - name: Sleep for 15 seconds
      #   run: sleep 15

And that’s it - all the heavy lifting is being done via Tailscale, Github action with the webhook approach glues it nicely together.