mirror of
https://github.com/Evil0ctal/Douyin_TikTok_Download_API.git
synced 2026-10-29 12:59:49 +08:00
The CloakBrowser commit lives in one place now: docker/compose.yml's default, which moves with the checkout together with the Playwright pins in docker/browser_rpc/requirements.txt (a verified-with line, held equal by a hygiene test). The installer no longer writes it into .env, and an upgrade comments out the two values earlier versions wrote there. A frozen copy is how an install kept 0.5.10 after the requirements had moved to Playwright 1.64. Installs and upgrades follow releases, not main: - A fresh install clones main and checks out the latest release's tag, and pins DTK_IMAGE_TAG to that version instead of `latest`. DTK_VERSION picks another (5.1.6, 5.1, latest, sha-<commit>); DTK_REPO_URL a mirror. - An upgrade checks out the target's tag and hands over to that tag's installer. Then: compose's own config check, pull, browser build, a backup (--no-backup skips it), migrations, and only then the pin and the restart. Anything that fails before the restart, Ctrl-C included, puts the checkout and .env back. A release that lacks the database's migration is refused before anything is pulled. "Already on" needs the running version, .env and the checkout to agree. dtkctl is rewritten only when it has no upgrade command, so edits to its compose line survive. - The v4 branch's V4.x releases are never taken for the latest. No python3 is needed for detection, the release check or version comparison. Fresh installs on more machines: ceilings come from what Docker can use (the VM on Docker Desktop), memswap_limit follows mem_limit (4.6-7.6 GiB hosts failed to start), no service asks for more CPUs than exist (1-CPU hosts died at the first migration), Redis' maxmemory stays under its ceiling, and an edited compose.host.yml is kept as .old. An existing install is found from container labels, also under --yes and DTK_INSTALL_DIR; volumes of the same project without its .env stop a new install that could not open them. Presets are validated and taken out of the environment, Ctrl-C stops the script, and the model download no longer runs chmod -R on what was typed. The menu: Stop keeps the containers, Back returns, removal stops when `down -v` fails, restore takes a name from the list and says it fills in rather than rolls back, settings list in full and set over stdin, logs show every service, restart applies .env, free disk only offers this install's images, transcription goes back off when it does not come up, and an empty password cancels. An install found without being named - the checkout the script is in, the directory it runs from, the default one, Docker's labels - has to be a checkout of this project whose dtkctl belongs to the user or root and is writable by nobody else; a stray file called dtkctl is never run. Piped through curl | bash the script has no directory of its own, and no longer takes the parent of the current one for it. The two installers are compared line by line for code now, not only for control-flow words: a changed number or flag in one used to pass.
633 lines
25 KiB
YAML
633 lines
25 KiB
YAML
# Production orchestration for a single host.
|
|
#
|
|
# see docker/README.md write .env with random secrets
|
|
# docker compose -p dtk -f docker/compose.yml up -d
|
|
# docker compose -p dtk -f docker/compose.yml logs api # setup token
|
|
#
|
|
# Only `api` publishes a port, bound to loopback. postgres and redis sit on an
|
|
# internal network with no route off the host at all. Everything the containers
|
|
# need beyond that comes from ../.env, which docker/README.md shows how to write
|
|
# and which is never committed (docs/design/08-security.md,
|
|
# docs/design/09-deployment.md).
|
|
#
|
|
# Note on ../.env: compose resolves `env_file` paths relative to this file, so
|
|
# service variables come from the repository root .env even though the project
|
|
# directory is docker/. Interpolation in *this* file (the published address
|
|
# below) does not: it reads docker/.env or the shell. Publish elsewhere with
|
|
# DTK_BIND_HOST=0.0.0.0 docker compose -p dtk -f docker/compose.yml up -d
|
|
# or point compose at the root file with COMPOSE_ENV_FILES=.env.
|
|
name: dtk
|
|
|
|
x-app-image: &app-image
|
|
# Overridable so an operator can run a published image instead of building
|
|
# one. Left as a local name by default: the repository is the source of
|
|
# truth, and `up` on a clean machine should produce something runnable
|
|
# without depending on a registry being reachable or current.
|
|
image: ${DTK_IMAGE:-dtk-app}:${DTK_IMAGE_TAG:-dev}
|
|
build:
|
|
context: ..
|
|
dockerfile: docker/Dockerfile
|
|
args:
|
|
# Bump together with the runtime pin in pyproject.toml.
|
|
PYTHON_VERSION: "3.12.8"
|
|
|
|
x-app-env: &app-env
|
|
env_file:
|
|
# Missing on a fresh clone; the entrypoint then refuses to start with an
|
|
# actionable message instead of compose failing with a file-not-found.
|
|
- path: ../.env
|
|
required: false
|
|
|
|
#: Resource ceilings for the two Python app services.
|
|
#:
|
|
#: These existed nowhere before, on any service. Without them a saturated disk
|
|
#: queue or a runaway job slows every Postgres write, which slows the api, which
|
|
#: fails /readyz - and api is `restart: unless-stopped`, so it gets restarted in
|
|
#: a loop while the actual cause carries on. "Overload cannot destabilise the
|
|
#: main API" was an assumption until this block existed; the kernel is what
|
|
#: makes it true, not the fact that the work happens in another container.
|
|
#:
|
|
#: Sized from measurement on 2026-09-08 rather than guessed: api was using
|
|
#: 104 MiB and worker 71 MiB at rest, so 512M is roughly five times headroom -
|
|
#: enough that a large page or a burst of concurrent tasks has room, low enough
|
|
#: that a leak is stopped rather than swallowed by an 8 GiB host.
|
|
#:
|
|
#: pids_limit matters as much as memory here: a fork bomb, or a subprocess leak,
|
|
#: exhausts the host's process table and takes down containers that are behaving
|
|
#: perfectly. 256 is far above what an asyncio service needs and far below the
|
|
#: default of unlimited.
|
|
x-app-limits: &app-limits
|
|
mem_limit: 512m
|
|
memswap_limit: 512m
|
|
cpus: 2.0
|
|
pids_limit: 256
|
|
|
|
x-app-hardening: &app-hardening
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=64m,mode=1777
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
#: Backups are the one thing these containers write to disk. The image is
|
|
#: read-only by design, so they get a named volume and DTK_BACKUP_DIR pointing
|
|
#: at it - shared, because the worker writes the archive and the API is what
|
|
#: lists and serves it.
|
|
x-app-backups: &app-backups
|
|
volumes:
|
|
- backup-data:/var/lib/dtk/backups
|
|
environment:
|
|
DTK_BACKUP_DIR: /var/lib/dtk/backups
|
|
|
|
x-app: &app
|
|
<<: [*app-image, *app-env, *app-hardening, *app-backups, *app-limits]
|
|
|
|
services:
|
|
# ----------------------------------------------------------------------
|
|
# Data stores. Internal network only: neither of them has any business
|
|
# reaching the internet, and nothing outside the compose project reaches them.
|
|
# ----------------------------------------------------------------------
|
|
postgres:
|
|
# Measured at 112 MiB at rest, but Postgres' working set is shared_buffers
|
|
# plus per-connection memory plus the OS page cache it relies on, so this is
|
|
# the one service where a tight limit degrades correctness rather than
|
|
# protecting it. 2g leaves room for the pool and for TimescaleDB's chunk
|
|
# metadata; it exists to stop a runaway query taking the host, not to be a
|
|
# budget Postgres has to live inside.
|
|
mem_limit: 2g
|
|
pids_limit: 256
|
|
image: timescale/timescaledb-ha:pg17
|
|
env_file:
|
|
- path: ../.env
|
|
required: false
|
|
environment:
|
|
# POSTGRES_PASSWORD comes from ../.env. The master key does not: the
|
|
# database has no use for the key that encrypts its own contents, so it is
|
|
# blanked out here even though the shared env file carries it.
|
|
DTK_SECRET_KEY: ""
|
|
POSTGRES_DB: dtk
|
|
POSTGRES_USER: dtk
|
|
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
|
|
volumes:
|
|
# PGDATA for the timescaledb-ha image. Mounting the usual
|
|
# /var/lib/postgresql/data instead would leave the real data directory
|
|
# inside the container and lose the database on the first rebuild.
|
|
- postgres-data:/home/postgres/pgdata/data
|
|
networks:
|
|
- data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U dtk -d dtk"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 30s
|
|
restart: unless-stopped
|
|
stop_grace_period: 1m
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
redis:
|
|
# 8 MiB at rest. The queue, token buckets and the response cache all live
|
|
# here, and Redis dies rather than swaps when it hits a ceiling, so the
|
|
# limit is generous relative to the measurement.
|
|
mem_limit: 512m
|
|
pids_limit: 128
|
|
image: redis:8-alpine
|
|
# The image's entrypoint only drops privileges when its first argument is
|
|
# literally `redis-server`, and the password has to come from the container's
|
|
# environment (see the command below), so the drop is done here instead.
|
|
# Without this, redis would run as root.
|
|
user: redis
|
|
env_file:
|
|
- path: ../.env
|
|
required: false
|
|
environment:
|
|
DTK_SECRET_KEY: ""
|
|
# $$ escapes compose interpolation, so REDIS_PASSWORD is expanded by the
|
|
# shell inside the container from the env file rather than being written
|
|
# into the resolved compose config.
|
|
#
|
|
# maxmemory is set well under mem_limit and is not optional. The response
|
|
# cache writes one entry per distinct request and keeps it for the TTL in
|
|
# cache.content_ttl (30 min by default); measured entries run 16-200 KB, so
|
|
# a busy instance can put hundreds of megabytes in here inside one TTL
|
|
# window. With no ceiling that ends as an OOM kill of the whole container -
|
|
# the queue included - rather than as a cache miss.
|
|
#
|
|
# volatile-lru, not allkeys-lru: `tasks:queue` is a list with no expiry and
|
|
# so is never an eviction candidate, and neither is sched:lastused. What
|
|
# can be evicted is what carries a TTL - cache entries, rate-limit
|
|
# counters, scheduler token buckets, console sessions - all of which are
|
|
# rebuildable or merely inconvenient to lose. A queued task is not.
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
if [ -z "$$REDIS_PASSWORD" ]; then
|
|
echo "redis: REDIS_PASSWORD is empty; write .env first (see docker/README.md)" >&2
|
|
exit 1
|
|
fi
|
|
exec redis-server \
|
|
--requirepass "$$REDIS_PASSWORD" \
|
|
--appendonly yes \
|
|
--save 60 1 \
|
|
--maxmemory ${DTK_REDIS_MAXMEMORY:-320mb} \
|
|
--maxmemory-policy volatile-lru
|
|
volumes:
|
|
- redis-data:/data
|
|
networks:
|
|
- data
|
|
healthcheck:
|
|
# The queue lives here, so an unauthenticated PONG is not good enough:
|
|
# authenticate the probe the same way the application does.
|
|
test: ["CMD-SHELL", "REDISCLI_AUTH=\"$$REDIS_PASSWORD\" redis-cli ping | grep -q PONG"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
stop_grace_period: 30s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Schema. A one-shot task rather than something api and worker each attempt:
|
|
# concurrent CREATE TABLE races are how a first boot corrupts itself, and no
|
|
# user should have to remember to run migrations after a pull.
|
|
# ----------------------------------------------------------------------
|
|
migrate:
|
|
<<: *app
|
|
command: ["migrate"]
|
|
networks:
|
|
- data
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
restart: "no"
|
|
# A one-shot container has no healthcheck: its exit status is its health,
|
|
# which is what service_completed_successfully below reads.
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Application. Same image, different first argument.
|
|
# ----------------------------------------------------------------------
|
|
api:
|
|
<<: *app
|
|
command: ["api"]
|
|
environment:
|
|
# Listens on every interface *inside* the container; exposure is decided
|
|
# by the published address below, not by this value.
|
|
DTK_BIND_HOST: "0.0.0.0"
|
|
DTK_BIND_PORT: "8000"
|
|
DTK_CONSOLE_DIR: /app/web/dist
|
|
# Repeated rather than inherited: a YAML merge key replaces `environment`
|
|
# wholesale instead of merging it, so a service that declares any variable
|
|
# of its own loses every one from the anchor. The API lists and serves the
|
|
# archives the worker writes, so the two must name the same directory.
|
|
DTK_BACKUP_DIR: /var/lib/dtk/backups
|
|
DTK_MEDIA_DIR: /var/lib/dtk/media
|
|
volumes:
|
|
# Repeated for the same YAML reason as the environment above: naming any
|
|
# volume here replaces the anchor's list wholesale.
|
|
- backup-data:/var/lib/dtk/backups
|
|
# Read-only, and the only component besides the downloader that sees the
|
|
# media at all. The console needs to hand a stored file to a browser, and
|
|
# the API is the only part of this system with authentication, scopes,
|
|
# rate limiting and an audit trail - so it serves the bytes and the
|
|
# downloader stays what its own header insists it must remain: a sink,
|
|
# never a relay. `:ro` because indexing and serving are both reads; a
|
|
# download is deleted through the downloader, which owns the writes.
|
|
- media-data:/var/lib/dtk/media:ro
|
|
ports:
|
|
# Loopback by default. Serving the outside world is an explicit act, and
|
|
# it is on the operator to put TLS in front of it.
|
|
- "${DTK_BIND_HOST:-127.0.0.1}:${DTK_BIND_PORT:-8000}:8000"
|
|
networks:
|
|
- data
|
|
- edge
|
|
# The transcriber's network. Cancelling a transcription from the console
|
|
# tells the sidecar directly, the way a cancelled download tells the
|
|
# downloader; without this the call failed on DNS and was swallowed.
|
|
- compute
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
healthcheck:
|
|
# Readiness, not liveness: `up --wait` should return when the API can
|
|
# actually serve, and a reverse proxy wants the same signal. browser-rpc
|
|
# is an optional dependency and must not appear in it.
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- |
|
|
import sys, urllib.request
|
|
with urllib.request.urlopen("http://127.0.0.1:8000/readyz", timeout=3) as r:
|
|
sys.exit(0 if r.status == 200 else 1)
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 40s
|
|
restart: unless-stopped
|
|
stop_grace_period: 30s
|
|
|
|
worker:
|
|
<<: *app
|
|
command: ["worker"]
|
|
networks:
|
|
- data
|
|
- edge
|
|
- compute
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
healthcheck:
|
|
# The worker has no port to probe. This asserts that the interpreter runs
|
|
# and that the path to Redis - where leases, budgets and the queue live -
|
|
# is open; a worker that cannot reach Redis does no work at all.
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- |
|
|
import os, socket, sys, urllib.parse
|
|
url = urllib.parse.urlparse(os.environ.get("DTK_REDIS_URL", "redis://redis:6379/0"))
|
|
socket.create_connection((url.hostname or "redis", url.port or 6379), 5).close()
|
|
sys.exit(0)
|
|
interval: 20s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
restart: unless-stopped
|
|
# Long enough for an in-flight upstream request to finish and release its
|
|
# identity lease instead of leaving it to expire by TTL.
|
|
stop_grace_period: 45s
|
|
# Scale with: docker compose -p dtk -f docker/compose.yml up -d --scale worker=N
|
|
# Throughput is bounded by the identity pool, not by this number.
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Headless browser RPC. Optional by design: a deployment that imports its
|
|
# cookies by hand does not need the heaviest container in the stack.
|
|
#
|
|
# docker compose -p dtk -f docker/compose.yml --profile browser up -d
|
|
#
|
|
# Set DTK_BROWSER_RPC_URL=http://browser-rpc:9000 in ../.env to let api and
|
|
# worker use it. Left unset they run manual-import only, which is the correct
|
|
# degraded mode rather than an error.
|
|
# ----------------------------------------------------------------------
|
|
browser-rpc:
|
|
# The heavy one, and the reason this block exists. Measured on 2026-09-08
|
|
# while warm: 2.57 GiB resident and 629% CPU - it legitimately uses six
|
|
# cores' worth of Chromium. Left unbounded it is the service that starves
|
|
# the api and postgres it shares a host with, which is the exact failure
|
|
# "isolate the heavy work in its own container" is supposed to prevent and
|
|
# does not, on its own.
|
|
#
|
|
# 4g is roughly 1.5x the warm measurement, so a second context has room and
|
|
# a leak still hits a wall. The CPU cap is deliberately below the six cores
|
|
# observed: signing and minting are off the request path (doc 01), so this
|
|
# service is exactly the one that should yield when the machine is busy.
|
|
#
|
|
# pids_limit is high because Chromium is many processes per context, not
|
|
# because anything here is careless: a renderer, a GPU process and several
|
|
# utility processes per tab, times the warm contexts.
|
|
mem_limit: 4g
|
|
cpus: 4.0
|
|
pids_limit: 1024
|
|
profiles:
|
|
- browser
|
|
image: dtk-browser-rpc:${DTK_IMAGE_TAG:-dev}
|
|
build:
|
|
context: ..
|
|
dockerfile: docker/Dockerfile.browser
|
|
args:
|
|
# Pin the backend to one repository and one commit. Several GitHub orgs
|
|
# publish identically described CloakBrowser repositories, so a floating
|
|
# tag is not a version (docs/design/04-transport-signing.md).
|
|
CLOAKBROWSER_REPO: ${CLOAKBROWSER_REPO:-https://github.com/CloakHQ/cloakbrowser}
|
|
# The commit docker/browser_rpc/requirements.txt was verified with. It
|
|
# is a default here, not a line in .env, so that it moves with the
|
|
# checkout together with those requirements. When .env carried it, an
|
|
# install from September kept CloakBrowser 0.5.10 after the checkout's
|
|
# requirements had moved to the Playwright 1.64 that 0.6.3 was fixed
|
|
# for - a pair nobody had run. Set CLOAKBROWSER_COMMIT only to choose
|
|
# another revision on purpose; tests/unit/test_repo_hygiene.py keeps
|
|
# this, the requirements and the installers' CLOAKBROWSER_PIN equal.
|
|
CLOAKBROWSER_COMMIT: ${CLOAKBROWSER_COMMIT:-fbf005bc199920b592df17ac4d489324e29d56d8}
|
|
# Same env file as the rest of the stack, for the same reason: every
|
|
# DTK_BROWSER_* knob the README documents - the backend name above all - has
|
|
# to be settable in one place. `environment:` wins over `env_file:`, so only
|
|
# what the container's own topology fixes is repeated below.
|
|
env_file:
|
|
- path: ../.env
|
|
required: false
|
|
environment:
|
|
# This container holds no credentials and never talks to the database, so
|
|
# the master key that arrives with the shared env file is blanked here.
|
|
DTK_SECRET_KEY: ""
|
|
# Fixed by the container, not by the operator: the bind address has to
|
|
# match `expose` and the healthcheck, and the profile root has to match
|
|
# the tmpfs mounted below.
|
|
DTK_BROWSER_BIND_HOST: "0.0.0.0"
|
|
DTK_BROWSER_BIND_PORT: "9000"
|
|
DTK_BROWSER_PROFILE_ROOT: /profiles
|
|
#
|
|
# Everything else - DTK_BROWSER_BACKEND, WARM_CONTEXTS,
|
|
# WARM_REFRESH_SECONDS, MAX_CONCURRENT_MINTS, SIGN_PROXY_URL,
|
|
# GEO_PROBE_URL, DEFAULT_COUNTRY - is deliberately absent so ../.env can
|
|
# set it. The service defaults already match what this file used to pin
|
|
# (cloak, 1, 1800, 2, unset), and backend selection stays explicit: there
|
|
# is no fallback between backends, so an unset value means "cloak", never
|
|
# "whatever starts".
|
|
expose:
|
|
- "9000"
|
|
networks:
|
|
# Reaches the internet through the identity's proxy; never touches the
|
|
# database network.
|
|
- edge
|
|
read_only: true
|
|
tmpfs:
|
|
# Browser profiles live in RAM: a mint profile is single-use, and a tmpfs
|
|
# makes that structural rather than a promise about cleanup code.
|
|
#
|
|
# /tmp is the budget that matters, and it is sized from a measurement
|
|
# rather than a guess. The driver passes --disable-dev-shm-usage itself
|
|
# (read off a live launch line on 2026-09-08), so every renderer's shared
|
|
# memory lands here and NOT in /dev/shm below, which was observed at 0%
|
|
# while /tmp sat at 57%. Each resident context holds roughly 290M of
|
|
# deleted-but-open files - invisible to du, visible to df - so 1g fitted
|
|
# barely three, and opening a fourth killed Chromium with SIGSEGV and
|
|
# "Target crashed". That reaches the caller as a signing failure and
|
|
# reads exactly like a platform block.
|
|
#
|
|
# Since signing now keeps one context per identity (see
|
|
# browser_rpc/service.py), the ceiling is roughly
|
|
# 300M x DTK_BROWSER_WARM_CONTEXTS x 2 platforms + a mint in flight.
|
|
# 3g covers the default of 1 with room to raise it to 4. It is RAM: this
|
|
# is a memory budget, not free space.
|
|
- /tmp:size=3g,mode=1777
|
|
- /profiles:size=1g,mode=0700,uid=10001,gid=10001
|
|
# Kept, but do not count on it: the driver disables /dev/shm use, so this is
|
|
# insurance for a future build that stops doing so, not the fix for renderer
|
|
# crashes. The fix is the /tmp size above.
|
|
shm_size: 1gb
|
|
# Chromium's own sandbox needs to create user namespaces. Granting one
|
|
# capability is the lesser evil against running the browser with
|
|
# --no-sandbox, which removes the renderer boundary entirely.
|
|
cap_drop:
|
|
- ALL
|
|
cap_add:
|
|
- SYS_ADMIN
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
# Chromium leaves zombies behind; PID 1 has to reap them.
|
|
init: true
|
|
healthcheck:
|
|
# A 200 is not enough here: the service answers /rpc/health even when the
|
|
# browser backend failed to start, precisely so the operator gets a reason
|
|
# instead of a restart loop. The reason lives in the body, so the probe
|
|
# reads it - otherwise an image built without CLOAKBROWSER_COMMIT reports
|
|
# itself healthy and mints nothing.
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- |
|
|
import json, sys, urllib.request
|
|
with urllib.request.urlopen("http://127.0.0.1:9000/rpc/health", timeout=5) as r:
|
|
body = json.load(r)
|
|
sys.exit(0 if r.status == 200 and body.get("status") == "ok" else 1)
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 60s
|
|
restart: unless-stopped
|
|
stop_grace_period: 30s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Media downloader. Optional by design, like browser-rpc: an instance that
|
|
# only wants metadata never needs to store a byte of video.
|
|
#
|
|
# docker compose -p dtk -f docker/compose.yml --profile downloader up -d
|
|
#
|
|
# Set DTK_DOWNLOADER_URL=http://downloader:9100 in ../.env to let api and
|
|
# worker use it. Left unset, POST /api/v1/downloads answers 501 with an
|
|
# actionable message and everything else is unaffected.
|
|
#
|
|
# This is the only container in the stack that dials arbitrary CDN hosts, so
|
|
# it is also the one with the least in it: a scratch image holding one static
|
|
# Go binary, no shell, no package manager, and exactly one writable path.
|
|
# ----------------------------------------------------------------------
|
|
downloader:
|
|
# Sized from what the work actually is. Transfers are streamed with
|
|
# io.Copy, so resident memory is a handful of 32 KiB buffers per concurrent
|
|
# file plus the runtime - not the size of the files. 512m is generous
|
|
# against that and still a wall a leak hits.
|
|
#
|
|
# The CPU cap is the deliberate one. Pulling a dozen files at once is IO,
|
|
# not compute, and the reason this service exists in its own container is
|
|
# that its overload must not reach the api it shares a host with. Two cores
|
|
# is more than enough to saturate a home connection.
|
|
mem_limit: 512m
|
|
memswap_limit: 512m
|
|
cpus: 2.0
|
|
pids_limit: 128
|
|
profiles:
|
|
- downloader
|
|
image: ${DTK_DOWNLOADER_IMAGE:-dtk-downloader}:${DTK_IMAGE_TAG:-dev}
|
|
build:
|
|
context: ..
|
|
dockerfile: docker/Dockerfile.downloader
|
|
args:
|
|
VERSION: ${DTK_IMAGE_TAG:-dev}
|
|
env_file:
|
|
- path: ../.env
|
|
required: false
|
|
environment:
|
|
# This container holds no credentials and never touches the database.
|
|
# Both are blanked even though the shared env file carries them: the
|
|
# downloader is the component whose compromise would matter most, and it
|
|
# is the one that needs neither.
|
|
DTK_SECRET_KEY: ""
|
|
DTK_DATABASE_URL: ""
|
|
DTK_REDIS_URL: ""
|
|
# Fixed by the container: the bind address has to match `expose`, the
|
|
# healthcheck and the volume mounted below.
|
|
DTK_DOWNLOADER_BIND: "0.0.0.0:9100"
|
|
DTK_DOWNLOADER_ROOT: /var/lib/dtk/media
|
|
#
|
|
# DTK_DOWNLOADER_WORKERS, ITEM_WORKERS, QUEUE, TIMEOUT_SECONDS and TOKEN
|
|
# are deliberately absent so ../.env can set them. The defaults - 4, 4,
|
|
# 64, 900s, no token - are what this file used to pin.
|
|
expose:
|
|
- "9100"
|
|
networks:
|
|
# The CDNs, and nothing else. It has no route to postgres or redis
|
|
# because it has no reason to reach either.
|
|
- edge
|
|
volumes:
|
|
- media-data:/var/lib/dtk/media
|
|
read_only: true
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
healthcheck:
|
|
# The binary probes itself: a scratch image has no curl, and adding one
|
|
# would undo the reason this image is scratch.
|
|
test: ["CMD", "/usr/local/bin/downloader", "healthcheck"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
# Long enough for an in-flight transfer to be cancelled and its .part
|
|
# removed, rather than left behind for the next run to overwrite.
|
|
stop_grace_period: 30s
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
transcriber:
|
|
mem_limit: 4g
|
|
memswap_limit: 4g
|
|
cpus: 2.0
|
|
profiles:
|
|
- transcriber
|
|
image: ${DTK_TRANSCRIBER_IMAGE:-dtk-transcriber}:${DTK_IMAGE_TAG:-dev}
|
|
build:
|
|
context: ..
|
|
dockerfile: docker/Dockerfile.transcriber
|
|
env_file:
|
|
- path: ../.env
|
|
required: false
|
|
environment:
|
|
DTK_SECRET_KEY: ""
|
|
DTK_DATABASE_URL: ""
|
|
DTK_REDIS_URL: ""
|
|
DTK_TRANSCRIBER_MODEL_DIR: /models/funasr
|
|
DTK_TRANSCRIBER_MEDIA_ROOT: /var/lib/dtk/media
|
|
expose:
|
|
- "9200"
|
|
networks:
|
|
- compute
|
|
volumes:
|
|
- media-data:/var/lib/dtk/media:ro
|
|
- ${DTK_TRANSCRIBER_MODEL_PATH:-./models/funasr-onnx}:/models/funasr:ro
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=1g,mode=1777
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
pids_limit: 256
|
|
healthcheck:
|
|
test:
|
|
- CMD
|
|
- python
|
|
- -c
|
|
- |
|
|
import json, os, urllib.request
|
|
token = os.environ.get("DTK_TRANSCRIBER_TOKEN", "")
|
|
request = urllib.request.Request("http://127.0.0.1:9200/health")
|
|
if token:
|
|
request.add_header("X-Transcriber-Token", token)
|
|
with urllib.request.urlopen(request, timeout=5) as response:
|
|
health = json.load(response)
|
|
if health.get("status") != "ok":
|
|
raise SystemExit(1)
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
restart: unless-stopped
|
|
|
|
networks:
|
|
# No gateway: postgres and redis cannot reach anything off this host.
|
|
data:
|
|
internal: true
|
|
edge: {}
|
|
# The transcriber, and the api and worker that call it. No gateway either:
|
|
# the models are mounted, so the sidecar has no reason to reach anything.
|
|
compute:
|
|
internal: true
|
|
|
|
volumes:
|
|
postgres-data:
|
|
redis-data:
|
|
backup-data:
|
|
# The media volume. Named rather than a bind mount so a default install
|
|
# cannot write into the checkout. The downloader mounts it read-write and is
|
|
# the only component that writes; the api mounts it read-only so the console
|
|
# can hand a stored file to a browser behind `media:read`. The worker still
|
|
# only indexes what is stored and never opens a file.
|
|
media-data:
|