# ── instagram-scheduler · Docker Compose ──────────────────────────────────── # Compose Spec (no `version:` key — it has been obsolete since Compose v2). # # docker compose up -d start # docker compose logs -f watch # docker compose down stop, keeping data # docker compose down -v stop and DELETE the library. There is no undo. # # Every setting below can be overridden without editing this file. Put values in # `.env` next to it (see .env.example) — Compose reads that automatically and # substitutes ${VAR} here. # # Two things to decide before going to production: # 1. How the app is reachable over HTTPS. Instagram DOWNLOADS your media from it, # so it must be publicly reachable. See compose.caddy.yaml for a one-file # answer, or docs/architecture.md §7 for nginx. # 2. Where the data lives. See the volumes section — bind mount for a folder you # can browse, named volume for one Docker manages. name: instagram-scheduler services: app: # Pull a published image, or build locally. `build` wins when both are given, # so comment it out to use the registry image instead. image: ${IMAGE:-instagram-scheduler:local} build: context: . dockerfile: Dockerfile args: NODE_VERSION: ${NODE_VERSION:-24} container_name: ${CONTAINER_NAME:-instagram-scheduler} # `unless-stopped` rather than `always`: a container you deliberately stopped # should stay stopped across a host reboot. restart: ${RESTART_POLICY:-unless-stopped} # tini is already the image's entrypoint, but this also enables Docker's own # init for anything spawned outside it (ffmpeg subprocesses). init: true # Host:container. Bind to 127.0.0.1 when a reverse proxy on the same host is # terminating TLS — otherwise the app is exposed directly on every interface, # which is rarely what you want given it holds Instagram credentials. ports: - "${BIND_ADDRESS:-127.0.0.1}:${HOST_PORT:-4300}:4300" environment: # ── The setting that matters most ────────────────────────────────────── # Instagram fetches your videos FROM here. Public, HTTPS, no trailing slash. # Wrong or unreachable means every publish fails with an error Instagram # will not explain. PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-} # ── Scheduling timezone ─────────────────────────────────────────────── # An IANA zone. Seeds the first run only; afterwards the panel setting wins. TIMEZONE: ${TIMEZONE:-UTC} # Container clock, for log timestamps. Separate from TIMEZONE on purpose. TZ: ${TZ:-UTC} # ── Reverse proxy ───────────────────────────────────────────────────── # Set to 1 when something in front terminates TLS, so req.ip and the secure # -cookie decision read X-Forwarded-* instead of seeing the proxy. TRUST_PROXY: ${TRUST_PROXY:-1} # ── Paths (inside the container; change the volume, not these) ───────── DB_PATH: /data/scheduler.db LIBRARY_DIR: /data/library RENDERS_DIR: /data/renders # ── Uploads ─────────────────────────────────────────────────────────── # Instagram's own ceiling for video is 300MB, so above that is only useful # if you intend to transcode down before publishing. MAX_UPLOAD_MB: ${MAX_UPLOAD_MB:-500} # ── Worker ──────────────────────────────────────────────────────────── # 0 makes this a panel-only container. If you run several replicas, exactly # ONE may have the worker on, or they race to advance the same jobs and each # spends its own API budget doing it. WORKER_ENABLED: ${WORKER_ENABLED:-1} TICK_SECONDS: ${TICK_SECONDS:-10} MAX_ATTEMPTS: ${MAX_ATTEMPTS:-3} # ── Instagram rate limiting ─────────────────────────────────────────── # The platform limit is 200 calls/hour/account and we stop below it on # purpose. Lower this if another tool shares the same account; raising it # towards 200 defeats the mechanism. See docs/architecture.md §4. IG_CALL_BUDGET_PER_HOUR: ${IG_CALL_BUDGET_PER_HOUR:-150} IG_MAX_OPEN_CONTAINERS: ${IG_MAX_OPEN_CONTAINERS:-3} # ── Sessions ────────────────────────────────────────────────────────── SESSION_TTL_DAYS: ${SESSION_TTL_DAYS:-30} # ── Unattended first-run admin (optional) ───────────────────────────── # Only used when NO user exists yet, so these cannot reset a password later. # Leave both unset to use the onboarding wizard at /setup instead. ADMIN_USERNAME: ${ADMIN_USERNAME:-} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} NODE_ENV: production # Anything not covered above. Compose merges this with `environment`, and # `environment` wins on a conflict. env_file: - path: .env required: false volumes: # ── Your data ───────────────────────────────────────────────────────── # Default is a BIND MOUNT to ./data, so the database, the media library and # the renders are ordinary files you can browse, back up with rsync, and # inspect without Docker. # # The trade-off: the host directory must be writable by uid 1000, which is # the user the container runs as. The entrypoint checks this and tells you # exactly what to run if it is not. # # mkdir -p ./data && sudo chown -R 1000:1000 ./data # # Prefer a Docker-managed volume instead? Swap the line below for: # - scheduler-data:/data # and uncomment the `volumes:` block at the bottom. Ownership is then # Docker's problem, at the cost of the files living under /var/lib/docker. - ${DATA_DIR:-./data}:/data # Container clock, read-only. Harmless if the host has no such file. - /etc/localtime:/etc/localtime:ro healthcheck: # Defined in the image; repeated here so `docker compose ps` reports health # even when the image is overridden. test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4300/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 30s timeout: 5s start_period: 20s retries: 3 # ── Resource limits ───────────────────────────────────────────────────── # ffmpeg will use every core it is given. Capping CPU keeps a long transcode # from starving everything else on a small VPS; the encode simply takes longer. # Memory is generous because a 300MB video plus SQLite's page cache adds up. deploy: resources: limits: cpus: ${CPU_LIMIT:-2.0} memory: ${MEMORY_LIMIT:-2g} reservations: memory: ${MEMORY_RESERVATION:-256m} # Log rotation. Without this, a container that runs for months fills the disk # with JSON logs — and this one logs every publish, poll and hold. logging: driver: json-file options: max-size: ${LOG_MAX_SIZE:-10m} max-file: ${LOG_MAX_FILE:-3} networks: - internal # Uncomment to join an existing reverse-proxy network (see below). # - proxy # ── Hardening ─────────────────────────────────────────────────────────── # The app writes only to /data, so the rest of the filesystem can be read-only. # Commented out by default because it needs tmpfs mounts for anything that # writes to /tmp, and npm/node occasionally do. Turn it on once you have # confirmed your workload is happy: # # read_only: true # tmpfs: # - /tmp:size=256m security_opt: - no-new-privileges:true networks: # An internal bridge, so the container is not on the default network with # everything else on the host. internal: driver: bridge # An EXTERNAL network shared with a reverse proxy stack that already exists. # Create it once with: docker network create proxy # Then uncomment this and the `- proxy` line above. # proxy: # external: true # Uncomment together with the named-volume line in `volumes:` above. # volumes: # scheduler-data: # driver: local