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
I can update the password in Account/Credentials
Let’s try uploading a file
If I click the link, I see the image I uploaded
Under Dashboard/Uploads I can see the image as well
Interestingly, I could even use that image as a background image now using customization
Or our logo image
If we do a tree command, we can see the files have been stored in uploads
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
I’ll fix the logo and site name
Helm
If we can do docker we can do as a Kubernetes manifest
In fact, let’s skip a step and just go right to helm
I’ll start with a new GIT repo
I worked with Copilot to work out some of the details and finished the Ingress myself
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
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
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"
I uploaded the same file as before
I can use Docker Desktop to see the file was put into an uploads subfolder
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
I’ll test an upload
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
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
But a 1.5Mb file gets rejected
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
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
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
Then upload a file
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