OS Apps: Favicon Downloader and Shiori

Published: Aug 29, 2024 by Isaac Johnson

Today I wanted to dig into a few quick Open-source apps on my to-do list to check out; Favicon Downloader and Shiori.

Favicon Downloader is a very simple, but quite useful, containerized utility for downloading favicon files from websites. These are the small images in the upper left on your tabs when you view websites.

Shiori is a bookmark manager, but a bit more than that as it can download websites and turn them into ePubs. We look at the containerized version before building a Kubernetes YAML manifest and hosting in k8s.

Let’s dig in.

Favicon Downloader

I can pull down the git repo here.

builder@LuiGi:~/Workspaces$ git clone https://github.com/seadfeng/favicon-downloader.git
Cloning into 'favicon-downloader'...
remote: Enumerating objects: 811, done.
remote: Counting objects: 100% (204/204), done.
remote: Compressing objects: 100% (135/135), done.
remote: Total 811 (delta 99), reused 148 (delta 52), pack-reused 607 (from 1)
Receiving objects: 100% (811/811), 2.45 MiB | 4.92 MiB/s, done.
Resolving deltas: 100% (368/368), done.

Next, I’ll build the docker image locally

builder@LuiGi:~/Workspaces/favicon-downloader$ cp .docker/Dockerfile .
builder@LuiGi:~/Workspaces/favicon-downloader$ docker build -t favicondownloader:latest .
[+] Building 91.6s (25/25) FINISHED                                                                      docker:default
 => [internal] load build definition from Dockerfile                                                               0.1s
 => => transferring dockerfile: 1.78kB                                                                             0.0s
 => [internal] load metadata for docker.io/library/node:20-alpine                                                  0.4s
 => [internal] load .dockerignore                                                                                  0.1s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load build context                                                                                  0.1s
 => => transferring context: 5.27MB                                                                                0.1s
 => [base 1/1] FROM docker.io/library/node:20-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87  6.7s
 => => resolve docker.io/library/node:20-alpine@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139  0.0s
 => => sha256:2ba186860f733cc6abe783afaec3d55af4852d15cafc3f030544609c22d6e217 448B / 448B                         0.4s
 => => sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 7.67kB / 7.67kB                     0.0s
 => => sha256:2cc3d19887bfea8bf52574716d5f16d4668e35158de866099711ddfb2b16b6e0 1.72kB / 1.72kB                     0.0s
 => => sha256:d58137ded569a53fbc27bc227ab40807d2306033d1b7eed800f2b2f0c8c083bf 6.38kB / 6.38kB                     0.0s
 => => sha256:731f5f878c76e7151b9c66968cbb701e7e815fd48718033cf20ba30a2317cff1 42.31MB / 42.31MB                   5.1s
 => => sha256:17ea4cb95273cd126d8bd46096946efa3d2bad82680b0c838ead8400a98c055a 1.39MB / 1.39MB                     1.0s
 => => extracting sha256:731f5f878c76e7151b9c66968cbb701e7e815fd48718033cf20ba30a2317cff1                          1.1s
 => => extracting sha256:17ea4cb95273cd126d8bd46096946efa3d2bad82680b0c838ead8400a98c055a                          0.0s
 => => extracting sha256:2ba186860f733cc6abe783afaec3d55af4852d15cafc3f030544609c22d6e217                          0.0s
 => [deps 1/6] RUN apk add --no-cache libc6-compat                                                                 1.7s
 => [builder 1/5] WORKDIR /app                                                                                     0.2s
 => [runner 2/9] RUN addgroup --system --gid 1001 nodejs                                                           0.6s
 => [runner 3/9] RUN adduser --system --uid 1001 nextjs                                                            0.5s
 => [deps 2/6] WORKDIR /app                                                                                        0.1s
 => [deps 3/6] RUN node -v                                                                                         0.6s
 => [deps 4/6] COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./                                  0.2s
 => [deps 5/6] RUN ls -la                                                                                          0.5s
 => [deps 6/6] RUN   if [ -f yarn.lock ]; then yarn --frozen-lockfile;   elif [ -f package-lock.json ]; then npm  33.6s
 => [builder 2/5] COPY --from=deps /app/node_modules ./node_modules                                                3.2s
 => [builder 3/5] COPY . .                                                                                         0.1s
 => [builder 4/5] RUN yarn build                                                                                  38.9s
 => [builder 5/5] RUN ls -la /app/.next/standalone                                                                 0.6s
 => [runner 4/9] COPY --from=builder /app/public ./public                                                          0.1s
 => [runner 5/9] RUN mkdir .next                                                                                   0.4s
 => [runner 6/9] RUN chown nextjs:nodejs .next                                                                     0.5s
 => [runner 7/9] COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./                                0.3s
 => [runner 8/9] COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static                        0.2s
 => [runner 9/9] RUN ls -la /app                                                                                   0.5s
 => exporting to image                                                                                             0.4s
 => => exporting layers                                                                                            0.4s
 => => writing image sha256:49de564120978f189395746c4b1320ff2b8ba369d7c10e06f01158d9c8f5f60e                       0.0s
 => => naming to docker.io/library/favicondownloader:latest                                                        0.0s

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview
builder@LuiGi:~/Workspaces/favicon-downloader$

I can then run it locally

builder@LuiGi:~/Workspaces/favicon-downloader$ docker run -p 3000:3000 favicondownloader:latest
  ▲ Next.js 14.2.5
  - Local:        http://localhost:3000
  - Network:      http://0.0.0.0:3000

 ✓ Starting...
 ✓ Ready in 57ms

I can then access the browser

/content/images/2024/08/favicon-01.png

For instance, I can pull my own

/content/images/2024/08/favicon-02.png

I can now see it

/content/images/2024/08/favicon-03.png

/content/images/2024/08/favicon-freshbrewed.science.png

Hosting

First, I’ll push my build out to Dockerhub

builder@LuiGi:~/Workspaces/favicon-downloader$ docker tag favicondownloader:latest idjohnson/favicondownloader:latest
builder@LuiGi:~/Workspaces/favicon-downloader$ docker push idjohnson/favicondownloader:latest
The push refers to repository [docker.io/idjohnson/favicondownloader]
24f5bdcc031b: Pushed
75a7df487577: Pushed
194ec978536f: Pushed
12653338691a: Pushed
6cf421852d24: Pushed
c96ae09775ca: Pushed
714363458333: Pushed
9f13ff84d634: Pushed
bdd35ac0454c: Pushed
186d35b3efff: Mounted from library/node
32abe9a81c15: Mounted from library/node
3762a94f9dbd: Mounted from library/node
78561cef0761: Mounted from library/node
latest: digest: sha256:18d63c1b4ab2b57e160f9395a7e38fdff1b6fab9f92fd8e68422a9beba3022d8 size: 3030

I can then make a manifest that would expose it

$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: favicondownloader-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: favicondownloader
  template:
    metadata:
      labels:
        app: favicondownloader
    spec:
      containers:
      - name: favicondownloader
        image: idjohnson/favicondownloader:latest
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: favicondownloader-service
spec:
  selector:
    app: favicondownloader
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP

I can then apply it

$ kubectl apply -f manifest.yaml
deployment.apps/favicondownloader-deployment created
service/favicondownloader-service created

I will then create a DNS record

$ gcloud dns --project=myanthosproject2 record-sets create favicon.steeped.space --zone="steepedspace" --type="A" --ttl="300" --rrdatas="75.73.224.240"
NAME                    TYPE  TTL  DATA
favicon.steeped.space.  A     300  75.73.224.240

I’ll now add the ingress

$ cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
  name: favicongcpingress
spec:
  rules:
  - host: favicon.steeped.space
    http:
      paths:
      - backend:
          service:
            name: favicondownloader-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - favicon.steeped.space
    secretName: favicongcp-tls

And apply

$ kubectl apply -f ./ingress.yaml
ingress.networking.k8s.io/favicongcpingress created

We can now try favicon.steeped.space

/content/images/2024/08/favicon-04.png

Shiori installation

First, I’ll try and run with docker

builder@LuiGi:~/Workspaces/favicon-downloader$ mkdir shiori
builder@LuiGi:~/Workspaces/favicon-downloader$ docker run -d --rm --name shiori -p 8080:8080 -v $(pwd):/shiori ghcr.io/go-shiori/shiori
Unable to find image 'ghcr.io/go-shiori/shiori:latest' locally
latest: Pulling from go-shiori/shiori
94d04197b5f5: Pull complete
e795eb5935ad: Pull complete
28e8a13529cb: Pull complete
61b19bf916ce: Pull complete
f8084089c2e1: Pull complete
Digest: sha256:85a47b264a5b96dec5e558b385f03cf867397dcc37d36d08b7f035681c846649
Status: Downloaded newer image for ghcr.io/go-shiori/shiori:latest
52b5241c9031bc876d687934441f99d80af3e7a266e910f13661f7e090ecb39a

I can then hit it on port 8080

/content/images/2024/08/shiori-01.png

From the FAQ we can see the login/pass is shiori:gopher

My next step was to try and get it to run in Kubernetes natively

The installation

I can the use a manifest

$ cat shiori.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shiori
  labels:
    app: shiori
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shiori
  template:
    metadata:
      labels:
        app: shiori
    spec:
      volumes:
      - name: app
        persistentVolumeClaim:
          claimName: shiori-pvc
      containers:
      - name: shiori
        image: ghcr.io/go-shiori/shiori:latest
        command: ["/usr/bin/shiori", "serve", "--webroot", "/shiori"]
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        volumeMounts:
        - mountPath: /srv/shiori
          name: app
        env:
        - name: SHIORI_DIR
          value: /srv/shiori
        - name: HTTP_ROOT_PATH
          value: "/shiori"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shiori-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: local-path
---
apiVersion: v1
kind: Service
metadata:
  name: shiori
spec:
  type: NodePort
  selector:
    app: shiori
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
    nodePort: 32654

Then apply

$ kubectl apply -f ./shiori.yaml
deployment.apps/shiori created
persistentvolumeclaim/shiori-pvc created
service/shiori created

I’ll then port-forward

$ kubectl port-forward svc/shiori 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

I use the shiori and gopher password to start

This worked just fine

/content/images/2024/08/shiori-02.png

I’ll go to settings to change the password

/content/images/2024/08/shiori-03.png

I logged out and verified the old password was changed

/content/images/2024/08/shiori-04.png

Just to be sure it saved in the PVC, i rotate the pod and verified the password was still changed

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep shiori
shiori-577d7bb646-jc5mq                              1/1     Running             0                   5m9s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl delete pod shiori-577d7bb646-jc5mq
pod "shiori-577d7bb646-jc5mq" deleted
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl port-forward svc/shiori 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080

As before, let’s create an Ingress for it

$ gcloud dns --project=myanthosproject2 record-sets create shiori.steeped.space --zone="steepedspace" --type="A" --ttl="300" --rrdatas="75.73.224.240"
NAME                   TYPE  TTL  DATA
shiori.steeped.space.  A     300  75.73.224.240

Which needs an ingress YAML

$ cat shiori.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
  name: shiorigcpingress
spec:
  rules:
  - host: shiori.steeped.space
    http:
      paths:
      - backend:
          service:
            name: shiori
            port:
              number: 8080
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - shiori.steeped.space
    secretName: shiorigcp-tls

easily applied

$ kubectl apply -f ./shiori.ingress.yaml
ingress.networking.k8s.io/shiorigcpingress created

Once I saw the Cert created the site came up

$ kubectl get cert shiorigcp-tls
NAME            READY   SECRET          AGE
shiorigcp-tls   True    shiorigcp-tls   2m

Usage

Let’s create an eBook from the website. I really have no idea what this will end up doing, to be fair.

/content/images/2024/08/shiori-05.png

It looks like it to the main page and created a quick HTML book

/content/images/2024/08/shiori-06.png

It’s pretty much the summary and the current front-page articles

/content/images/2024/08/shiori-07.png

I’m curious how the pseudo-paywall type articles are handled.

Here is a medium article about a Job at Google and it cuts off half-way if you aren’t logged in.

I’ll add that

/content/images/2024/08/shiori-08.png

I see I can pull that down as an ePub which would work on my kindles and Nook (1st Gens).

/content/images/2024/08/shiori-09.png

But, alas, a paywall defeater it is not and I got cut off in the same place

/content/images/2024/08/shiori-10.png

Summary

Favicon Downloader and Shiori are small light simple utility containers that were easy to host in Kubernetes. I can see both coming into use from time to time.

Kubernetes Containers Open-source Favicon Downloader Shiori

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