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
|
mise install # both halves
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
mise exec -- python manage.py migrate
|
mise exec -- python manage.py migrate
|
||||||
mise exec -- python manage.py runserver 8778 # then open localhost:8778
|
./do start # Django + frontend watcher
|
||||||
cd frontend && mise exec -- npx shadow-cljs watch app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [frontend/README.md](frontend/README.md) for the **load frames** and **save**
|
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.
|
# Channels arrives with the websocket consumers, which are out of step 9's scope.
|
||||||
Django>=5.0,<6.0
|
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.
|
`var/blobs`. Never in the database, and never in a migration.
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Dev default. The deployment that needs a real one will set it, and until there
|
# 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
|
# is a deployment, a checked-in placeholder that says so beats a checked-in secret
|
||||||
# that does not.
|
# that does not.
|
||||||
SECRET_KEY = "dev-only-not-a-secret-arthur"
|
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-not-a-secret-arthur")
|
||||||
DEBUG = True
|
DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes"}
|
||||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"]
|
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 = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
|
|
@ -36,6 +43,7 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
|
@ -77,7 +85,7 @@ TEMPLATES = [
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
"NAME": os.environ.get("DJANGO_DB_PATH", str(BASE_DIR / "db.sqlite3")),
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
|
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
|
||||||
|
|
@ -107,6 +115,10 @@ STATICFILES_DIRS = [
|
||||||
("mediapipe", BASE_DIR / "frontend" / "public" / "mediapipe"),
|
("mediapipe", BASE_DIR / "frontend" / "public" / "mediapipe"),
|
||||||
]
|
]
|
||||||
STATIC_ROOT = BASE_DIR / "var" / "static"
|
STATIC_ROOT = BASE_DIR / "var" / "static"
|
||||||
|
STORAGES = {
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage"},
|
||||||
|
}
|
||||||
|
|
||||||
# --- blobs ------------------------------------------------------------------
|
# --- 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
|
# `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
|
# refuse a path that is not a hash, and `collectstatic` has no business walking
|
||||||
# gigabytes of frames.
|
# 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
|
# 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.
|
# 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