Download
depot / docs / automation

Running it unattended

Everything on this site so far assumed a human at the keyboard. This page removes the human: cron jobs, CI pipelines, containers — DepotDownloader as a build step rather than a tool you run. The good news is it was built for this; the work is in handling auth, failure and politeness correctly before you walk away. Most of what follows is the way I actually run my own scheduled jobs, including the mistakes that taught me each rule.

01The three rules of headless

Rule one: nothing may prompt. A password prompt, a Steam Guard code, a QR render — any of them hangs a pipeline forever. Every interactive step happens once, on your machine, before automation starts. Rule two: prefer anonymous. If the job is server files, free apps or licensed-to-anonymous content, there's no credential to manage, leak or rotate — most automation around this tool needs no account at all. Rule three: treat non-zero exit as failure and own the retry. The tool reports; your script decides.

02Auth without a human: seeding the session

When the job genuinely needs an account, do not put -password in a pipeline — it lands in logs, process lists and shell history (and 2FA would still prompt). The supported pattern is the refresh token: log in once, interactively, on the machine or volume that will run the job:

$ ./DepotDownloader -app 440 -username you -remember-password -manifest-only -dir /tmp/seed

(A -manifest-only run makes a cheap seeding vehicle.) That stores a token in account.config; every later run with -username you -remember-password goes straight through, no prompt. The operational consequences: the file must persist between runs — a real machine or a persistent volume, not a throwaway container layer; it's a bearer credential, so guard it like an SSH key and remember the kill switch is server-side ("deauthorize all devices", details); and tokens die eventually — a healthy pipeline alerts on auth failure instead of silently retrying into a rate limit.

If even the one interactive login is awkward (no console on the box, or you want zero human touch), you can drive Steam Guard fully unattended. -no-mobile forces the typed-code path instead of a phone push, and the code can be piped in from an external authenticator — the community pattern is steamguard-cli generating the current TOTP code and feeding it via the stdin/2FA flag. It works, but it means the account's 2FA secret now lives on the build box — a real security trade you should make deliberately, not by default. For most pipelines the persistent refresh token above is both simpler and safer; reach for TOTP piping only when you truly can't do the one-time interactive seed.

03The Docker recipe

There's no official image — it's been asked — but the community maintains a minimal one, and the DIY version is four lines anyway. Community image:

$ docker run --rm -v $(pwd)/data:/data ghcr.io/sonroyaalmerol/steam-depot-downloader \ DepotDownloader -app 258550 -os linux -dir /data/rust

DIY, pinned to the release you've tested:

# Dockerfile — self-contained build needs no .NET image FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates unzip curl && rm -rf /var/lib/apt/lists/* RUN curl -fsSL -o /tmp/dd.zip https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_3.4.0/DepotDownloader-linux-x64.zip \ && unzip /tmp/dd.zip -d /opt/depotdownloader && chmod +x /opt/depotdownloader/DepotDownloader ENTRYPOINT ["/opt/depotdownloader/DepotDownloader"]

The self-contained zip is the whole reason this is easy — no runtime image, no dependency dance (people who based images on bare dotnet runtimes hit exactly that, issue #457). For authenticated jobs, mount a volume that persists the session store; for anonymous jobs, --rm away. If the container's home is a NAS rather than a CI box, the same image and volume rules apply with appliance-specific wrinkles — running DepotDownloader on a NAS & in Docker covers the PUID/PGID permissions and persistent-login mounts that matter on Synology, unRAID and friends.

04A GitHub Actions example

The anonymous case — fetch server files, do something with them — fits in one job:

jobs: fetch-server: runs-on: ubuntu-latest steps: - name: Get DepotDownloader run: | curl -fsSL -o dd.zip "https://github.com/SteamRE/DepotDownloader/releases/download/DepotDownloader_3.4.0/DepotDownloader-linux-x64.zip" unzip dd.zip -d dd && chmod +x dd/DepotDownloader - name: Pull TF2 server run: ./dd/DepotDownloader -app 232250 -os linux -dir ./server - name: Do your thing run: ls -la ./server

Pin the release version — your pipeline shouldn't change behaviour because upstream shipped. (If the automation is keeping a game server current, note that SteamCMD is the better day-to-day tool for that loop — DepotDownloader earns its place when you need a build SteamCMD can't pin to an old version.) For authenticated Actions jobs, be honest with yourself about the trade: ephemeral runners have no persistent session store, so you'd be wiring the account.config through cache or secrets — doable, but a self-hosted runner or a scheduled job on a real box (the cron pattern) is the design that doesn't fight the platform.

05Handling failure

The contract is the standard one — 0 on success, non-zero on failure — and that's also the entire documented surface: there's no published table mapping specific failures to specific codes, so scripts should branch on "failed or not" and read the output for the why. Three failure families deserve different handling, which is worth encoding:

# retry transient, fail fast on permanent for i in 1 2 3; do ./DepotDownloader -app 232250 -os linux -dir /srv/tf2 > dd.log 2>&1 && exit 0 grep -qE "No manifest request code|not available from this account" dd.log && break sleep $((i * 60)) done cat dd.log; exit 1

The logic: network and CDN hiccups deserve a backoff retry (downloads resume chunk-wise, so retries are cheap); request-code refusals and license errors are policy — retrying them is noise at best and a rate-limit risk at worst, so fail fast and page a human. And always capture the log; a cron job whose only artifact is "exit 1" schedules its own 2 a.m. debugging session. I learned that one the hard way — the first unattended job I set up logged nothing, and when it failed I had no idea why until I rebuilt it from scratch.

06Concurrency, and being a good CDN citizen

Within one run, -max-downloads (default 8) sets concurrent chunk fetches — raise it on fat pipes, lower it when the box shares its uplink. Across runs, two rules from elsewhere on the site become load-bearing in automation: parallel instances each need a unique -loginid or they'll kick each other's sessions in a loop, and schedules should match reality — games update daily at most, so nightly checks with a randomized minute (don't be one of a thousand 0 4 * * * jobs) cover everything a tighter loop would, without hammering infrastructure Valve provides for free. If you're refreshing many machines on one LAN, -use-lancache means the CDN sees one download instead of twenty. Delta updates already do most of the politeness for you — a no-change check is seconds and kilobytes — so the main sin to avoid is re-downloading from scratch into empty directories on every run: point -dir at the same path and let the delta machinery work.

One rate limit worth knowing for archival work: if you're pulling many manifests in a session — sweeping a depot's whole version history, say — Steam rate-limits manifest requests per login, and people hit it after roughly 110–120 requests, then get locked out for about an hour. There's no flag to raise it; the practical answer is to batch sanely (download what you need, don't hammer the full history in one burst) and treat a sudden run of refusals as the budget exhausting, not a permanent failure. My rule of thumb when I'm archiving history: pull a batch, wait out the hour if I hit the wall, and never try to brute-force the whole thing in one sitting.

07Recipes: a service, and a few patterns

Run it as a scheduled service (systemd). Cron works, but a systemd timer is the sturdier path for a server — it logs to the journal, restarts cleanly, and with Persistent=true catches up a run missed while the box was off. (If that always-on box is a Synology or unRAID appliance, running it on a NAS on a schedule uses the appliance's own task scheduler instead, but the seed-the-login-once logic is identical.) A minimal pair: a .service that runs your DepotDownloader wrapper script (Type=oneshot, the persistent token from §02 on a real volume), and a .timer with OnCalendar=*-*-* 04:17:00 and Persistent=true. The off-the-round-number minute is the schedule-politeness point from above, in service form.

Archive several versions in a loop. Feed a list of manifest IDs and pull each into its own folder — the gotcha that bites people is reusing one -dir, where a new manifest's files land on top of the previous build and quietly Frankenstein it. Distinct directory per version, always:

for m in 7848722008564294070 3660787314279169352; do ./DepotDownloader -app 489830 -depot 489831 -manifest $m \ -username you -remember-password -dir "./archive/$m" done

A reusable wrapper. Many people keep a one-line .bat/.sh that forwards arguments to the framework build (dotnet DepotDownloader.dll %* on Windows, the equivalent on Unix) so the long invocation is one short command — handy, but a reminder that the self-contained release needs no dotnet prefix at all. And for grabbing every DLC of a game, enumerate its DLC depots on SteamDB and pass them as a depot list in one command rather than looping — DepotDownloader takes multiple -depot values at once.