OS Apps: Vert, Watchtower and Tugtainer

Published: Oct 28, 2025 by Isaac Johnson

Vert is an excellent media convertor that I found from Marius. It just is about converting types. I had a lot of old 3gpp files from “the olden days” of cell phones to try out.

Watchtower is a great container updating tool (itself a container) but for those that want more UI, there is a related project, Tugtainer which I found offers a bit better user experience.

Let’s try all these tools both locally in Docker and in some cases Kubernetes.

Vert.sh

I saw this Marius post a few weeks ago about Vert which is an open-source file converter

Let’s start with Docker:

$ docker run -d \
    --restart unless-stopped \
    -p 3000:80 \
    --name "vert" \
    ghcr.io/vert-sh/vert:latest

Which loads just fine

/content/images/2025/10/vert-01.png

I now convert files using this local docker instance. For example, I can upload a PNG and then select GIF and convert all to convert it to a gif

/content/images/2025/10/vert-02.png

I tried a few video files like AVI and old phone 3GP files

/content/images/2025/10/vert-03.png

However, we can just use vert.sh directly

/content/images/2025/10/vert-04.png

Watchtower

Watchtower is all about automating the update of a containerized app whenever a new version is pushed to a registry.

Let’s take a look at it’s usage docs to see how we might deploy this on a docker host.

Let’s start with a container that is way out of date, Cinny

$ docker ps | grep cinny
9fe031d65142   ghcr.io/cinnyapp/cinny:latest                                    "/docker-entrypoint.…"   21 months ago    Up 4 weeks                0.0.0.0:8088->80/tcp, :::8088->80/tcp                                              cinny

/content/images/2025/10/watchtower-01.png

I should be able to run

docker run -d \
  --name watchtower \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower cinny --debug

It looks like it scheduled a check in 24hr

builder@builder-T100:~$ docker run -d \
  --name watchtower \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower cinny --debug
Unable to find image 'containrrr/watchtower:latest' locally
latest: Pulling from containrrr/watchtower
57241801ebfd: Pull complete
3d4f475b92a2: Pull complete
1f05004da6d7: Pull complete
Digest: sha256:6dd50763bbd632a83cb154d5451700530d1e44200b268a4e9488fefdfcf2b038
Status: Downloaded newer image for containrrr/watchtower:latest
9e91a2848c4948e52c6c76bf3afd0ff0462728a7e31174949f1426c70c3a9c8b
builder@builder-T100:~$ docker ps | grep watch
9e91a2848c49   containrrr/watchtower                                            "/watchtower cinny -…"   7 seconds ago    Up 7 seconds (health: starting)   8080/tcp                                                                           watchtower
builder@builder-T100:~$ docker logs watchtower
time="2025-10-15T12:02:41Z" level=debug msg="Sleeping for a second to ensure the docker api client has been properly initialized."
time="2025-10-15T12:02:42Z" level=debug msg="Making sure everything is sane before starting"
time="2025-10-15T12:02:42Z" level=debug msg="Retrieving running containers"
time="2025-10-15T12:02:42Z" level=debug msg="There are no additional watchtower containers"
time="2025-10-15T12:02:42Z" level=debug msg="Watchtower HTTP API skipped."
time="2025-10-15T12:02:42Z" level=info msg="Watchtower 1.7.1"
time="2025-10-15T12:02:42Z" level=info msg="Using no notifications"
time="2025-10-15T12:02:42Z" level=info msg="Only checking containers which name matches \"cinny\""
time="2025-10-15T12:02:42Z" level=info msg="Scheduling first run: 2025-10-16 12:02:42 +0000 UTC"
time="2025-10-15T12:02:42Z" level=info msg="Note that the first check will be performed in 23 hours, 59 minutes, 59 seconds"

I’m patient so I’ll wait a day and see (after 8am Thur…)

I checked on Friday at 6:15a and saw it indeed updating the image.

time="2025-10-15T12:02:42Z" level=info msg="Note that the first check will be performed in 23 hours, 59 minutes, 59 seconds"
time="2025-10-16T12:02:42Z" level=debug msg="Checking containers for updated images"
time="2025-10-16T12:02:42Z" level=debug msg="Retrieving running containers"
time="2025-10-16T12:02:42Z" level=debug msg="Trying to load authentication credentials." container=/cinny image="ghcr.io/cinnyapp/cinny:latest"
time="2025-10-16T12:02:42Z" level=debug msg="No credentials for ghcr.io found" config_file=/config.json
time="2025-10-16T12:02:42Z" level=debug msg="Got image name: ghcr.io/cinnyapp/cinny:latest"
time="2025-10-16T12:02:42Z" level=debug msg="Checking if pull is needed" container=/cinny image="ghcr.io/cinnyapp/cinny:latest"
time="2025-10-16T12:02:42Z" level=debug msg="Built challenge URL" URL="https://ghcr.io/v2/"
time="2025-10-16T12:02:42Z" level=debug msg="Got response to challenge request" header="Bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:user/image:pull\"" status="401 Unauthorized"
time="2025-10-16T12:02:42Z" level=debug msg="Checking challenge header content" realm="https://ghcr.io/token" service=ghcr.io
time="2025-10-16T12:02:42Z" level=debug msg="Setting scope for auth token" image=ghcr.io/cinnyapp/cinny scope="repository:cinnyapp/cinny:pull"
time="2025-10-16T12:02:42Z" level=debug msg="No credentials found."
time="2025-10-16T12:02:42Z" level=debug msg="Parsing image ref" host=ghcr.io image=cinnyapp/cinny normalized=ghcr.io/cinnyapp/cinny tag=latest
time="2025-10-16T12:02:42Z" level=debug msg="Doing a HEAD request to fetch a digest" url="https://ghcr.io/v2/cinnyapp/cinny/manifests/latest"
time="2025-10-16T12:02:42Z" level=debug msg="Found a remote digest to compare with" remote="sha256:8a7f4eee8164afc3e7ccf074ba5ac7d139fe415ae7612526e9bc9812789e622f"
time="2025-10-16T12:02:42Z" level=debug msg=Comparing local="sha256:419e4a03cb9e3b1547eeb0e7e14bf6b9ebc42bd0b499bbebcc5b489f2942f489" remote="sha256:8a7f4eee8164afc3e7ccf074ba5ac7d139fe415ae7612526e9bc9812789e622f"
time="2025-10-16T12:02:42Z" level=debug msg="Digests did not match, doing a pull."
time="2025-10-16T12:02:42Z" level=debug msg="Pulling image" container=/cinny image="ghcr.io/cinnyapp/cinny:latest"
time="2025-10-16T12:02:45Z" level=info msg="Found new ghcr.io/cinnyapp/cinny:latest image (ffe77a51da41)"
time="2025-10-16T12:02:45Z" level=info msg="Stopping /cinny (9fe031d65142) with SIGTERM"
time="2025-10-16T12:02:55Z" level=debug msg="Removing container 9fe031d65142"
time="2025-10-16T12:02:56Z" level=info msg="Creating /cinny"
time="2025-10-16T12:02:56Z" level=debug msg="Starting container /cinny (a1433993acb8)"
time="2025-10-16T12:02:56Z" level=info msg="Session done" Failed=0 Scanned=1 Updated=1 notify=no
time="2025-10-16T12:02:56Z" level=debug msg="Scheduled next run: 2025-10-17 12:02:42 +0000 UTC"

Evidence would suggest it did bounce the container at that time:

$ docker ps | grep cinny
a1433993acb8   ghcr.io/cinnyapp/cinny:latest                                    "/docker-entrypoint.…"   23 hours ago    Up 23 hours             0.0.0.0:8088->80/tcp, :::8088->80/tcp                                              cinny
9e91a2848c49   containrrr/watchtower                                            "/watchtower cinny -…"   47 hours ago    Up 47 hours (healthy)   8080/tcp  

I can now see Cinny is up to version 4.10.1

/content/images/2025/10/watchtower-02.png

What is interesting to me is there seems to be a TCP port slated for watchtower, 8080:

$ docker ps | grep 8080
9e91a2848c49   containrrr/watchtower                                            "/watchtower cinny -…"   47 hours ago    Up 47 hours (healthy)   8080/tcp                                                                           watchtower

But it does not serve anything web traffic

/content/images/2025/10/watchtower-03.png

I saw some docs about an HTTP API mode, but that too didn’t do anything

/content/images/2025/10/watchtower-04.png

I even double checked the container logs just in case it did trigger but didn’t respond on the web ui

builder@builder-T100:~$ docker logs watchtower 2>&1 | tail -n 5
time="2025-10-16T12:02:55Z" level=debug msg="Removing container 9fe031d65142"
time="2025-10-16T12:02:56Z" level=info msg="Creating /cinny"
time="2025-10-16T12:02:56Z" level=debug msg="Starting container /cinny (a1433993acb8)"
time="2025-10-16T12:02:56Z" level=info msg="Session done" Failed=0 Scanned=1 Updated=1 notify=no
time="2025-10-16T12:02:56Z" level=debug msg="Scheduled next run: 2025-10-17 12:02:42 +0000 UTC"

Tugtainer

Another interesting app that came on my radar is Tugtainer. This can be used to both list your docker containers and update them similar to Watchtower

It’s easy to install. Let’s add it to our docker host with a docker run command

builder@builder-T100:~/tugtainer$ mkdir data
builder@builder-T100:~/tugtainer$ docker run -d -p 9412:80 \
    --name=tugtainer \
    --restart=unless-stopped \
    -v ./data:/tugtainer \
    -v /var/run/docker.sock:/var/run/docker.sock \
    quenary/tugtainer:latest
Unable to find image 'quenary/tugtainer:latest' locally
latest: Pulling from quenary/tugtainer
8c7716127147: Already exists
eb47ef3fe06b: Already exists
326ad4d2abf0: Already exists
97385090cab0: Already exists
1157ce8d3c4a: Pull complete
d248418bcf38: Pull complete
e147d769b83c: Pull complete
271d55492ae9: Pull complete
5a37a23d9d0d: Pull complete
ef016fbb39d2: Pull complete
e15da9aca888: Pull complete
47a1f3722687: Pull complete
38c1499509de: Pull complete
Digest: sha256:3bb2dc4e3b428719b015252253ae88fd4efdc224b93bfb089946c198bd779586
Status: Downloaded newer image for quenary/tugtainer:latest
8cf5956d4904246de02449fcc095c1f96153433e0da3f76159853db7976f5047

I’m now presented with the login page where I can create an admin password

/content/images/2025/10/tugtainer-01.png

Then I use it to sign back in

/content/images/2025/10/tugtainer-02.png

I’m now presented with a large list of all my running containers

/content/images/2025/10/tugtainer-03.png

For any service I can expand to see what ports it serves (if any)

/content/images/2025/10/tugtainer-04.png

Let’s test with Beszel. I can see I do not have a tag pinned in the docker compose and it’s been 6 months since I fired it up.

builder@builder-T100:~/beszel$ cat docker-compose.yaml
services:
  beszel:
    image: 'henrygd/beszel'
    container_name: 'beszel'
    restart: unless-stopped
    ports:
      - '8095:8090'
    volumes:
      - ./beszel_data:/beszel_data

builder@builder-T100:~/beszel$ docker ps | grep besz
ab9ef739723f   henrygd/beszel                                                   "/beszel serve --htt…"   6 months ago    Up 4 weeks               0.0.0.0:8095->8090/tcp, :::8095->8090/tcp                                          beszel
c3f76f918df1   henrygd/beszel-agent                                             "/agent"                 11 months ago   Up 4 weeks                                                                                                  beszel-agent

It looks like presently I’m running 0.6.2

/content/images/2025/10/tugtainer-05.png

As we can see, that was incredibly easy to use to update a containerized app:

I can now tick the “Check” and “Update” boxes to have Tugtainer automatically check for updates and update

/content/images/2025/10/tugtainer-06.png

Another app I have tied to “latest” is dockpeek

builder@builder-T100:~/dockpeek$ cat docker-compose.yml
services:
  dockpeek:
    image: ghcr.io/dockpeek/dockpeek:latest
    container_name: dockpeek
    environment:
      - SECRET_KEY=my_secret_key   # Set secret key
      - USERNAME=builder           # Change default username
      - PASSWORD=Redliub$1Redliub$1 #  Change default password
    ports:
      - "3420:8000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

Here I’ll just set the auto-update and watch it work

For containers that are pinned to a service, this won’t do much, like my kokoro instance

/content/images/2025/10/tugtainer-08.png

While Tugtainer can update images, it cannot stop/start them if they are down. I find it’s best to pair up with Containery for that

/content/images/2025/10/tugtainer-09.png

Exposing with TLS

Let’s fire up an A record in Azure DNS

 az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.72.233.202 -n tugtainer
{
  "ARecords": [
    {
      "ipv4Address": "75.72.233.202"
    }
  ],
  "TTL": 3600,
  "etag": "5657cb50-4a98-4329-a957-849990a8881a",
  "fqdn": "tugtainer.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/tugtainer",
  "name": "tugtainer",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

I can now create the ingres, service and endpoint to hand off traffic

$ cat ./tugtainer-ingress.yaml

apiVersion: v1
kind: Endpoints
metadata:
  name: tugtainer-external-ip
subsets:
- addresses:
  - ip: 192.168.1.99
  ports:
  - name: tugtainerint
    port: 9412
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: tugtainer-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: tugtainer
    port: 80
    protocol: TCP
    targetPort: 9412
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.org/websocket-services: tugtainer-external-ip
  generation: 1
  name: tugtaineringress
spec:
  rules:
  - host: tugtainer.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: tugtainer-external-ip
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - tugtainer.tpk.pw
    secretName: tugtainer-tls

$ kubectl apply -f ./tugtainer-ingress.yaml
endpoints/tugtainer-external-ip created
service/tugtainer-external-ip created
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/tugtaineringress created

Once the cert is live

$ kubectl get cert tugtainer-tls
NAME            READY   SECRET          AGE
tugtainer-tls   True    tugtainer-tls   10m

I can just login with the URL to check on containers

/content/images/2025/10/tugtainer-10.png

As far as settings go, we can change from dark to light mode

/content/images/2025/10/tugtainer-11.png

We can set our timezone

/content/images/2025/10/tugtainer-12.png

As well as crontab or whether to prune images

/content/images/2025/10/tugtainer-13.png

Because the notifications link uses Apprise we can do something like create a Discord Webhook integration for Tugtainer

/content/images/2025/10/tugtainer-14.png

Then send a test

/content/images/2025/10/tugtainer-15.png

and see it show up in our Discord channel

/content/images/2025/10/tugtainer-16.png

Summary

Today we looked into 3 containerized apps - Vert, Watchtower and Tugtainer. Vert is a great tool for converting media such as old camera phone videos or unique image formats.

Watchtower and Tugtainer serve much of the same purpose: finding and updating containerized apps to the newest version. We looked at the basic WT usage of updating a named container, but it can also work in a docker compose setup to update containers automatically.

This would work to host my MCP server and check for newer containers (and refresh) every 5 minutes.

version: "3"
services:
  cavo:
    image: docker pull harbor.freshbrewed.science/library/vikunjamcp@latest
    ports:
      - "8000:8000"
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 3600

Tugtainer has a very nice web interface and can easily check and update containers.

I did find it handy to have containery in my stack so I could look at logs and manually stop/start containers.

vert watchtower tugtainer opensource docker

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes