OS Web Upload Tools: Chibisafe and a PHP uploader

Published: Mar 14, 2024 by Isaac Johnson

There are times we wish to have users send us files outside of just email. I wanted to explore some self-hosted OS options and came accross Chibisafe. This is a pretty full features web-based uploader with a variety of features. I also found a very simple PHP Uploader. In both cases, they had a Docker option, but no Kubernetes.

In the writeup today, we’ll explore launching with Docker and then Kubernetes YAML manifests. After looking at basic usage, we’ll create our own Helm charts and use them with Azure DNS records and Nginx TLS ingress. Lastly, I’ll publish them to a Harbor CR OCI repo (where you are welcome to use them yourself)

Let’s get started!

Chibisafe

I saw a docker compose example from a mariushosting post

version: "3.9"
services:
  chibisafe:
    image: chibisafe/chibisafe:latest
    container_name: Chibisafe
    mem_limit: 8g
    cpu_shares: 2048
    security_opt:
      - no-new-privileges:true
    volumes:
      - /volume1/docker/chibisafe/db:/home/node/chibisafe/database:rw
      - /volume1/docker/chibisafe/uploads:/home/node/chibisafe/uploads:rw
      - /volume1/docker/chibisafe/logs:/home/node/chibisafe/logs:rw
    ports:
      - 2425:8000
    restart: on-failure:5

On my Docker host, I’ll set up some folders

builder@builder-T100:~$ cd chibiupload/
builder@builder-T100:~/chibiupload$ pwd
/home/builder/chibiupload
builder@builder-T100:~/chibiupload$ mkdir db
builder@builder-T100:~/chibiupload$ mkdir uploads
builder@builder-T100:~/chibiupload$ mkdir logs

I can then fire up a docker run version

$ docker run -d -p 4140:8000 --name=chibisafe -v /home/builder/chibiupload/db:/home/node/chibisafe/database:rw -v /
home/builder/chibiupload/uploads:/home/node/chibisafe/uploads:rw -v /home/builder/chibiupload/logs:/home/node/chibisafe/logs:rw chibisafe/chibisafe:latest
Unable to find image 'chibisafe/chibisafe:latest' locally
latest: Pulling from chibisafe/chibisafe
4abcf2066143: Pull complete
e7ced292c644: Pull complete
b32c0114bba5: Pull complete
f3748d9674b0: Pull complete
254711663c3b: Pull complete
bf2721150cd1: Pull complete
f423a62ee824: Pull complete
623a959eeb7b: Pull complete
2748763e5dea: Pull complete
b7483c6c8ed4: Pull complete
8fc321370221: Pull complete
e73c070be51f: Pull complete
a6bb8a55237c: Pull complete
527c2067fa17: Pull complete
973c462005e5: Pull complete
12d3e97842df: Pull complete
35aa0fe23ff4: Pull complete
Digest: sha256:79d5f6776476f64257e24d78830315b0bf746b3711662f2285b2eb2afb87b6b4
Status: Downloaded newer image for chibisafe/chibisafe:latest
0c3076158195f15cfd41f03adc38a78530a965493625489557a408ce083e790e

The first time launched, the login is admin/admin

/content/images/2024/03/chibisafe-01.png

I can update the password in Account/Credentials

/content/images/2024/03/chibisafe-02.png

Let’s try uploading a file

/content/images/2024/03/chibisafe-03.png

If I click the link, I see the image I uploaded

/content/images/2024/03/chibisafe-04.png

Under Dashboard/Uploads I can see the image as well

/content/images/2024/03/chibisafe-05.png

Interestingly, I could even use that image as a background image now using customization

/content/images/2024/03/chibisafe-06.png

Or our logo image

/content/images/2024/03/chibisafe-07.png

If we do a tree command, we can see the files have been stored in uploads

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

DNS

Before we explore farther, let’s create an A Record and setup an ingress

$ az account set --subscription "Pay-As-You-Go"
$ az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n upload
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "3c4824aa-6f02-45cc-bd9c-2ba74e568908",
  "fqdn": "upload.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/upload",
  "name": "upload",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "type": "Microsoft.Network/dnszones/A"
}

I can now create a simple YAML manifest to forward traffic through Kubernetes off to the upload tool

$ cat uploadtpkpw.yaml
---
apiVersion: v1
kind: Endpoints
metadata:
  name: upload-external-ip
subsets:
- addresses:
  - ip: 192.168.1.100
  ports:
  - name: uploadint
    port: 4140
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: upload-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: upload
    port: 80
    protocol: TCP
    targetPort: 4140
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    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: upload-external-ip
  generation: 1
  labels:
    app.kubernetes.io/instance: uploadingress
  name: uploadingress
spec:
  rules:
  - host: upload.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: upload-external-ip
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - upload.tpk.pw
    secretName: upload-tls
    
$ kubectl apply -f uploadtpkpw.yaml
endpoints/upload-external-ip created
service/upload-external-ip created
ingress.networking.k8s.io/upload created

Which works

/content/images/2024/03/chibisafe-09.png

I’ll fix the logo and site name

/content/images/2024/03/chibisafe-10.png

Helm

If we can do docker we can do as a Kubernetes manifest

/content/images/2024/03/chibisafe-11.png

In fact, let’s skip a step and just go right to helm

I’ll start with a new GIT repo

/content/images/2024/03/chibisafe-12.png

I worked with Copilot to work out some of the details and finished the Ingress myself

/content/images/2024/03/chibisafe-13.png

To test, let’s do a fresh instance

$ az account set --subscription "Pay-As-You-Go"
$ az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n chibisafe
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "b78f3420-707b-4722-b611-fa5c1013648b",
  "fqdn": "chibisafe.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/chibisafe",
  "name": "chibisafe",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "type": "Microsoft.Network/dnszones/A"
}

Then I can use that in the helm install

$ helm install --create-namespace -n chibisafe chibisafe --set persistence.storageClass=managed-nfs-storage --set ingress.hostFQDN=chibisafe.tpk.pw ./chibisafe/
NAME: chibisafe
LAST DEPLOYED: Sun Feb 25 15:14:14 2024
NAMESPACE: chibisafe
STATUS: deployed
REVISION: 1
TEST SUITE: None

Things spun up

$ kubectl get deployment -n chibisafe
NAME        READY   UP-TO-DATE   AVAILABLE   AGE
chibisafe   1/1     1            1           37s

$ kubectl get pvc -n chibisafe
NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
chibisafe-uploads    Bound    pvc-8d07a967-2b38-4e9b-9ca8-f15456c28640   10Gi       RWO            managed-nfs-storage   45s
chibisafe-database   Bound    pvc-4346c238-f340-46a2-972c-86ed530b16c1   1Gi        RWO            managed-nfs-storage   44s
chibisafe-logs       Bound    pvc-ed452b1e-099a-46fb-922a-9968f7fabc36   1Gi        RWO            managed-nfs-storage   45s

$ kubectl get ingress -n chibisafe
NAME                CLASS    HOSTS              ADDRESS                                                PORTS     AGE
chibisafe-ingress   <none>   chibisafe.tpk.pw   192.168.1.215,192.168.1.36,192.168.1.57,192.168.1.78   80, 443   50s

$ kubectl get cert -n chibisafe
NAME            READY   SECRET          AGE
chibisafe-tls   False   chibisafe-tls   51s
$ kubectl get cert -n chibisafe
NAME            READY   SECRET          AGE
chibisafe-tls   True    chibisafe-tls   90s

I can also upgrade

$ helm upgrade --install --create-namespace -n chibisafe chibisafe --set persistence.storageClass=managed-nfs-storage --set ingress.hostFQDN=chibisafe.tpk.pw ./chibisafe/
Release "chibisafe" has been upgraded. Happy Helming!
NAME: chibisafe
LAST DEPLOYED: Sun Feb 25 15:20:17 2024
NAMESPACE: chibisafe
STATUS: deployed
REVISION: 2
TEST SUITE: None

Which after working out some details in ports, wsa working

/content/images/2024/03/chibisafe-14.png

We can see the files show up on the PVC via the pod with a quick test

The last step is to package and push to Harbor

$ helm package chibisafe
Successfully packaged chart and saved it to: /home/builder/Workspaces/uploadToolsCharts/chibisafe-0.1.0.tgz

$ helm push chibisafe-0.1.0.tgz oci://harbor.freshbrewed.science/chartrepo
Pushed: harbor.freshbrewed.science/chartrepo/chibisafe:0.1.0
Digest: sha256:1d35df8e02dfab256b030c5355b4ba452d66632e9ff68226746422992d888dba

I was pretty sure I shared globally these charts, but just to be certain, I used an Azure Cloud Shell to check

isaac [ ~ ]$ helm pull oci://harbor.freshbrewed.science/chartrepo/chibisafe --version 0.1.0
Pulled: harbor.freshbrewed.science/chartrepo/chibisafe:0.1.0
Digest: sha256:1d35df8e02dfab256b030c5355b4ba452d66632e9ff68226746422992d888dba

/content/images/2024/03/chibisafe-27.png

PHP upload

Doing a quick search I found this PHP based containerized solution on Github: adrianlzt / docker-nginx-php-upload

This should be easy to get going. Since I plan to add some helm and YAMLs, I forked to my namespace at https://github.com/idjohnson/docker-nginx-php-upload

I’ll clone and build

$ git clone https://github.com/idjohnson/docker-nginx-php-upload.git
Cloning into 'docker-nginx-php-upload'...
remote: Enumerating objects: 13, done.
remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13
Unpacking objects: 100% (13/13), 2.78 KiB | 474.00 KiB/s, done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd docker-nginx-php-upload/
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ ls
Dockerfile  README.md  index.php  nginx.conf  upload_dir.sh
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ docker build -t nginxphpupload:0.0.1 .
[+] Building 21.6s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                           0.0s
 => => transferring dockerfile: 454B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                              0.0s
 => => transferring context: 2B                                                                                                0.0s
 => [internal] load metadata for docker.io/richarvey/nginx-php-fpm:latest                                                      2.1s
 => [auth] richarvey/nginx-php-fpm:pull token for registry-1.docker.io                                                         0.0s
 => [internal] load build context                                                                                              0.1s
 => => transferring context: 1.82kB                                                                                            0.0s
 => [1/7] FROM docker.io/richarvey/nginx-php-fpm:latest@sha256:fdaa086c82611024776048db9d1eec6324aa1058e1d867a15004c92d24df6  14.3s
 => => resolve docker.io/richarvey/nginx-php-fpm:latest@sha256:fdaa086c82611024776048db9d1eec6324aa1058e1d867a15004c92d24df61  0.0s
 => => sha256:320ac70804b0464ea34902d65908ffb2cc0ee6308ae57741ad4a2babf24c696b 6.35kB / 6.35kB                                 0.0s
 => => sha256:482c13708aabf63e831cec9a7a7c482e06954909130a8378bc2ea0555b092729 2.65MB / 2.65MB                                 0.3s
 => => sha256:d7669819f57db417efc1d47dd1b87f06a54415b54e0d8cab87dc8eb81b7dd74a 20.21kB / 20.21kB                               0.0s
 => => sha256:8a49fdb3b6a5ff2bd8ec6a86c05b2922a0f7454579ecc07637e94dfd1d0639b6 3.40MB / 3.40MB                                 0.4s
 => => sha256:4231935b557bcd8db7765211fa8740725f28e3126a7afa24ee1203806f31d6d9 1.26kB / 1.26kB                                 0.3s
 => => sha256:fdaa086c82611024776048db9d1eec6324aa1058e1d867a15004c92d24df6187 1.61kB / 1.61kB                                 0.0s
 => => sha256:c5d500e07dec3657e7a372a5ad5db361efcf83fab171693f93ac7983b18fb98e 12.04MB / 12.04MB                               1.1s
 => => sha256:2889ac5addf84973ce7a59ca03e0e8eee13d74b802798524b45eee73915efb46 268B / 268B                                     0.5s
 => => extracting sha256:8a49fdb3b6a5ff2bd8ec6a86c05b2922a0f7454579ecc07637e94dfd1d0639b6                                      0.4s
 => => sha256:47d27eee9758d0ababfc06f0d05c67fe0d95831f6a3a24c76e799f74c175aa8a 15.30MB / 15.30MB                               1.0s
 => => sha256:28fafcf5fc82310f86d0c2508ca0d0c37e63329371fa284dc8c8f47137f1a989 497B / 497B                                     0.6s
 => => sha256:b73ec7700db0bae04766a4db7250af33002ab008b2829eafa4cd4d9e9703ac76 2.45kB / 2.45kB                                 1.0s
 => => extracting sha256:482c13708aabf63e831cec9a7a7c482e06954909130a8378bc2ea0555b092729                                      0.3s
 => => sha256:af419af80d233d644a241622a0ec3038e61a86c946a37053a22e78ff209ae94d 18.98kB / 18.98kB                               1.2s
 => => sha256:5348f4af10b1f5913857037e9fb8cb260a0ab5c0123c19ffa97a6ad5cdabbe29 30.59kB / 30.59kB                               1.2s
 => => sha256:6e1e3a1c74d78db3b6b277be0568e9a77782a9f3b0c2befc7fc7b4a35e60b7e1 9.18kB / 9.18kB                                 1.2s
 => => sha256:606f4c9aa9ab7899dfb8df4c78512733cc327b8c90a8532c38a50237e90de922 99.74MB / 99.74MB                               3.8s
 => => sha256:3e1f7523e15df72dc4f51b29dd5e850dc80237293dc840c9a91b8ddb21222532 1.51MB / 1.51MB                                 1.4s
 => => extracting sha256:4231935b557bcd8db7765211fa8740725f28e3126a7afa24ee1203806f31d6d9                                      0.1s
 => => sha256:86ce28c6bc206fbacd8ff9b8b7a4d5947647466a99703077c49b8bed87093a4a 27.48MB / 27.48MB                               2.3s
 => => sha256:41ff54a93019a932d044158f0fa2800d12308b988406ed00cf297e3984a46d2b 861B / 861B                                     1.5s
 => => extracting sha256:2889ac5addf84973ce7a59ca03e0e8eee13d74b802798524b45eee73915efb46                                      0.1s
 => => sha256:700763ca11830e99c03f8cd89a7f2282c35327c59b6cfd8ec92a5bb5c5ad6652 153B / 153B                                     1.7s
 => => sha256:6a95c5916b15bf9f5f019ba51421873dccbcb460e990af60e1c55f1fa8e3c76a 878B / 878B                                     1.8s
 => => extracting sha256:c5d500e07dec3657e7a372a5ad5db361efcf83fab171693f93ac7983b18fb98e                                      0.2s
 => => sha256:033a2477c38fe4ab6bbfa903ead6430f2043fc99f491a4b8c824fbcaaa34e0ed 272B / 272B                                     2.1s
 => => extracting sha256:28fafcf5fc82310f86d0c2508ca0d0c37e63329371fa284dc8c8f47137f1a989                                      0.1s
 => => sha256:00a6cd309722ea2bf98df2bc5234ebd522c1635f4486feb601803ae4a1eb72a3 1.23kB / 1.23kB                                 2.3s
 => => extracting sha256:47d27eee9758d0ababfc06f0d05c67fe0d95831f6a3a24c76e799f74c175aa8a                                      1.3s
 => => sha256:ce3d977011807d08cc03e8517fb7eb2e73a72a4c6852c844b769d0ab40111c8b 1.32kB / 1.32kB                                 2.4s
 => => sha256:741860fe1dd4a4b9b93d43b92a08c41ce1c016775a53a1a194bb904c7c0a2f39 183B / 183B                                     2.5s
 => => sha256:b2959f58ee003645f020188ff48f586a54437b34fd986fdca0b42197f47e49d0 7.29kB / 7.29kB                                 2.5s
 => => sha256:9b447aafce4c54183cef9a0b0e7b0bab24e17bdc7573f1620c3ea0475a91ecad 24.18kB / 24.18kB                               2.8s
 => => sha256:f3ca4d22409b13dc9aeef652c8df9d64198e3693cdf013b34d86f495cfb0b28e 2.78kB / 2.78kB                                 2.7s
 => => extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                      0.0s
 => [2/7] RUN mkdir -p /run/nginx                                                                                              1.2s
 => [3/7] RUN apk add --no-cache nginx-mod-http-lua                                                                            1.7s
 => [4/7] RUN mkdir -p /var/www/html/uploads /var/www/html/conf/nginx/ /var/www/html/scripts /data                             0.6s
 => [5/7] COPY nginx.conf /var/www/html/conf/nginx/                                                                            0.0s
 => [6/7] COPY index.php /var/www/html/                                                                                        0.0s
 => [7/7] COPY upload_dir.sh /var/www/html/scripts/                                                                            0.0s
 => exporting to image                                                                                                         0.1s
 => => exporting layers                                                                                                        0.1s
 => => writing image sha256:7d52a5ee2617f542f67bb6ee4d69fbd1b80344732f35ed966f0f3a43adc9ad17                                   0.0s
 => => naming to docker.io/library/nginxphpupload:0.0.1                                                                        0.0s

I’ll fire a quick test

builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ docker run -p 8899:80 nginxphpupload:0.0.1
2024-02-25 22:18:23,941 WARN No file matches via include "/etc/supervisor/conf.d/*.conf"
2024-02-25 22:18:23,941 INFO Set uid to user 0 succeeded
2024-02-25 22:18:23,944 INFO RPC interface 'supervisor' initialized
2024-02-25 22:18:23,944 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2024-02-25 22:18:23,945 INFO supervisord started with pid 1
2024-02-25 22:18:24,948 INFO spawned: 'php-fpm' with pid 23
2024-02-25 22:18:24,950 INFO spawned: 'nginx' with pid 24
2024/02/25 22:18:24 [notice] 24#24: using the "epoll" event method
2024/02/25 22:18:24 [notice] 24#24: nginx/1.24.0
2024/02/25 22:18:24 [notice] 24#24: OS: Linux 5.15.133.1-microsoft-standard-WSL2
2024/02/25 22:18:24 [notice] 24#24: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/02/25 22:18:24 [notice] 24#24: start worker processes
2024/02/25 22:18:24 [notice] 24#24: start worker process 25
2024/02/25 22:18:24 [notice] 24#24: start worker process 26
2024/02/25 22:18:24 [notice] 24#24: start worker process 27
2024/02/25 22:18:24 [notice] 24#24: start worker process 28
2024/02/25 22:18:24 [notice] 24#24: start worker process 29
2024/02/25 22:18:24 [notice] 24#24: start worker process 30
2024/02/25 22:18:24 [notice] 24#24: start worker process 31
2024/02/25 22:18:24 [notice] 24#24: start worker process 32
2024/02/25 22:18:24 [notice] 24#24: start worker process 33
2024/02/25 22:18:24 [notice] 24#24: start worker process 34
2024/02/25 22:18:24 [notice] 24#24: start worker process 35
2024/02/25 22:18:24 [notice] 24#24: start worker process 36
2024/02/25 22:18:24 [notice] 24#24: start worker process 37
2024/02/25 22:18:24 [notice] 24#24: start worker process 38
2024/02/25 22:18:24 [notice] 24#24: start worker process 39
2024/02/25 22:18:24 [notice] 24#24: start worker process 40
[25-Feb-2024 22:18:24] NOTICE: fpm is running, pid 23
[25-Feb-2024 22:18:24] NOTICE: ready to handle connections
2024-02-25 22:18:25,997 INFO success: php-fpm entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2024-02-25 22:18:25,997 INFO success: nginx entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
172.17.0.1 - - [25/Feb/2024:22:18:26 +0000] "GET /livereload/625142004/625142389 HTTP/1.1" 404 485 "http://localhost:8899/about/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
172.17.0.1 - - [25/Feb/2024:22:18:30 +0000] "GET /livereload/625142004/625142389 HTTP/1.1" 404 485 "http://localhost:8899/about/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
172.17.0.1 - - [25/Feb/2024:22:18:31 +0000] "GET / HTTP/1.1" 200 326 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0"
2024/02/25 22:18:31 [error] 27#27: *3 open() "/var/www/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: _, request: "GET /favicon.ico HTTP/1.1", host: "localhost:8899", referrer: "http://localhost:8899/"
172.17.0.1 - - [25/Feb/2024:22:18:31 +0000] "GET /favicon.ico HTTP/1.1" 404 485 "http://localhost:8899/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0"
172.17.0.1 - - [25/Feb/2024:22:18:34 +0000] "GET /livereload/625142004/625142389 HTTP/1.1" 404 485 "http://localhost:8899/about/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"

/content/images/2024/03/chibisafe-15.png

I uploaded the same file as before

/content/images/2024/03/chibisafe-16.png

I can use Docker Desktop to see the file was put into an uploads subfolder

/content/images/2024/03/chibisafe-17.png

Let’s push a container anyone can use

$ docker tag nginxphpupload:0.0.1 idjohnson/nginxphpupload:0.0.1
$ docker push idjohnson/nginxphpupload:0.0.1
The push refers to repository [docker.io/idjohnson/nginxphpupload]
6adac3c394d8: Pushed
744e40ae342b: Pushed
2ecd13216bf1: Pushed
811377d61146: Pushed
086772f2bd82: Pushed
5416d47a3f93: Pushed
5f70bf18a086: Mounted from richarvey/nginx-php-fpm
030120f212b9: Mounted from richarvey/nginx-php-fpm
213b8dd2cc72: Mounted from richarvey/nginx-php-fpm
c2548f805609: Mounted from richarvey/nginx-php-fpm
b4fe1c47436b: Mounted from richarvey/nginx-php-fpm
f03f6ca13a17: Mounted from richarvey/nginx-php-fpm
1dad2d97c3a4: Mounted from richarvey/nginx-php-fpm
2c1b1e79b0d6: Mounted from richarvey/nginx-php-fpm
7526f640a87a: Mounted from richarvey/nginx-php-fpm
3ebadd0586ff: Mounted from richarvey/nginx-php-fpm
d0ed1d08d503: Mounted from richarvey/nginx-php-fpm
0e4f769d0ba5: Mounted from richarvey/nginx-php-fpm
e923cad0972d: Mounted from richarvey/nginx-php-fpm
7023d2d2f53f: Mounted from richarvey/nginx-php-fpm
d02624f6b23c: Mounted from richarvey/nginx-php-fpm
af068dca00a8: Mounted from richarvey/nginx-php-fpm
3703a89b34b2: Mounted from richarvey/nginx-php-fpm
8ec37037bfbf: Mounted from richarvey/nginx-php-fpm
f4bcf221c4b1: Mounted from richarvey/nginx-php-fpm
35eb56907d91: Mounted from richarvey/nginx-php-fpm
9ad36a0f1de1: Mounted from richarvey/nginx-php-fpm
b755a0594558: Mounted from richarvey/nginx-php-fpm
30b446f2b9c3: Mounted from richarvey/nginx-php-fpm
0d051a39fc49: Mounted from richarvey/nginx-php-fpm
0fa05acb49e4: Mounted from richarvey/nginx-php-fpm
66f266ec81c1: Mounted from richarvey/nginx-php-fpm
cb4ba546b449: Mounted from richarvey/nginx-php-fpm
ab74b45bffd4: Mounted from richarvey/nginx-php-fpm
9bbacedbd5e4: Mounted from richarvey/nginx-php-fpm
1a72c946ba2b: Mounted from richarvey/nginx-php-fpm
e759f13eb8bc: Mounted from richarvey/nginx-php-fpm
bb01bd7e32b5: Mounted from richarvey/nginx-php-fpm
0.0.1: digest: sha256:6e1756a831388a6974941e5b18543f519f5db98fc5ef0faadd53ae89936c1c1c size: 8234

To turn this into a container with which I can use a PVC, I’ll want to stub out the ‘mkdir’ line on uploads

$ cat Dockerfile
FROM richarvey/nginx-php-fpm:latest
RUN mkdir -p /run/nginx
RUN apk add --no-cache nginx-mod-http-lua
ENV PHP_UPLOAD_MAX_FILESIZE 10000
ENV PHP_POST_MAX_SIZE 10000
ENV PHP_MEM_LIMIT 10000
ENV RUN_SCRIPTS 1
#RUN mkdir -p /var/www/html/uploads /var/www/html/conf/nginx/ /var/www/html/scripts /data
RUN mkdir -p /var/www/html/conf/nginx/ /var/www/html/scripts /data
COPY nginx.conf /var/www/html/conf/nginx/
COPY index.php /var/www/html/
COPY upload_dir.sh /var/www/html/scripts/

I’ll build and push that

builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ docker build -t idjohnson/nginxphpupload:0.0.2 .
[+] Building 3.8s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                                           0.0s
 => => transferring dockerfile: 522B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                              0.0s
 => => transferring context: 2B                                                                                                0.0s
 => [internal] load metadata for docker.io/richarvey/nginx-php-fpm:latest                                                      1.6s
 => [auth] richarvey/nginx-php-fpm:pull token for registry-1.docker.io                                                         0.0s
 => [internal] load build context                                                                                              0.0s
 => => transferring context: 93B                                                                                               0.0s
 => [1/7] FROM docker.io/richarvey/nginx-php-fpm:latest@sha256:fdaa086c82611024776048db9d1eec6324aa1058e1d867a15004c92d24df61  0.0s
 => CACHED [2/7] RUN mkdir -p /run/nginx                                                                                       0.0s
 => CACHED [3/7] RUN apk add --no-cache nginx-mod-http-lua                                                                     0.0s
 => [4/7] RUN mkdir -p /var/www/html/conf/nginx/ /var/www/html/scripts /data                                                   0.5s
 => [5/7] COPY nginx.conf /var/www/html/conf/nginx/                                                                            0.0s
 => [6/7] COPY index.php /var/www/html/                                                                                        0.0s
 => [7/7] COPY upload_dir.sh /var/www/html/scripts/                                                                            0.0s
 => exporting to image                                                                                                         0.1s
 => => exporting layers                                                                                                        0.1s
 => => writing image sha256:ab8858a21772d13d26d3928d9cb3691660730740c9c02e80841f5fd4ce72ea2b                                   0.0s
 => => naming to docker.io/idjohnson/nginxphpupload:0.0.2                                                                      0.0s
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ docker push idjohnson/nginxphpupload:0.0.2
The push refers to repository [docker.io/idjohnson/nginxphpupload]
6c43e4c10457: Pushed
70c80b192449: Pushed
0ed8be2eacb9: Pushed
9cc4282e166c: Pushed
086772f2bd82: Layer already exists
5416d47a3f93: Layer already exists
5f70bf18a086: Layer already exists
030120f212b9: Layer already exists
213b8dd2cc72: Layer already exists
c2548f805609: Layer already exists
b4fe1c47436b: Layer already exists
f03f6ca13a17: Layer already exists
1dad2d97c3a4: Layer already exists
2c1b1e79b0d6: Layer already exists
7526f640a87a: Layer already exists
3ebadd0586ff: Layer already exists
d0ed1d08d503: Layer already exists
0e4f769d0ba5: Layer already exists
e923cad0972d: Layer already exists
7023d2d2f53f: Layer already exists
d02624f6b23c: Layer already exists
af068dca00a8: Layer already exists
3703a89b34b2: Layer already exists
8ec37037bfbf: Layer already exists
f4bcf221c4b1: Layer already exists
35eb56907d91: Layer already exists
9ad36a0f1de1: Layer already exists
b755a0594558: Layer already exists
30b446f2b9c3: Layer already exists
0d051a39fc49: Layer already exists
0fa05acb49e4: Layer already exists
66f266ec81c1: Layer already exists
cb4ba546b449: Layer already exists
ab74b45bffd4: Layer already exists
9bbacedbd5e4: Layer already exists
1a72c946ba2b: Layer already exists
e759f13eb8bc: Layer already exists
bb01bd7e32b5: Layer already exists
0.0.2: digest: sha256:245a2e54892173aa788a40e9621f71339e696c59fcfd4b90c103d884804b0b64 size: 8234

I’ll whip up a manifest

$ cat k8s.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: upload-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxphpupload-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginxphpupload
  template:
    metadata:
      labels:
        app: nginxphpupload
    spec:
      containers:
      - name: nginxphpupload
        image: idjohnson/nginxphpupload:0.0.2
        ports:
        - containerPort: 80
        volumeMounts:
        - name: upload-volume
          mountPath: /var/www/html/uploads
      volumes:
      - name: upload-volume
        persistentVolumeClaim:
          claimName: upload-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: nginxphpupload-service
spec:
  selector:
    app: nginxphpupload
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

I’ll apply it

$ kubectl apply -f ./k8s.yaml
persistentvolumeclaim/upload-pvc created
deployment.apps/nginxphpupload-deployment created
service/nginxphpupload-service created

A quick port-forward shows it works

$ kubectl port-forward svc/nginxphpupload-service 8899:80
Forwarding from 127.0.0.1:8899 -> 80
Forwarding from [::1]:8899 -> 80
Handling connection for 8899
Handling connection for 8899
Handling connection for 8899

/content/images/2024/03/chibisafe-18.png

I’ll test an upload

/content/images/2024/03/chibisafe-19.png

And I can verify it worked

$ kubectl exec nginxphpupload-deployment-755774dfcd-scbw9 'tree'
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
.
├── conf
│   └── nginx
│       └── nginx.conf
├── index.php
├── logo.png
├── scripts
│   └── upload_dir.sh
└── uploads
    └── resume.pdf

4 directories, 5 files

I’ll next create some charts

/content/images/2024/03/chibisafe-20.png

To test, I’ll need an A record

$ az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n quickupload
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "ea01bba5-642f-4afb-94bc-63ee6c6c13f3",
  "fqdn": "quickupload.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/quickupload",
  "name": "quickupload",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "type": "Microsoft.Network/dnszones/A"
}

Then quick test

$ helm install -n quickupload --create-namespace quickupload ./helm/
NAME: quickupload
LAST DEPLOYED: Sun Feb 25 16:42:42 2024
NAMESPACE: quickupload
STATUS: deployed
REVISION: 1
TEST SUITE: None

I can see the ingress created

$ kubectl get ingress -n quickupload
NAME                               CLASS    HOSTS                ADDRESS                                                PORTS     AGE
quickupload-simpleupload-ingress   <none>   quickupload.tpk.pw   192.168.1.215,192.168.1.36,192.168.1.57,192.168.1.78   80, 443   84s

A small upload works fine

/content/images/2024/03/chibisafe-21.png

But a 1.5Mb file gets rejected

/content/images/2024/03/chibisafe-22.png

We had some env vars in the Dockerfile as you may recall

ENV PHP_UPLOAD_MAX_FILESIZE 10000
ENV PHP_POST_MAX_SIZE 10000
ENV PHP_MEM_LIMIT 10000
ENV RUN_SCRIPTS 1

I can add those to my deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "simpleupload.fullname" . }}
  labels:
    {{- include "simpleupload.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: {{ include "simpleupload.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "simpleupload.name" . }}
    spec:
      containers:
      - name: nginxphpupload
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - name: http
          containerPort: {{ .Values.service.targetPort }}
          protocol: TCP
        env:
        - name: PHP_UPLOAD_MAX_FILESIZE
          value: "{{ .Values.env.PHP_UPLOAD_MAX_FILESIZE }}"
        - name: PHP_POST_MAX_SIZE
          value: "{{ .Values.env.PHP_POST_MAX_SIZE }}"
        - name: PHP_MEM_LIMIT
          value: "{{ .Values.env.PHP_MEM_LIMIT }}"
        volumeMounts:
        - name: upload-volume
          mountPath: /var/www/html/uploads
      volumes:
      - name: upload-volume
        persistentVolumeClaim:
          claimName: {{ include "simpleupload.fullname" . }}-pvc

Then a larger value that the default in my values.yaml

env:
  PHP_UPLOAD_MAX_FILESIZE: 100000
  PHP_POST_MAX_SIZE: 100000
  PHP_MEM_LIMIT: 100000

I can update

$ helm upgrade --install -n quickupload --create-namespace quickupload ./helm/
Release "quickupload" has been upgraded. Happy Helming!
NAME: quickupload
LAST DEPLOYED: Sun Feb 25 19:08:54 2024
NAMESPACE: quickupload
STATUS: deployed
REVISION: 2
TEST SUITE: None

I can see the settings were passed

$ kubectl describe pod quickupload-simpleupload-7c6b6f969-2qt28 -n quickupload | tail -n 47 | head -n 19
Controlled By:  ReplicaSet/quickupload-simpleupload-7c6b6f969
Containers:
  nginxphpupload:
    Container ID:   containerd://7837372597e5fc9b082e26871970b3951b0ffeb2981c2992778a9d35baa5d6f7
    Image:          idjohnson/nginxphpupload:0.0.2
    Image ID:       docker.io/idjohnson/nginxphpupload@sha256:245a2e54892173aa788a40e9621f71339e696c59fcfd4b90c103d884804b0b64
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sun, 25 Feb 2024 19:09:37 -0600
    Ready:          True
    Restart Count:  0
    Environment:
      PHP_UPLOAD_MAX_FILESIZE:  100000
      PHP_POST_MAX_SIZE:        100000
      PHP_MEM_LIMIT:            100000
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-c8zcg (ro)
      /var/www/html/uploads from upload-volume (rw)

But I still got an error

/content/images/2024/03/chibisafe-23.png

In the container, i do see updated valeus

$ kubectl exec -it quickupload-simpleupload-7c6b6f969-2qt28 -n quickupload -- /bin/sh
/var/www/html # export | grep PHP | grep '100'
export PHP_MEM_LIMIT='100000'
export PHP_POST_MAX_SIZE='100000'
export PHP_UPLOAD_MAX_FILESIZE='100000'

I’ll try as value with a unit like M for Megabytes

$ helm upgrade --install -n quickupload --create-namespace quickupload --set env.PHP_UPLOAD_MAX_FILESIZE=16M --set env.PHP_POST_MAX_SIZE=16M --set env.PHP_MEM_LIMIT=128M ./helm/
Release "quickupload" has been upgraded. Happy Helming!
NAME: quickupload
LAST DEPLOYED: Sun Feb 25 19:15:56 2024
NAMESPACE: quickupload
STATUS: deployed
REVISION: 3
TEST SUITE: None

In the end, I had a few more annotations to sort out (namely setting to “0” the max)

    cert-manager.io/cluster-issuer: azuredns-tpkpw
    field.cattle.io/publicEndpoints: '[{"addresses":["192.168.1.215","192.168.1.36","192.168.1.57","192.168.1.78"],"port":443,"protocol":"HTTPS","serviceName":"quickupload:quickupload-simpleupload","ingressName":"quickupload:quickupload-simpleupload-ingress","hostname":"quickupload.tpk.pw","path":"/","allNodes":false}]'
    ingress.kubernetes.io/proxy-body-size: "0"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    meta.helm.sh/release-name: quickupload
    meta.helm.sh/release-namespace: quickupload
    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.org/client-max-body-size: "0"
    nginx.org/websocket-services: quickupload-simpleupload

But now I could upload a 1.5Mb file

/content/images/2024/03/chibisafe-24.png

I can see the files

$ kubectl exec -it -n quickupload quickupload-simpleupload-76f6c4b748-qm592 -- /bin/sh
/var/www/html # ls -ltrah ./uploads/
total 2M
-rw-r--r--    1 nginx    nginx     251.3K Feb 25 22:46 resume.pdf
drwxr-xr-x    1 nginx    nginx       4.0K Feb 26 01:16 ..
-rw-r--r--    1 nginx    nginx       1.5M Feb 26 01:23 downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
drwxrwxrwx    2 nginx    nginx       4.0K Feb 26 01:23 .

Usage

I could imagine asking people to upload a submission for some contest using the URL

Say I had the index.php as

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
  <title>CONTEST: Upload your submission</title>
</head>
<body>
  <form enctype="multipart/form-data" action="index.php" method="POST">
    <p>Upload your submisison for our Contest</p>
    <input type="file" name="uploaded_file"></input><br />
    <input type="submit" value="Upload"></input>
  </form>
</body>
</html>
<?PHP
  if(!empty($_FILES['uploaded_file']))
  {
    $path = "/var/www/html/uploads/";
    $path = $path . basename( $_FILES['uploaded_file']['name']);

    if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $path)) {
      echo "The file ".  basename( $_FILES['uploaded_file']['name']).
      " has been uploaded";
    } else{
        echo "There was an error uploading the file, please try again!";
    }
  }
?>

An entrant would go there to the submission upload URL

/content/images/2024/03/chibisafe-25.png

Then upload a file

/content/images/2024/03/chibisafe-26.png

I might then check daily for new submissions

/var/www/html # ls -ltra ./uploads/
total 4600
-rw-r--r--    1 nginx    nginx       257307 Feb 25 22:46 resume.pdf
drwxr-xr-x    1 nginx    nginx         4096 Feb 26 01:16 ..
-rw-r--r--    1 nginx    nginx      1575682 Feb 26 01:23 downtown_berlin_corperate_office_building_clean_and_bright_2_78710595-fe99-4b58-984d-54eb5e32851d.png
-rw-r--r--    1 nginx    nginx      2861389 Feb 26 01:29 000001.csv
drwxrwxrwx    2 nginx    nginx         4096 Feb 26 01:29 .

Then pull them down

builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ kubectl cp -n quickupload quickupload-simpleupload-76f6c4b748-qm592:/var/www/html/uploads/000001.csv ./user-000001.csv
tar: Removing leading `/' from member names
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ ls -ltra user-000001.csv
-rw-r--r-- 1 builder builder 2861389 Feb 25 19:30 user-000001.csv

As you can see, the Docker container is on Zulu and I’m on CST so the time stamps would match your local TZ.

I’ll now package and push the helm chart

builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ mv helm quickupload
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ helm package quickupload
Successfully packaged chart and saved it to: /home/builder/Workspaces/docker-nginx-php-upload/simpleupload-0.1.3.tgz
builder@DESKTOP-QADGF36:~/Workspaces/docker-nginx-php-upload$ helm push simpleupload-0.1.3.tgz oci://harbor.freshbrewed.science/chartrepo
Pushed: harbor.freshbrewed.science/chartrepo/simpleupload:0.1.3
Digest: sha256:2bc087d336d971fe6545301ee808f428d666a88b5098affd839536c1e33438a1

I also pushed it to Github for anyone else to enjoy

$ git push
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 16 threads
Compressing objects: 100% (13/13), done.
Writing objects: 100% (14/14), 3.41 KiB | 3.41 MiB/s, done.
Total 14 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/idjohnson/docker-nginx-php-upload.git
   e100b0b..b061106  master -> master

Summary

I hope you enjoyed looking at these two very different ways of using a containerized web application to receive files. Chibisafe is definitely the more mature and full featured option. It has many customizations and a rather slick looking UI. While you can use the Docker compose as listed on the Github page, you can also pull the latest Helm chart of mine: helm pull oci://harbor.freshbrewed.science/chartrepo/chibisafe --version 0.1.0

With the PHP Upload, which I forked from https://github.com/adrianlzt/docker-nginx-php-upload.gi, it is a far more simple upload tool. Because the core of it is a basic index.php, it’s rather easy to tweak to suite one’s needs. As before, you can build your own docker image and run it docker run --rm -it -v "$PWD/data:/data" -p 32773:80 adrianlzt/docker-nginx-php-upload. However, as with Chibisafe, you are welcome to pull the latest Helm chart of mine to use helm pull oci://harbor.freshbrewed.science/chartrepo/simpleupload --version 0.1.3

Kubernetes Helm Upload OpenSource

Have something to add? Feedback? Try our new forums

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