PDFtoMD
Self-HostingDockerself-hostedprivacy

How to Self-Host PDF to Markdown Conversion with Docker

Keep sensitive documents in house by running PDF to Markdown conversion on your own servers. A guide to self-hosting with Docker, from container setup to a private conversion endpoint your team can call, with no data leaving your network.

9 min readBy Rafael Abellan

Some documents should never leave your network. Signed contracts, patient records, unreleased financials, HR files, source code specifications: the moment you upload one of these to a third-party website, you have handed a copy to someone else. For a lot of teams that is a hard no, whether because of a data residency law, a client contract, or simple caution. If that describes you, the good news is that PDF to Markdown conversion runs perfectly well on hardware you control.

This guide shows you how to self-host a PDF to Markdown converter with Docker. You will build a container that wraps an open-source conversion engine, expose a small private HTTP endpoint your team and scripts can call, and keep every byte of every document inside your own firewall. No files leave your network, no external API sees your data, and you get a repeatable deployment you can run on a laptop, a bare-metal server, or a private cloud instance.

Why Self-Host PDF to Markdown Conversion

A hosted converter is the fastest way to get clean Markdown out of a PDF, and for most everyday documents it is exactly the right call. But there are concrete reasons a team decides to run conversion in house:

  • Data privacy and compliance: Regulations like GDPR, HIPAA, and various data residency rules can require that certain documents stay within a specific network or jurisdiction. Self-hosting keeps the data path entirely under your control.
  • Confidential material: Legal bundles, board decks, and pre-release research are too sensitive to send to any outside service, even a trusted one.
  • Air-gapped environments: Some networks have no outbound internet access at all. A local container is the only option.
  • Volume and cost: If you are converting tens of thousands of documents, running your own engine on a machine you already pay for can be cheaper than per-page pricing.
  • Predictable behaviour: A pinned container image gives you the same conversion output every time, with no upstream changes you did not schedule.

The trade-off is real: you own the setup, the updates, and the quality tuning. Open-source engines are excellent but they are not magic, and complex layouts still need a human eye. If you only convert the occasional file and privacy is not a constraint, a hosted tool such as PDFtoMD is simpler. If control is the priority, read on.

What Goes Inside the Container

The heart of a self-hosted converter is the conversion engine. Several strong open-source projects turn PDFs into Markdown, and any of them can be containerized. Popular choices include Marker, MinerU, Docling, MarkItDown, and PyMuPDF4LLM. They differ in speed, accuracy on tables and equations, and how much they lean on machine-learning models. For a broad view of the landscape, see our roundup of the best PDF to Markdown converters compared.

For this guide we will use a lightweight, dependency-friendly stack: a small Python web service that wraps a conversion library, packaged with FastAPI so it exposes a clean HTTP endpoint. This keeps the image small, starts fast, and runs on CPU without a GPU. If you later need heavier OCR or equation handling, you can swap the engine without changing the container contract.

The Application

Start with a tiny FastAPI app. It accepts an uploaded PDF, runs it through the converter, and returns Markdown text. Save this as app.py:

from fastapi import FastAPI, UploadFile, HTTPException
from fastapi.responses import PlainTextResponse
import pymupdf4llm
import pymupdf
import tempfile, os

app = FastAPI(title="PDF to Markdown")

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/convert", response_class=PlainTextResponse)
async def convert(file: UploadFile):
    if not file.filename.lower().endswith(".pdf"):
        raise HTTPException(400, "Only PDF files are accepted")
    data = await file.read()
    with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
        tmp.write(data)
        path = tmp.name
    try:
        doc = pymupdf.open(path)
        markdown = pymupdf4llm.to_markdown(doc)
    finally:
        os.unlink(path)
    return markdown

Two endpoints, nothing else. /health lets your orchestrator check that the service is alive, and /convert does the work. The temporary file is deleted in a finally block so nothing lingers on disk after the response is sent.

Dependencies

Pin your versions in requirements.txt so every build is reproducible:

fastapi==0.115.0
uvicorn[standard]==0.30.6
pymupdf4llm==0.0.17
pymupdf==1.24.10
python-multipart==0.0.9

The Dockerfile

Now describe the image. This uses a slim Python base, installs the dependencies in a cached layer, copies the app in, and runs it with Uvicorn:

FROM python:3.12-slim

WORKDIR /app

# Install dependencies first so this layer caches
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the application
COPY app.py .

# Run as a non-root user for safety
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Running as a non-root user is a small hardening step that costs nothing. Copying requirements.txt before the application code means Docker can reuse the dependency layer whenever only your code changes, which makes rebuilds fast.

Not ready to run your own server? Convert a PDF to clean Markdown right now, free.

Build and Run the Container

With those three files in a folder, build the image and start it:

# Build the image and tag it
docker build -t pdf-to-md:1.0 .

# Run it, mapping port 8000 to the host
docker run -d --name pdf-to-md -p 8000:8000 pdf-to-md:1.0

# Confirm it is healthy
curl http://localhost:8000/health

You should see {"status": "ok"} come back. Now convert a real document. The endpoint takes a multipart file upload and returns Markdown as plain text:

curl -X POST http://localhost:8000/convert \
  -F "file=@contract.pdf" \
  -o contract.md

# Look at the result
head -n 40 contract.md

That is a working private conversion endpoint. Every step happened on your machine, and contract.pdf never touched the public internet.

A Compose File for Real Deployments

The single docker run command is fine for testing, but for anything you plan to keep running you want Docker Compose. It captures the configuration in a file you can commit and review. Save this as docker-compose.yml:

services:
  pdf-to-md:
    build: .
    image: pdf-to-md:1.0
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 2g

Two details matter here. Binding the port to 127.0.0.1 instead of 0.0.0.0 means the service listens only on the local machine, so it is not exposed to your wider network until you deliberately put a reverse proxy in front of it. The memory limit stops a very large or malformed PDF from exhausting the host. Bring it up with docker compose up -d and the container will restart automatically if the machine reboots or the process crashes.

Exposing a Private Endpoint to Your Team

A local port is useful for scripts on the same box, but your team needs to reach it too. The standard pattern is to put a reverse proxy such as Nginx or Caddy in front of the container. The proxy terminates TLS, adds authentication, and forwards clean requests to the service.

Here is a minimal Caddy configuration that serves the endpoint over HTTPS on your internal domain and requires a shared token. Save it as Caddyfile:

pdf.internal.example.com {
    # Require a bearer token on every request
    @noauth {
        not header Authorization "Bearer YOUR_LONG_RANDOM_TOKEN"
    }
    respond @noauth "Unauthorized" 401

    reverse_proxy 127.0.0.1:8000
}

Replace the token with a long random string kept in your secrets manager, and point the domain at an internal DNS record that never leaves your network. Now your team calls a single stable URL, the proxy checks the token, and the container does the conversion. For a fuller treatment of designing conversion endpoints, including request shapes and scaling, see our PDF to Markdown API integration guide.

If your users prefer to run everything on their own workstations rather than call a shared server, the same container image works locally. That pairs naturally with a fully offline AI stack, which we cover in optimizing PDFs for local LLMs with Ollama and LM Studio.

Scaling and Batch Conversion

Conversion is CPU bound, so throughput scales with cores. A few practical levers:

  • Multiple workers: Uvicorn can run several worker processes. Add --workers 4 to the command for a four-core machine, and each worker handles a request in parallel.
  • A queue for large jobs: If a single PDF can take a minute or more, do not make the client wait on the HTTP connection. Accept the file, return a job ID, and process it in a background worker. The client polls for the result.
  • Horizontal scaling: Because the container is stateless, you can run several copies behind the reverse proxy and let it round-robin between them.
  • Resource limits per container: Keep the memory cap from the Compose file so one bad document cannot take down a shared host.

For folder-level batch work you often do not even need the HTTP layer. You can shell into the container or run a one-off command that walks a directory. If that is your main use case, our guide to batch converting PDF to Markdown from the command line covers scripts that scale to hundreds of files.

Keeping Data Private, Even In House

Self-hosting removes the third party, but privacy still needs care inside your own walls. A few habits keep the deployment tight:

  • Do not persist inputs: The sample app writes a temporary file and deletes it immediately. Avoid logging document contents, and be careful that debug logging does not capture uploaded text.
  • Restrict network egress: The container needs no outbound internet access at run time. Block it at the firewall so even a compromised dependency cannot phone home.
  • Authenticate every request: The bearer token in the proxy is the minimum. For larger teams, integrate with your existing single sign-on.
  • Pin and scan images: Use exact version tags, and run a scanner such as Trivy against your image in CI so known vulnerabilities are caught before deployment.
  • Encrypt in transit: Terminate TLS at the proxy so documents are never sent as plaintext across your internal network.

These same privacy concerns are exactly why regulated teams look at self-hosting in the first place. If you work with contracts, reports, and internal wikis, our guide on PDF to Markdown for business teams walks through the document workflows that benefit most. And if you are still weighing whether a hosted service or a private deployment fits each type of document, the use cases page lays out the common scenarios side by side.

Frequently Asked Questions

Do I need a GPU to self-host PDF to Markdown conversion?

No. The stack in this guide runs entirely on CPU and works on a modest server or even a laptop. A GPU only becomes useful if you switch to a heavy machine-learning OCR engine for scanned documents or complex equations. For text-based PDFs, CPU conversion is fast and cheap.

Which conversion engine should I put in the container?

Start with a lightweight library like PyMuPDF4LLM for clean, text-based PDFs. If you deal with scanned pages, dense tables, or scientific notation, look at Marker, MinerU, or Docling, which lean harder on models and handle those cases better. The container contract stays the same. Only the engine inside changes.

Is self-hosting cheaper than a hosted converter?

It depends on volume. If you convert a handful of documents a week, a hosted tool is almost certainly cheaper once you account for the time to build, secure, and maintain your own setup. At high volume, or when compliance forbids external services, running your own container is the better economic and practical choice.

How do I keep the container updated?

Rebuild the image on a schedule with refreshed base and dependency versions, run your test PDFs through it to confirm output quality has not regressed, scan it for vulnerabilities, then roll it out. Because the image is versioned, you can pin the exact tag in production and roll back instantly if a new build behaves differently.

Can I run this fully offline or air-gapped?

Yes. Build the image on a connected machine, export it with docker save, move the archive into the air-gapped environment, and load it with docker load. Once the image is in place, the service needs no internet access to convert documents.

The Takeaway

Self-hosting PDF to Markdown conversion is more approachable than it sounds. An open-source engine, a thin web service, a Dockerfile, and a reverse proxy give you a private endpoint that keeps every document inside your network. You trade a little setup and maintenance for full control over your data, which for sensitive material is a trade worth making.

If your documents are not sensitive and you would rather skip the operations work entirely, PDFtoMD converts PDFs to clean Markdown in seconds, with a free tier of 3 conversions a month and no credit card required. Whichever path fits your risk profile, the goal is the same: clean, structured Markdown you can actually use, produced on terms you are comfortable with.

Rafael Abellan

About the author

Rafael Abellan

Founder, PDFtoMD

Rafael Abellan is the founder of Agência Triva and ships independent side projects in parallel. PDFtoMD came out of a personal frustration: he kept burning through Claude AI's token limit by uploading long PDFs, then losing hours waiting for the cap to reset. He built the tool to fix his own workflow, and now uses it every day.

Ready to convert your PDFs to Markdown?

Free account · 3 conversions/month · No credit card required