From 10eede7941c16032253977936c819e51a8d83374 Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 21 Sep 2026 10:50:01 -0400 Subject: [PATCH 1/5] Add Dockerfile and .dockerignore for the PTH server --- server/.dockerignore | 7 +++++++ server/Dockerfile | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..aa47cf3 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,7 @@ +.venv/ +__pycache__/ +*.egg-info/ +*.db +*.db-wal +*.db-shm +build/ diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0456036 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,35 @@ +# PTH server (Flask + SQLite). Build context: this directory. +FROM python:3.12-slim + +# uv installs dependencies exactly as pinned in uv.lock +COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /bin/uv + +ENV PYTHONUNBUFFERED=1 \ + TZ=UTC \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + PATH=/app/.venv/bin:$PATH \ + PTH_DB_PATH=/data/pth_data.db + +WORKDIR /app + +# dependencies first: this layer is cached and only rebuilt when the lockfile changes +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev --no-install-project + +# then the application code +COPY app.py ./ +COPY pth_server ./pth_server +RUN uv sync --frozen --no-dev + +# run as a non-root user; /data is where the SQLite database lives (mounted from the host) +RUN useradd --system --uid 1001 pth && mkdir -p /data && chown pth:pth /data +USER pth +VOLUME ["/data"] +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:5000/pth/api/ndays?days=1', timeout=4).status==200 else 1)" + +# 2 workers: enough for one sensor plus dashboard users; access log to stdout for `docker logs` +CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "--access-logfile", "-", "app:app"] From 4b1b13f09ab27db579ebdb119affdc5d30866f64 Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 21 Sep 2026 11:13:33 -0400 Subject: [PATCH 2/5] Add CI and deploy compose for the PTH server --- .github/workflows/build-server-image.yml | 65 ++++++++++++++++++++++++ deploy/.env.example | 7 +++ deploy/compose.yaml | 30 +++++++++++ 3 files changed, 102 insertions(+) create mode 100644 .github/workflows/build-server-image.yml create mode 100644 deploy/.env.example create mode 100644 deploy/compose.yaml diff --git a/.github/workflows/build-server-image.yml b/.github/workflows/build-server-image.yml new file mode 100644 index 0000000..7e9e6ae --- /dev/null +++ b/.github/workflows/build-server-image.yml @@ -0,0 +1,65 @@ +name: Build PTH server image + +# Pull requests: build only (proves the image still builds). +# main and version tags: build and publish to GitHub Container Registry. +on: + push: + branches: [main] + tags: ["v*"] + paths: ["server/**", ".github/workflows/build-server-image.yml"] + pull_request: + paths: ["server/**", ".github/workflows/build-server-image.yml"] + +env: + IMAGE: ghcr.io/${{ github.repository_owner }}/pth-server + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # allows pushing to ghcr.io + steps: + - uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} # provided by Actions, no secret to manage + + - name: Work out image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + type=sha,format=short + + - name: Build (and push on main/tags) + uses: docker/build-push-action@v6 + with: + context: server + platforms: linux/amd64 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test the image + run: | + docker build -t pth-test server + docker run -d --name pth -p 5000:5000 -v /tmp/pth-data:/data pth-test + for i in $(seq 30); do + curl -fsS http://127.0.0.1:5000/pth/api/ndays?days=1 && break || sleep 1 + done + curl -fsS -X POST -H 'Content-Type: application/json' \ + -d '{"time":1700000000,"device_id":"ci","Test Channel":1.5}' \ + http://127.0.0.1:5000/pth/api/store_data + curl -fsS "http://127.0.0.1:5000/pth/api/closest?time=1700000000&device_id=ci" + docker rm -f pth diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..d18d439 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,7 @@ +# Copy to .env next to compose.yaml on the server and fill in. + +# Folder on the attached volume that holds pth_data.db +PTH_DATA_DIR=/mnt/volume_nyc1_XXXXXXXXXX/pth + +# Image tag to run. "latest" follows main; pin a release tag (e.g. v1.1) for production. +PTH_VERSION=latest diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..7c79cda --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,30 @@ +# PTH server on the lab server. +# +# cp .env.example .env # then set PTH_DATA_DIR (and PTH_VERSION to pin a release) +# docker compose pull && docker compose up -d +# +# No ports are published: the edge proxy (Caddy) reaches this container over +# the shared "lab-net" network at http://pth:5000. +name: pth + +services: + pth: + image: ghcr.io/ddomlab/pth-server:${PTH_VERSION:-latest} + container_name: pth + restart: unless-stopped + environment: + TZ: UTC + PTH_DB_PATH: /data/pth_data.db + volumes: + # SQLite database, on the attached volume so it outlives the container + - ${PTH_DATA_DIR:?set PTH_DATA_DIR in .env}:/data + networks: + - lab-net + logging: + driver: json-file + options: { max-size: "10m", max-file: "3" } + +networks: + lab-net: + # created once on the server with: docker network create lab-net + external: true From a977a87ee787341e68c0d9c6de2aefd0e9a7253d Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 21 Sep 2026 11:25:23 -0400 Subject: [PATCH 3/5] CI: simplify triggers, allow manual runs --- .github/workflows/build-server-image.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-server-image.yml b/.github/workflows/build-server-image.yml index 7e9e6ae..67c6be7 100644 --- a/.github/workflows/build-server-image.yml +++ b/.github/workflows/build-server-image.yml @@ -6,9 +6,8 @@ on: push: branches: [main] tags: ["v*"] - paths: ["server/**", ".github/workflows/build-server-image.yml"] pull_request: - paths: ["server/**", ".github/workflows/build-server-image.yml"] + workflow_dispatch: # adds a "Run workflow" button in the Actions tab env: IMAGE: ghcr.io/${{ github.repository_owner }}/pth-server From a702a53d3c0badf433d539d15b44508fb619b821 Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 21 Sep 2026 11:37:36 -0400 Subject: [PATCH 4/5] CI: set up Buildx so layer caching works --- .github/workflows/build-server-image.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-server-image.yml b/.github/workflows/build-server-image.yml index 67c6be7..2818a8f 100644 --- a/.github/workflows/build-server-image.yml +++ b/.github/workflows/build-server-image.yml @@ -21,6 +21,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 # builder that supports layer caching + - name: Log in to GitHub Container Registry if: github.event_name == 'push' uses: docker/login-action@v3 From c499bf379848d3fd74d274e12845280577145f10 Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 21 Sep 2026 13:03:47 -0400 Subject: [PATCH 5/5] CI: fix smoke test (no volume mount; show container logs) --- .github/workflows/build-server-image.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-server-image.yml b/.github/workflows/build-server-image.yml index 2818a8f..6c3587e 100644 --- a/.github/workflows/build-server-image.yml +++ b/.github/workflows/build-server-image.yml @@ -56,7 +56,10 @@ jobs: - name: Smoke test the image run: | docker build -t pth-test server - docker run -d --name pth -p 5000:5000 -v /tmp/pth-data:/data pth-test + # no volume mount: the image's own /data is owned by the app user. + # (On a real server, chown the mounted folder to 1001:1001 first.) + docker run -d --name pth -p 5000:5000 pth-test + sleep 3 && docker logs pth for i in $(seq 30); do curl -fsS http://127.0.0.1:5000/pth/api/ndays?days=1 && break || sleep 1 done