Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/build-server-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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*"]
pull_request:
workflow_dispatch: # adds a "Run workflow" button in the Actions tab

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: 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
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
# 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
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
7 changes: 7 additions & 0 deletions deploy/.env.example
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions deploy/compose.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
__pycache__/
*.egg-info/
*.db
*.db-wal
*.db-shm
build/
35 changes: 35 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
Loading