deploy: P4 stack kit — docker-compose (app + Tailscale sidecar, userspace), systemd unit (Delegate=yes), Dockerfile on 3.14-slim with --state-dir, serve.json, gitignore stack-local .env/state
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,10 @@ data/
|
|||||||
*.sqlite3-wal
|
*.sqlite3-wal
|
||||||
*.sqlite3-shm
|
*.sqlite3-shm
|
||||||
|
|
||||||
|
# deploy state (stack-local, never committed)
|
||||||
|
.env
|
||||||
|
tailscale-state/
|
||||||
|
|
||||||
# python
|
# python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# Bike-Tinder app container (Phase 2 build).
|
# Bike-Tinder app container (Phase 4 deploy).
|
||||||
# The compose stack, systemd unit and Tailscale sidecar belong to the parent —
|
# The compose stack, systemd unit and Tailscale sidecar live beside this file —
|
||||||
# this file is only the app image contract.
|
# this file is only the app image contract.
|
||||||
FROM python:3.13-slim
|
FROM python:3.14-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
|
||||||
# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/
|
# Data lands on the stack volume: /data/bike-tinder.sqlite3 and /data/img/.
|
||||||
|
# /state is the hunt state dir (bind-mounted read-only) used by POST /api/import.
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", "--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img"]
|
CMD ["python", "-m", "app.serve", "--host", "0.0.0.0", "--port", "8000", \
|
||||||
|
"--db", "/data/bike-tinder.sqlite3", "--img-dir", "/data/img", \
|
||||||
|
"--state-dir", "/state"]
|
||||||
|
|||||||
64
deploy/docker-compose.yml
Normal file
64
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Bike-Tinder stack — deck app + Tailscale sidecar.
|
||||||
|
#
|
||||||
|
# Deployed on Rotarran at /var/mnt/DATA/podman-stacks/bike-tinder/ (a checkout
|
||||||
|
# of the `release` branch). Run via the user unit podman-stack-bike-tinder.service
|
||||||
|
# (Delegate=yes) or manually:
|
||||||
|
# podman-compose -f deploy/docker-compose.yml --env-file .env up -d
|
||||||
|
#
|
||||||
|
# Networking:
|
||||||
|
# - app: published on 127.0.0.1:9453 only — Hermes cron triggers (import /
|
||||||
|
# agent read) and local debugging. The LAN sees nothing.
|
||||||
|
# - tailscale (userspace mode, no tun/caps): joins the tailnet as `biketinder`
|
||||||
|
# and serves https://biketinder.tailf35a34.ts.net -> http://app:8000 (443).
|
||||||
|
# Secrets: TS_AUTHKEY + OPENROUTER_API_KEY come from the stack-local .env
|
||||||
|
# (never committed). Before the authkey is placed, the sidecar simply reports
|
||||||
|
# "no authkey" and exits — the app runs fine on its own.
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "5"
|
||||||
|
compress: "true"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile
|
||||||
|
image: localhost/bike-tinder:latest
|
||||||
|
container_name: bike-tinder-app
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=America/Halifax
|
||||||
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
|
||||||
|
volumes:
|
||||||
|
- /var/mnt/DATA/podman-stacks/bike-tinder/data:/data
|
||||||
|
- /var/home/admin/.hermes/scripts/motorcycle-hunt.d:/state:ro,z
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:9453:8000"
|
||||||
|
stop_grace_period: 15s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
logging:
|
||||||
|
<<: *default-logging
|
||||||
|
|
||||||
|
tailscale:
|
||||||
|
image: docker.io/tailscale/tailscale:latest
|
||||||
|
container_name: bike-tinder-ts
|
||||||
|
hostname: biketinder
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
environment:
|
||||||
|
- TS_AUTHKEY=${TS_AUTHKEY:-}
|
||||||
|
- TS_HOSTNAME=biketinder
|
||||||
|
- TS_STATE_DIR=/var/lib/tailscale
|
||||||
|
- TS_SERVE_CONFIG=/config/serve.json
|
||||||
|
- TS_USERSPACE=true
|
||||||
|
- TZ=America/Halifax
|
||||||
|
volumes:
|
||||||
|
- /var/mnt/DATA/podman-stacks/bike-tinder/tailscale-state:/var/lib/tailscale
|
||||||
|
- /var/mnt/DATA/podman-stacks/bike-tinder/deploy/tailscale:/config:ro
|
||||||
|
logging:
|
||||||
|
<<: *default-logging
|
||||||
18
deploy/podman-stack-bike-tinder.service
Normal file
18
deploy/podman-stack-bike-tinder.service
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# /var/home/admin/.config/systemd/user/podman-stack-bike-tinder.service
|
||||||
|
# (installed from the repo: deploy/podman-stack-bike-tinder.service)
|
||||||
|
[Unit]
|
||||||
|
Description=Bike-Tinder podman-compose stack (deck app + Tailscale sidecar)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
Delegate=yes
|
||||||
|
WorkingDirectory=/var/mnt/DATA/podman-stacks/bike-tinder
|
||||||
|
ExecStart=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env up -d
|
||||||
|
ExecStop=/var/home/admin/.local/bin/podman-compose -f deploy/docker-compose.yml --env-file .env down
|
||||||
|
TimeoutStartSec=600
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
10
deploy/tailscale/serve.json
Normal file
10
deploy/tailscale/serve.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"TCP": { "443": { "HTTPS": true } },
|
||||||
|
"Web": {
|
||||||
|
"${TS_CERT_DOMAIN}:443": {
|
||||||
|
"Handlers": {
|
||||||
|
"/": { "Proxy": "http://app:8000" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user