mirror of
https://github.com/Evil0ctal/Douyin_TikTok_Download_API.git
synced 2026-10-29 04:46:34 +08:00
**Publishing.** `docker-publish.yml` builds and pushes on a branch push, a `v*` tag, or by hand, for linux/amd64 and linux/arm64. Two images, and deliberately not three: the app (api, worker and migrate are one image differing only in the verb) and the Go downloader sidecar. browser-rpc is not published — it installs CloakBrowser from a pinned commit passed in as a build argument, so an image built here would ship somebody else's browser under this project's name and make the pin ours rather than the operator's. It stays a local build. `latest` moves only on a semver tag. A branch push gets a branch tag and an immutable sha tag, so pulling `latest` gets a release somebody decided to make rather than whatever was merged an hour ago. A fork without credentials builds without pushing instead of failing, and a manual run defaults to build-only so the workflow can be exercised without publishing. The two slow parts no longer run under emulation. The console stage emits JavaScript and CSS, which are not architecture-specific, so it is pinned to BUILDPLATFORM and built once; the downloader cross-compiles with GOARCH and runs its tests natively, because a cross-compiled test binary cannot be executed at the point it is built. Verified: a two-platform downloader build produces a manifest carrying both linux/amd64 and linux/arm64. The smoke test is what the image is actually for. `--version` would have tested the usage message and nothing else - the entrypoint takes a ROLE, not a flag - so it runs `dtk --version` through a shell, checks the console assets reached the runtime stage (a separate build stage that can silently produce nothing), and checks the entrypoint still refuses an unknown role. **README.** Adds what v4's has and v5's did not: a capability matrix per platform (checked against the endpoint tables, including that Douyin's follower and following lists are absent because Douyin serves them only to a signed-in session), the link formats accepted, the stack, contact details, star history, and CI and Docker Hub badges. The Chinese README's sponsor block was English prose. It is Chinese now; the English one stays English. Both files now carry the same twelve sections. `DTK_IMAGE` and `DTK_DOWNLOADER_IMAGE` make the image name overridable, because the README was about to document a variable compose did not read. The default stays a local name: `up` on a clean machine should produce something runnable without depending on a registry. The `pull` step is spelled out, since these services declare both `image` and `build` and compose builds rather than pulls when the image is absent. CI now also watches `main`, since v5 is what `main` will hold and a workflow that only watches the old branch name goes quiet exactly when the rename matters.
561 lines
23 KiB
YAML
561 lines
23 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
|
|
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
|
|
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}
|
|
CLOAKBROWSER_COMMIT: ${CLOAKBROWSER_COMMIT:-}
|
|
# 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"
|
|
|
|
networks:
|
|
# No gateway: postgres and redis cannot reach anything off this host.
|
|
data:
|
|
internal: true
|
|
edge: {}
|
|
|
|
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:
|