Add Fly release image and local start command
This commit is contained in:
parent
7249e73e7c
commit
690de21fa4
7 changed files with 176 additions and 7 deletions
16
.dockerignore
Normal file
16
.dockerignore
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.git
|
||||
.venv
|
||||
**/node_modules
|
||||
frontend/.shadow-cljs
|
||||
frontend/out
|
||||
frontend/.cpcache
|
||||
frontend/test/browser/out
|
||||
static/arthur/js
|
||||
db.sqlite3
|
||||
var
|
||||
frames
|
||||
scratch
|
||||
audio.wav
|
||||
manifest.json
|
||||
*.take
|
||||
*.tflite
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM node:20-bookworm AS frontend
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl tar \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -fsSL 'https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jdk/hotspot/normal/eclipse' -o /tmp/jdk.tar.gz \
|
||||
&& mkdir -p /opt/java \
|
||||
&& tar -xzf /tmp/jdk.tar.gz -C /opt/java --strip-components=1 \
|
||||
&& rm /tmp/jdk.tar.gz
|
||||
|
||||
ENV JAVA_HOME=/opt/java
|
||||
ENV PATH="/opt/java/bin:${PATH}"
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY frontend/ ./
|
||||
RUN npx shadow-cljs release app
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DJANGO_DEBUG=0 \
|
||||
DJANGO_DB_PATH=/data/db.sqlite3 \
|
||||
DJANGO_BLOB_ROOT=/data/blobs
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --create-home --shell /usr/sbin/nologin app
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . ./
|
||||
COPY --from=frontend /app/static/arthur/js/ ./static/arthur/js/
|
||||
RUN python manage.py collectstatic --noinput \
|
||||
&& mkdir -p /data \
|
||||
&& chown -R app:app /app /data
|
||||
|
||||
USER app
|
||||
EXPOSE 8000
|
||||
CMD ["sh", "-c", "python manage.py migrate --noinput && exec gunicorn server.wsgi:application --bind 0.0.0.0:8000 --workers 1 --threads 4 --timeout 120"]
|
||||
|
|
@ -26,8 +26,7 @@ gaps; its controls are still pending.
|
|||
mise install # both halves
|
||||
pip install -r requirements.txt
|
||||
mise exec -- python manage.py migrate
|
||||
mise exec -- python manage.py runserver 8778 # then open localhost:8778
|
||||
cd frontend && mise exec -- npx shadow-cljs watch app
|
||||
./do start # Django + frontend watcher
|
||||
```
|
||||
|
||||
See [frontend/README.md](frontend/README.md) for the **load frames** and **save**
|
||||
|
|
|
|||
59
do
Executable file
59
do
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run the local app and its frontend watcher with one command."""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def start():
|
||||
with socket.socket() as probe:
|
||||
if probe.connect_ex(("127.0.0.1", 8778)) == 0:
|
||||
raise SystemExit("port 8778 is already in use")
|
||||
|
||||
commands = [
|
||||
(["mise", "exec", "--", "python", "manage.py", "runserver", "8778"], ROOT),
|
||||
(["mise", "exec", "--", "npx", "shadow-cljs", "watch", "app"], ROOT / "frontend"),
|
||||
]
|
||||
children = []
|
||||
stopping = False
|
||||
|
||||
def stop(_signal, _frame):
|
||||
nonlocal stopping
|
||||
stopping = True
|
||||
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
try:
|
||||
for command, directory in commands:
|
||||
children.append(subprocess.Popen(command, cwd=directory, start_new_session=True))
|
||||
print("arthur: http://localhost:8778 (Ctrl-C stops both processes)", flush=True)
|
||||
while not stopping:
|
||||
for child in children:
|
||||
if child.poll() is not None:
|
||||
raise SystemExit(f"app process exited with status {child.returncode}")
|
||||
time.sleep(0.25)
|
||||
finally:
|
||||
for child in children:
|
||||
if child.poll() is None:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
for child in children:
|
||||
try:
|
||||
child.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
child.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1:] == ["start"]:
|
||||
start()
|
||||
else:
|
||||
raise SystemExit("usage: ./do start")
|
||||
38
fly.toml
Normal file
38
fly.toml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
app = "arthur"
|
||||
primary_region = "iad"
|
||||
|
||||
[build]
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[env]
|
||||
DJANGO_DEBUG = "0"
|
||||
DJANGO_ALLOWED_HOSTS = ".fly.dev"
|
||||
DJANGO_CSRF_TRUSTED = "https://arthur.fly.dev"
|
||||
DJANGO_DB_PATH = "/data/db.sqlite3"
|
||||
DJANGO_BLOB_ROOT = "/data/blobs"
|
||||
|
||||
[[mounts]]
|
||||
source = "data"
|
||||
destination = "/data"
|
||||
initial_size = "1gb"
|
||||
|
||||
[http_service]
|
||||
internal_port = 8000
|
||||
force_https = true
|
||||
auto_stop_machines = "stop"
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ["app"]
|
||||
|
||||
[[http_service.checks]]
|
||||
interval = "30s"
|
||||
timeout = "5s"
|
||||
grace_period = "60s"
|
||||
method = "GET"
|
||||
path = "/"
|
||||
headers = { Host = "arthur.fly.dev" }
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 1
|
||||
memory = "1gb"
|
||||
|
|
@ -8,3 +8,5 @@
|
|||
#
|
||||
# Channels arrives with the websocket consumers, which are out of step 9's scope.
|
||||
Django>=5.0,<6.0
|
||||
gunicorn>=23.0,<24.0
|
||||
whitenoise>=6.9,<7.0
|
||||
|
|
|
|||
|
|
@ -14,15 +14,22 @@ of what there is to know about this file:
|
|||
`var/blobs`. Never in the database, and never in a migration.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Dev default. The deployment that needs a real one will set it, and until there
|
||||
# is a deployment, a checked-in placeholder that says so beats a checked-in secret
|
||||
# that does not.
|
||||
SECRET_KEY = "dev-only-not-a-secret-arthur"
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"]
|
||||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-not-a-secret-arthur")
|
||||
DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes"}
|
||||
ALLOWED_HOSTS = os.environ.get(
|
||||
"DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]"
|
||||
).split(",")
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
origin for origin in os.environ.get("DJANGO_CSRF_TRUSTED", "").split(",")
|
||||
if origin
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
|
|
@ -36,6 +43,7 @@ INSTALLED_APPS = [
|
|||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
|
|
@ -77,7 +85,7 @@ TEMPLATES = [
|
|||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"NAME": os.environ.get("DJANGO_DB_PATH", str(BASE_DIR / "db.sqlite3")),
|
||||
"OPTIONS": {
|
||||
"timeout": 20,
|
||||
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
|
||||
|
|
@ -107,6 +115,10 @@ STATICFILES_DIRS = [
|
|||
("mediapipe", BASE_DIR / "frontend" / "public" / "mediapipe"),
|
||||
]
|
||||
STATIC_ROOT = BASE_DIR / "var" / "static"
|
||||
STORAGES = {
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage"},
|
||||
}
|
||||
|
||||
# --- blobs ------------------------------------------------------------------
|
||||
#
|
||||
|
|
@ -115,7 +127,7 @@ STATIC_ROOT = BASE_DIR / "var" / "static"
|
|||
# `static/`: a blob is served by a view that can set immutable cache headers and
|
||||
# refuse a path that is not a hash, and `collectstatic` has no business walking
|
||||
# gigabytes of frames.
|
||||
BLOB_ROOT = BASE_DIR / "var" / "blobs"
|
||||
BLOB_ROOT = Path(os.environ.get("DJANGO_BLOB_ROOT", str(BASE_DIR / "var" / "blobs")))
|
||||
|
||||
# The port the browser suite expects by default, and not 8777, which is still the
|
||||
# old JS tool's under `serve.py`. Both are meant to run side by side.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue