Pendahuluan
Shlink adalah URL shortener open-source yang powerful dan self-hosted. Dalam tutorial ini, kita akan deploy Shlink di Kubernetes cluster dengan:
- ✅ MariaDB 11.6 sebagai database
- ✅ Secure secrets management (tanpa hardcode password)
- ✅ High availability (2 replicas)
- ✅ Public access via Cloudflare Tunnel
- ✅ Web UI untuk management
- ✅ Real-time analytics & tracking
Arsitektur
1
2
3
4
5
6
7
8
| Internet
↓
Cloudflare Tunnel (cloudflared)
↓
Kubernetes Services
├── Shlink API (2 replicas)
├── Shlink Web Client (1 replica)
└── MariaDB Database (1 replica)
|
Prerequisites
- Kubernetes cluster (k3s) yang sudah running
- kubectl access
- Cloudflare account dengan domain
- Cloudflare Tunnel sudah di-setup
- Minimal 2GB RAM available
Step 1: Persiapan Namespace
Buat namespace dedicated untuk Shlink:
1
| kubectl create namespace shlink
|
Verify namespace:
1
| kubectl get namespaces | grep shlink
|
Step 2: Generate Secure Passwords
Generate strong random passwords untuk database dan API key:
1
2
3
4
| # Generate 3 passwords
openssl rand -base64 32 # Database root password
openssl rand -base64 32 # Database user password
openssl rand -base64 32 # Shlink API key
|
⚠️ Simpan passwords ini di password manager!
Step 3: Create Kubernetes Secrets
Buat secrets untuk menyimpan credentials (ganti YOUR_PASSWORD_HERE dengan password yang di-generate):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Database secret
kubectl create secret generic shlink-db-secret \
--from-literal=MYSQL_ROOT_PASSWORD='YOUR_ROOT_PASSWORD' \
--from-literal=MYSQL_DATABASE='shlink' \
--from-literal=MYSQL_USER='shlink_user' \
--from-literal=MYSQL_PASSWORD='YOUR_USER_PASSWORD' \
--namespace=shlink
# Application secret
kubectl create secret generic shlink-app-secret \
--from-literal=DEFAULT_DOMAIN='sh.yourdomain.com' \
--from-literal=INITIAL_API_KEY='YOUR_API_KEY' \
--from-literal=GEOLITE_LICENSE_KEY='' \
--namespace=shlink
|
Verify secrets:
1
| kubectl get secrets -n shlink
|
Output:
1
2
3
| NAME TYPE DATA AGE
shlink-app-secret Opaque 3 10s
shlink-db-secret Opaque 4 15s
|
Step 4: Deploy MariaDB Database
Create file shlink-database.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
| apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shlink-db-pvc
namespace: shlink
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: shlink-db
namespace: shlink
labels:
app: shlink-db
spec:
replicas: 1
selector:
matchLabels:
app: shlink-db
template:
metadata:
labels:
app: shlink-db
spec:
containers:
- name: mariadb
image: mariadb:11.6
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: shlink-db-secret
key: MYSQL_ROOT_PASSWORD
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: shlink-db-secret
key: MYSQL_DATABASE
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: shlink-db-secret
key: MYSQL_USER
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: shlink-db-secret
key: MYSQL_PASSWORD
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 20
periodSeconds: 5
volumes:
- name: db-data
persistentVolumeClaim:
claimName: shlink-db-pvc
---
apiVersion: v1
kind: Service
metadata:
name: shlink-db
namespace: shlink
spec:
selector:
app: shlink-db
ports:
- port: 3306
targetPort: 3306
clusterIP: None
|
Deploy:
1
| kubectl apply -f shlink-database.yaml
|
Monitor deployment:
1
| kubectl get pods -n shlink -w
|
Tunggu hingga pod status 1/1 Running (sekitar 1-2 menit).
Step 5: Deploy Shlink Application
Create file shlink-application.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
| apiVersion: v1
kind: ConfigMap
metadata:
name: shlink-config
namespace: shlink
data:
SHORT_DOMAIN_HOST: "sh.yourdomain.com"
SHORT_DOMAIN_SCHEMA: "https"
TIMEZONE: "Asia/Jakarta"
DB_DRIVER: "maria"
DB_NAME: "shlink"
DB_USER: "shlink_user"
DB_HOST: "shlink-db"
DB_PORT: "3306"
REDIRECT_STATUS_CODE: "302"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: shlink
namespace: shlink
spec:
replicas: 2
selector:
matchLabels:
app: shlink
template:
metadata:
labels:
app: shlink
spec:
containers:
- name: shlink
image: shlinkio/shlink:4.2.4
ports:
- containerPort: 8080
env:
- name: SHORT_DOMAIN_HOST
valueFrom:
configMapKeyRef:
name: shlink-config
key: SHORT_DOMAIN_HOST
- name: SHORT_DOMAIN_SCHEMA
valueFrom:
configMapKeyRef:
name: shlink-config
key: SHORT_DOMAIN_SCHEMA
- name: TIMEZONE
valueFrom:
configMapKeyRef:
name: shlink-config
key: TIMEZONE
- name: DB_DRIVER
valueFrom:
configMapKeyRef:
name: shlink-config
key: DB_DRIVER
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: shlink-config
key: DB_NAME
- name: DB_USER
valueFrom:
configMapKeyRef:
name: shlink-config
key: DB_USER
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: shlink-config
key: DB_HOST
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: shlink-db-secret
key: MYSQL_PASSWORD
- name: DEFAULT_DOMAIN
valueFrom:
secretKeyRef:
name: shlink-app-secret
key: DEFAULT_DOMAIN
- name: INITIAL_API_KEY
valueFrom:
secretKeyRef:
name: shlink-app-secret
key: INITIAL_API_KEY
livenessProbe:
httpGet:
path: /rest/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /rest/health
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: shlink-service
namespace: shlink
spec:
selector:
app: shlink
ports:
- port: 80
targetPort: 8080
type: ClusterIP
|
Deploy:
1
| kubectl apply -f shlink-application.yaml
|
Verify:
1
2
| kubectl get pods -n shlink
kubectl logs -n shlink -l app=shlink --tail=20
|
Step 6: Deploy Web Client (Optional)
Create file shlink-web-client.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| apiVersion: apps/v1
kind: Deployment
metadata:
name: shlink-web-client
namespace: shlink
spec:
replicas: 1
selector:
matchLabels:
app: shlink-web-client
template:
metadata:
labels:
app: shlink-web-client
spec:
containers:
- name: web-client
image: shlinkio/shlink-web-client:4.2.1
ports:
- containerPort: 8080
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: shlink-web-client
namespace: shlink
spec:
selector:
app: shlink-web-client
ports:
- port: 80
targetPort: 8080
type: ClusterIP
|
Deploy:
1
| kubectl apply -f shlink-web-client.yaml
|
Get service cluster IPs:
1
| kubectl get svc -n shlink
|
Update Cloudflare Tunnel config (~/.cloudflared/config.yml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| tunnel: YOUR_TUNNEL_ID
credentials-file: /path/to/credentials.json
ingress:
# Shlink Admin UI
- hostname: sh-admin.yourdomain.com
service: http://SHLINK_WEB_CLIENT_IP:80
# Shlink API
- hostname: sh.yourdomain.com
service: http://SHLINK_SERVICE_IP:80
# Catch-all
- service: http_status:404
|
Update Kubernetes configmap:
1
2
3
4
5
6
| kubectl delete configmap cloudflared-config -n cloudflare
kubectl create configmap cloudflared-config \
--from-file=config.yaml=~/.cloudflared/config.yml \
-n cloudflare
kubectl rollout restart deployment cloudflared -n cloudflare
|
Add DNS routes:
1
2
| cloudflared tunnel route dns YOUR_TUNNEL_NAME sh.yourdomain.com
cloudflared tunnel route dns YOUR_TUNNEL_NAME sh-admin.yourdomain.com
|
Test Health Endpoint
1
| curl https://sh.yourdomain.com/rest/health
|
Expected output:
1
2
3
4
| {
"status": "pass",
"version": "4.2.4"
}
|
Access Web UI
- Open browser:
https://sh-admin.yourdomain.com - Click “Add server”
- Server URL:
https://sh.yourdomain.com - API Key: (retrieve dengan command)
1
2
| kubectl get secret shlink-app-secret -n shlink \
-o jsonpath='{.data.INITIAL_API_KEY}' | base64 -d
|
- Click “Add server”
Create First Short URL
Via Web UI:
- Click “Create short URL”
- Long URL:
https://example.com - Custom slug:
example - Click “Create”
Result: https://sh.yourdomain.com/example
Via API:
1
2
3
4
5
6
7
8
9
10
| API_KEY=$(kubectl get secret shlink-app-secret -n shlink \
-o jsonpath='{.data.INITIAL_API_KEY}' | base64 -d)
curl -X POST https://sh.yourdomain.com/rest/v3/short-urls \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"longUrl": "https://example.com",
"customSlug": "example"
}'
|
Troubleshooting
Pods Tidak Running
1
2
3
4
5
| # Check pod events
kubectl describe pod -n shlink POD_NAME
# Check logs
kubectl logs -n shlink POD_NAME
|
Database Connection Error
1
2
3
| # Test database connection
POD=$(kubectl get pod -n shlink -l app=shlink-db -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n shlink $POD -- mariadb -u shlink_user -p shlink -e "SHOW DATABASES;"
|
Cloudflare Tunnel Not Connecting
1
2
| # Check tunnel logs
kubectl logs -n cloudflare -l app=cloudflared --tail=50
|
Maintenance
Backup Database
1
2
3
| POD=$(kubectl get pod -n shlink -l app=shlink-db -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n shlink $POD -- mysqldump -u root -p \
--all-databases > shlink-backup-$(date +%Y%m%d).sql
|
Update Shlink
1
2
3
4
5
| # Update image version
kubectl set image deployment/shlink shlink=shlinkio/shlink:4.3.0 -n shlink
# Watch rollout
kubectl rollout status deployment/shlink -n shlink
|
Scale Replicas
1
2
3
4
5
| # Scale up
kubectl scale deployment shlink -n shlink --replicas=3
# Scale down
kubectl scale deployment shlink -n shlink --replicas=1
|
Security Best Practices
- ✅ Never commit secrets to Git
- ✅ Use strong random passwords
- ✅ Rotate credentials regularly
- ✅ Enable Cloudflare WAF
- ✅ Monitor access logs
- ✅ Keep images updated
Resource Requirements
| Component | CPU Request | Memory Request | CPU Limit | Memory Limit |
|---|
| MariaDB | 250m | 256Mi | 500m | 512Mi |
| Shlink App (×2) | 250m | 256Mi | 500m | 512Mi |
| Web Client | 100m | 64Mi | 200m | 128Mi |
| Total | 850m | 832Mi | 1700m | 1664Mi |
Kesimpulan
Kita telah berhasil deploy Shlink URL shortener di Kubernetes dengan:
- ✅ Secure secrets management
- ✅ High availability (2 replicas)
- ✅ Persistent storage untuk database
- ✅ Public access via Cloudflare Tunnel
- ✅ Web UI untuk management
- ✅ Production-ready configuration
Referensi
Published: January 05, 2025 *Tags: #shlink #k8s #cloudflare #url-shortener #self-hosted *