Self-hosted
The parser also ships as a Docker image. It runs the same models as the cloud API and answers the same path with the same JSON, so moving between them is a change of base URL and authentication.
Run it when documents must not leave your network, when you want a fixed cost rather than a per-parse one, or when you need the parser next to something that has no internet access. It verifies its own license offline: no telemetry, no call home.
Start it
docker run -d --name affinda-parser \
-e AFFINDA_LICENSE_TOKEN='<your-license-token>' \
-p 8080:8080 \
affinda/resume-parser
The image is public, so there is no registry login. affinda/resume-parser is the current CPU
release; affinda/resume-parser:latest-gpu is the GPU build.
Then parse, with no Authorization header:
curl -X POST http://localhost:8080/v1/parse \
-F "file=@resume.pdf"
For durable state, mount a volume. The container counts its own parses in
/var/lib/affinda, and a container started with --rm and no volume loses that count when it is
removed:
docker run -d --name affinda-parser \
-v affinda-state:/var/lib/affinda \
-e AFFINDA_LICENSE_TOKEN='<your-license-token>' \
-p 8080:8080 \
affinda/resume-parser
Getting a license
Without a license the container runs in evaluation mode: 1,000 successful parses, then
402 entitlement_required. That is the same allowance a new cloud account gets, so you can
compare the two on the same documents.
A free trial license raises the total to 11,000 parses and lasts a year. Get one at https://resume-parser.affinda.com/self-hosted/get-your-key, which sends you to the console for your region. You need a verified email address, and one license is issued per organization: ask twice and you get the same license back.
Two ways to apply it:
AFFINDA_LICENSE_TOKEN=<token>at start, as above. Better, setAFFINDA_LICENSE_TOKEN_FILE=/run/secrets/affinda-licenseand mount the token as a file. When both are set the file wins. Neither is printed in the logs.- Without a restart, on a container that is already running:
docker exec affinda-parser affinda-license apply '<your-license-token>'
A token that is invalid, expired or unreadable stops the container at startup rather than quietly falling back. Leaving the variable unset is what keeps it in evaluation mode.
Each container counts independently, in its own state volume. Do not point two running replicas at the same volume.
Running on a GPU
CPU is the simple default and is fine at low and moderate volume: a 16-core host measures about 3,900 parses/hour, and 8 GB of RAM is recommended.
For sustained high volume, run affinda/resume-parser:latest-gpu on a GPU host. It is not just
faster, it is cheaper per document. The box we certify against is one NVIDIA L4, which on AWS is
g6.2xlarge. On our certification mix (89% text PDF, 10% DOCX, 1% scanned) one such box
sustained about 171,000 parses/hour with zero rejected requests, which at the US East
on-demand price we measured against (about $0.98/hour) works out to roughly 175,000 resumes
per dollar. That is more than an order of magnitude cheaper per document than the same volume
on CPU hosts, so if you are chasing the lowest cost at high volume, GPU is the answer.
Two caveats before you size a fleet from those numbers. The scanned fraction dominates throughput: the same box measured about 81,000 parses/hour when 10% of the documents were scans. And cloud prices move. Treat throughput and cost as roughly ±15%, run your own document mix past one box first, and scale out with identical boxes rather than one bigger one.
What is different from the cloud
| Cloud | Container | |
|---|---|---|
| Base URL | https://resume-parser.affinda.com |
http://localhost:8080 |
| Parse path | /v1/resumes/parse |
/v1/parse, and /v1/resumes/parse as an alias |
| Authentication | Authorization: Bearer <key> |
None. The license sits in the container. |
| Billing | One credit per successful parse | Licensed. Credits are not involved. |
| Exhausted allowance | 402 no_credits |
402 entitlement_required |
| Request size cap | 20 MB | 50 MB by default |
| Response headers | X-Credits-Remaining |
X-Affinda-License, X-Affinda-Parses-Remaining |
The response body is the same in both. The cloud returns the container's bytes unchanged, which is why The parse response applies equally to a self-hosted deployment.
What the container adds
It has its own API surface that the cloud does not expose:
| Endpoint | What it answers |
|---|---|
GET /health, GET /ready |
Liveness and readiness, for your orchestrator. |
GET /version |
The build, the model id and the inference backend. |
GET /formats |
Which formats this container accepts, as it is configured right now. |
GET /stats |
Throughput and queue counters. |
GET /license |
License tier, expiry and parses remaining. |
It also accepts query parameters on the parse path that the cloud endpoint does not forward,
including filename, confidence, debug=timings and diagnostic=1. Use GET /formats and
these parameters when you are debugging a document: they answer questions about your own
deployment that the cloud endpoint cannot.
The container's own reference lives in the image, alongside its changelog and troubleshooting notes.
Configuration worth knowing
| Variable | Default | What it does |
|---|---|---|
AFFINDA_PORT |
8080 |
Listen port. |
AFFINDA_LICENSE_TOKEN, AFFINDA_LICENSE_TOKEN_FILE |
unset | The license. The file form wins. |
AFFINDA_WORKERS |
sized from the host | How many parses run at once. |
AFFINDA_OCR |
on | OCR for scans and images. With it off, images are rejected with 415 unsupported_format. |
AFFINDA_STRICT_INTAKE |
unset | Set to 1 for a stricter posture that also rejects HTML, RTF and legacy .doc. Unset is the recommended default. |
Everything else has a working default. Size the host by parse volume rather than by document size, and see Running on a GPU once volume is the thing you are sizing for.