add k8s support and upload image to ghcr.io

This commit is contained in:
2026-07-16 19:06:33 +03:00
parent 36a1cdee1d
commit 4020fedf9a
7 changed files with 293 additions and 7 deletions
+25
View File
@@ -0,0 +1,25 @@
.git
.github
.env
.env.*
.venv
venv
ENV
__pycache__
*.py[cod]
stats.db
*.db
*.db-journal
*.db-shm
*.db-wal
.pytest_cache
.mypy_cache
.ruff_cache
run.sh
docker-compose*.yml
docker-compose*.yaml
+2
View File
@@ -3,4 +3,6 @@ MARZBAN_URL=https://your-marzban.example.com
MARZBAN_USERNAME=
MARZBAN_PASSWORD=
ADMIN_IDS=12345678,87654321
# Optional; remove/comment this line when no proxy is required.
# HTTP_PROXY=http://shared-http-proxy.proxy.svc.cluster.local:3128
DB_PATH=stats.db
+55
View File
@@ -0,0 +1,55 @@
name: Publish container
on:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/kr0sh512/tt-simple
tags: |
type=ref,event=tag
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and publish image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+21 -6
View File
@@ -1,11 +1,26 @@
FROM python:3.12-slim
FROM python:3.12-slim-bookworm
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN groupadd --gid 1000 app \
&& useradd \
--uid 1000 \
--gid 1000 \
--no-create-home \
--shell /usr/sbin/nologin \
app
COPY bot.py .
COPY .env .
COPY requirements.txt ./
RUN pip install \
--no-cache-dir \
--disable-pip-version-check \
-r requirements.txt
CMD sh -c "export $(grep -v '^#' .env | xargs) && python bot.py"
COPY --chown=app:app bot.py ./
USER app:app
CMD ["python", "bot.py"]
+96 -1
View File
@@ -42,10 +42,105 @@ cp .env.example .env
Fill `.env` and run:
```bash
export $(grep -v '^#' .env | xargs)
set -a
source .env
set +a
python bot.py
```
## Container image
The image contains only the application and Python dependencies. Runtime secrets
and the SQLite database are deliberately excluded.
Build and verify it locally:
```bash
docker build --pull -t tt-simple:dev .
docker run --rm --entrypoint sh tt-simple:dev -c \
'test ! -e /app/.env && test ! -e /app/stats.db && test -e /app/bot.py'
```
Run it with environment variables and persistent SQLite storage:
```bash
docker volume create tt-simple-data
docker run --rm \
--env-file .env \
--env DB_PATH=/data/stats.db \
--volume tt-simple-data:/data \
tt-simple:dev
```
### Publishing to GHCR
`.github/workflows/container.yml` publishes a multi-architecture image to:
```text
ghcr.io/kr0sh512/tt-simple
```
A push to `main` publishes `latest` and `sha-<commit>` tags. A Git tag such as
`v0.1.0` publishes the matching version tag:
```bash
git tag v0.1.0
git push origin main v0.1.0
```
### K3s deployment
Create the namespace first:
```bash
kubectl apply -f k8s/namespace.yaml
```
Create or update the application Secret from the local `.env` file. The Secret
is never stored in Git:
```bash
kubectl -n tt-simple create secret generic tt-simple-env \
--from-env-file=.env \
--dry-run=client -o yaml | kubectl apply -f -
```
For a private GHCR package, create a classic GitHub token with `read:packages`
and create the registry pull secret:
```bash
read -rsp "GHCR token: " GHCR_TOKEN
echo
kubectl -n tt-simple create secret docker-registry ghcr-creds \
--docker-server=ghcr.io \
--docker-username=kr0sh512 \
--docker-password="$GHCR_TOKEN" \
--dry-run=client -o yaml | kubectl apply -f -
unset GHCR_TOKEN
```
If the package is public, remove `imagePullSecrets` from
`k8s/tt-simple.yaml`. Deploy the PVC and bot after publishing the `v0.1.0`
image:
```bash
kubectl apply -f k8s/tt-simple.yaml
kubectl -n tt-simple rollout status deployment/tt-simple
kubectl -n tt-simple logs -f deployment/tt-simple
```
The manifest injects the Secret, mounts persistent storage at `/data`, and sets
`DB_PATH=/data/stats.db`. To use the shared non-Russian proxy, add this to the
local `.env` before updating `tt-simple-env`:
```dotenv
HTTP_PROXY=http://shared-http-proxy.proxy.svc.cluster.local:3128
```
The included namespace and pod labels satisfy the proxy `NetworkPolicy`.
## Notes
- Telegram Stars invoices use `currency="XTR"` and empty `provider_token`.
- Local SQLite database stores only payment statistics. User state is read from Marzban API.
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: tt-simple
labels:
# Required by the shared proxy NetworkPolicy when HTTP_PROXY is enabled.
proxy-access: "true"
+87
View File
@@ -0,0 +1,87 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tt-simple-data
namespace: tt-simple
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 256Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tt-simple
namespace: tt-simple
labels:
app.kubernetes.io/name: tt-simple
spec:
replicas: 1
strategy:
type: Recreate
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: tt-simple
template:
metadata:
labels:
app.kubernetes.io/name: tt-simple
# Required by the shared proxy NetworkPolicy when HTTP_PROXY is enabled.
proxy-client: "true"
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
imagePullSecrets:
- name: ghcr-creds
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: bot
# Replace this with an immutable release tag or image digest.
image: ghcr.io/kr0sh512/tt-simple:v0.1.0
imagePullPolicy: IfNotPresent
envFrom:
- secretRef:
name: tt-simple-env
env:
- name: DB_PATH
value: /data/stats.db
- name: PYTHONUNBUFFERED
value: "1"
- name: PYTHONDONTWRITEBYTECODE
value: "1"
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: tt-simple-data
- name: tmp
emptyDir:
sizeLimit: 32Mi