PINGDOM_CHECK

#ExtractSummit2026 The world's largest web scraping conference returns. Austin Oct 7–8 · Dublin Nov 10–11.

Register now
Data Services
Pricing
Login
Try Zyte APIContact Sales
  • Unblocking and Extraction

    Zyte API

    The ultimate API for web scraping. Avoid website bans and access a headless browser or AI Parsing

    Ban Handling

    Headless Browser

    AI Extraction

    SERP

    Enterprise

    DocumentationSupport

    Hosting and Deployment

    Scrapy Cloud

    Run, monitor, and control your Scrapy spiders however you want to.

    Coding Agent Add-Ons

    Agentic Web Data

    Plugins that give coding agents the context to build production Scrapy projects. Starts with Claude Code.

  • Data Services
  • Pricing
  • Browse

    • BlogArticles, podcasts, videos
    • Case studiesCustomer outcomes
    • White papersIn-depth reports
    • DocumentationGuides & API reference
    • EventsConferences, webinars, recordings

    Subscribe

    • NewsletterSwiftly delivered
    • Join our community2,000+ web scraping engineers
  • Product and E-commerce

    From e-commerce and online marketplaces

    Data for AI

    Collect and structure web data to feed AI

    Job Posting

    From job boards and recruitment websites

    Real Estate

    From Listings portals and specialist websites

    News and Article

    From online publishers and news websites

    Search

    Search engine results page data (SERP)

    Social Media

    From social media platforms online

  • Meet Zyte

    Our story, people and values

    Contact us

    Get in touch

    Support

    Knowledge base and raise support tickets

    Terms and Policies

    Accept our terms and policies

    Open Source

    Our open source projects and contributions

    Web Data Compliance

    Guidelines and resources for compliant web data collection

    Join the team building the future of web data
    We're Hiring
    Trust Center
    Security, compliance & certifications
Login
Try Zyte APIContact Sales
All articles
AI71, 71 articles
Data quality15, 15 articles
Developer interest59, 59 articles
Integration2, 2 articles
Open-source50, 50 articles
Proxies35, 35 articles
Scraping practice35, 35 articles
Scraping strategy47, 47 articles
Search results4, 4 articles
Web data74, 74 articles
Web scraping APIs49, 49 articles
Scrapy47, 47 articles
Scrapy Cloud26, 26 articles
Web Scraping Copilot11, 11 articles
Zyte API67, 67 articles
AI & Machine Learning3, 3 articles
Automotive3, 3 articles
E-commerce & retail33, 33 articles
Entertainment & Streaming2, 2 articles
Financial Services8, 8 articles
Government2, 2 articles
Market Research & Intelligence7, 7 articles
Media & publishing11, 11 articles
Real Estate2, 2 articles
Recruitment & HR3, 3 articles
Transportation & Logistics2, 2 articles
Travel & hospitality3, 3 articles
iPaaS2, 2 articles
Large language model29, 29 articles
MCP3, 3 articles
Python110, 110 articles
Scraping at Scale7, 7 articles
Scraping Fundamentals11, 11 articles
Web Scraping Industry Report20, 20 articles

Appearance

Discord Community
BlogLearnBuilding a production-style web scraper with Scrapy, Docker, and PostgreSQL
LearnWeb scraping APIs

Building a production-style web scraper with Scrapy, Docker, and PostgreSQL

A

Ayan Pahwa

·

7 min read · March 2, 2026

Watch the video here

Web scraping is often taught using scripts that invariably dump data into a JSON or CSV file. That’s fine for learning the basics, but it doesn’t reflect how scraping works at scale in real-world systems.

In practice:

  • Scrapers usually run as jobs, not always running scripts or daemons.
  • Data needs to be stored reliably.
  • Environments must be reproducible.
  • Scaling and maintenance should be easy.

In this blog post, let me walk through a demo project called scrape2postgresql, which shows how to:

  1. Use Scrapy to scrape structured data.
  2. Store results in PostgreSQL.
  3. Run everything using docker-compose.
  4. Keep spiders and database in separate containers.

This project uses books.toscrape.com, a safe demo website, to scrape book titles and prices, but the structure applies to almost any scraping use case.

Why Scrapy?

Scrapy is a full-featured web scraping framework, not just a request library.

It gives you:

  • A crawling engine.
  • Request scheduling.
  • Built-in support for pagination.
  • Structured item pipelines.
  • Retry and error handling.
  • Clear project structure.

Instead of writing while loops, requests and BeautifulSoup, Scrapy encourages you to think in terms of spiders, items, and pipelines. It scales much better as projects grow.

Why Docker (and docker-compose)?

A very common beginner setup looks like this:

  • Scrapy installed locally.
  • PostgreSQL installed locally.
  • Different Python versions and virtual environments.

This becomes painful fast, difficult to scale, manage and maintain. Enter Docker, which solves this by:

  • Packaging dependencies into a container.
  • Making environments consistent, reproducible and talk to one another.
  • Isolating concerns cleanly and sandboxing local networking.
  • Want to change databases from PostgreSQL to mongoDB? Just fetch it from docker hub and plug it in.
  • Want to log data in a chart? Add another container such as Grafana.

docker-compose goes one step further by allowing us to run multiple containers together as a bundle, taking care of inter-connectivity:

  • One container for Scrapy.
  • One container for PostgreSQL.

Each container does its individual thing well, making it easier to maintain the project and isolate bugs, if any.

High-level architecture

Before we dive into code, let’s understand the architecture.

We’re using docker-compose to fire up two Docker containers:

  1. The first container has our Scrapy spider whose sole job is to scrape the web page we provide and store the data in database - it starts only when we need it.
  2. The other container is a PostgresSQL database with a persistent volume mounted on the host - whatever information our spider scrapes gets stored in this database.

Since this is docker-compose the networking between these two containers is sorted, we just need to use the authentication credentials.

Scrapy Container (one-shot job) ───────> PostgreSQL Container (persistent service)

Key philosophy:

  • Scrapy is a job
    • starts
    • crawls
    • stores data
    • exits
  • PostgreSQL is a service
    • stays running
    • persists data
    • can be queried anytime

This separation is extremely important for scaling and maintenance.

Project Structure

Here’s the structure of scrape2postgresql:

1.
2├── docker-compose.yml
3├── Dockerfile
4├── Makefile
5├── requirements.txt
6├── run_spider.sh
7│
8└── bookscraper/
9    ├── scrapy.cfg
10    └── bookscraper/
11        ├── items.py
12        ├── pipelines.py
13        ├── settings.py
14        └── spiders/
15            └── books.py
Copy

Let’s go through each part and understand why it exists.

Scrapy project

The spider (/spiders/books.py)

The spider is where the website's crawling logic lives.

At a high level, our spider:

  • Accepts a URL dynamically.
  • Extracts book titles and prices.
  • Follows pagination links.
  • Yields structured data.

Initializing the spider

1def __init__(self, url=None, max_pages=None, *args, **kwargs):
2    super().__init__(*args, **kwargs)
3
4    if not url:
5        raise ValueError("You must pass a URL")
6
7    self.start_urls = [url]
Copy

Instead of hard-coding URLs, we pass them at runtime. This makes the spider reusable for different categories or sites with similar structure.

CSS selectors

Scrapy supports both XPath and CSS selectors. CSS selectors are usually simpler and more readable.

Example:

1for book in response.css("article.product_pod"):
2    title = book.css("h3 a::attr(title)").get()
3    price = book.css("p.price_color::text").get()
Copy

What this means:

  • article.product_pod selects each book card
  • h3 a::attr(title) extracts the book title
  • p.price_color::text extracts the price text

CSS selectors map directly to how the HTML is structured, making them easy to debug in browser DevTools.


Handling pagination

Pagination is one of the most important parts of any crawler. Basically it’s a logic using which you can navigate a website and move to the next page if/when needed.

1next_page = response.css("li.next a::attr(href)").get()
2if next_page:
3    yield response.follow(next_page, callback=self.parse)
Copy

Scrapy handles relative URLs automatically with response.follow(), so you don’t have to manually build full URLs.

This approach ensures:

  • All pages in a category are crawled
  • No duplicate requests.
  • No infinite loops.

The pipeline (pipelines.py)

Spiders extract data, but pipelines store data. This separation is intentional.

Our pipeline:

  • Opens a PostgreSQL connection.
  • Creates a table if needed.
  • Inserts each scraped item.
1class PostgresPipeline:
2    def open_spider(self, spider):
3        self.conn = psycopg2.connect(...)
4        self.cur = self.conn.cursor()
Copy

The open_spider() method runs once, when the spider starts.

Inserting data

1def process_item(self, item, spider):
2    self.cur.execute(
3        "INSERT INTO books (title, price) VALUES (%s, %s)",
4        (item["title"], item["price"])
5    )
6    self.conn.commit()
7    return item
Copy

Each item yielded by the spider passes through the pipeline.

This makes it easy to:

  • Add validation.
  • Normalize data.
  • Store in different backends later.

Dockerizing the Scraper

Dockerfile

The Dockerfile defines how the Scrapy container is built.

1FROM python:3.11-slim
2WORKDIR /app
3COPY requirements.txt .
4RUN pip install -r requirements.txt
5COPY bookscraper /app/bookscraper
6COPY run_spider.sh /app/run_spider.sh
7RUN chmod +x /app/run_spider.sh
Copy

Key points:

  • Uses a lightweight Python base image.
  • Installs dependencies once.
  • Copies the Scrapy project into the container.
  • Includes a run script.

The run script (run_spider.sh)

This script is what actually runs when the container starts.

1if [ -z "$URL" ]; then
2  echo "ERROR: URL not provided"
3  exit 1
4fi
5
6scrapy crawl books -a url="$URL"
Copy

Why a script?

  • Easier debugging.
  • Clearer error messages.
  • Simpler command invocation.
  • Easier to extend later (cron, retries, etc.).

Docker Compose

Docker Compose ties everything together.

1services:
2  postgres:
3    image: postgres:15
4    volumes:
5      - pgdata:/var/lib/postgresql/data
6
7  scrapy:
8    build: .
9    depends_on:
10      - postgres
Copy

Important concepts here:

  • Separate containers.
  • Shared network.
  • Persistent volumes.
  • Explicit dependencies.

Scrapy can talk to PostgreSQL using the service name (postgres) as hostname.

Makefile

Instead of typing long Docker commands, I’ve created a Makefile.

Clone project from https://github.com/apscrapes/scrape2postgresql and use make commands to set it up :

Example:

1make db
2make scrape url="https://books.toscrape.com/..."
3make psql
Copy

Why this design scales well

This setup scales since each component is isolated and replaceable.:

What? How?
Want more spiders? Add more Scrapy spiders
Want scheduled scraping? Trigger make scrape via cron or CI
Want a another DB? Swap PostgreSQL with another docker image (e.g., MongoDB)
Want to plot data-points? Add Grafana container

Final thoughts

scrape2postgresql is intentionally simple, but architecturally solid.

It demonstrates:

  • How Scrapy is meant to be used.
  • How Docker simplifies environments.
  • Why separating spiders and databases matters.
  • How real scraping pipelines are structured.

If you’re new to web scraping, this project gives you a strong foundation. If you’re experienced, it gives you a clean starting template.

Next steps you could explore

  • Add item validation.
  • Include Zyte to avoid bans.
  • Store historical price changes.
  • Add retries and throttling.
  • Expose data via an API.
  • Schedule scraping jobs.

Once you understand this setup, you can build data pipelines at scale.

Happy scraping 🚀.

In this article

  • Why Scrapy?
  • Why Docker (and docker-compose)?
  • High-level architecture
  • Project Structure
  • Scrapy project
  • The spider (/spiders/books.py)
  • The pipeline (pipelines.py)
  • Inserting data
  • Dockerizing the Scraper
  • Dockerfile
  • The run script (run_spider.sh)
  • Docker Compose
  • Makefile
  • Why this design scales well
  • Final thoughts
  • Next steps you could explore

Other lessons

Learn Scrapy

  • Scrapy Tutorial Part 1: First Spider
  • Scrapy Tutorial Part 2: Page Objects
  • Scrapy Tutorial Part 3: Web Scraping CoPilot

What is web scraping?

  • What Is Web Scraping?
  • What are the elements of a web scraping project?
  • Python Web Scaping Tools & Libraries
  • How to architect a web scraping solution: The step-by-step guide
  • Web crawling vs web scraping
  • Is Web & Data Scraping Legally Allowed?
  • Compliant Web Scraping Checklist
  • Best practices for web scraping
  • A Guide to Web Scraping With Java
  • Transition from Zenrows to Zyte API
  • Guide to Web Scraping APIs
  • Screen Scraping Explained
  • Large Scale Web Scraping with Python
  • Large Scale Web Scraping with Python
  • Building a Web Crawler in Python
  • A Practical Guide to XML Parsing with Python
  • Learn How to Scrape a Website
  • Advanced Use Cases for Session Management
  • Golang Web Scraping in 2025
  • Web Scraping Dynamic Websites With Zyte API
  • What is Data Parsing in Web Scraping?
  • Scrape Web Pages and Files Using Python, wget, and Zyte

Web Scraping How-to Videos

  • Web scraping videos

SERP Data Collection at Scale

  • SERP data collection at scale and why efficiency matters
  • Why Page One SERP data is no longer enough
  • Why pagination logic becomes operational debt
  • Why SERP data costs exploded

What is web scraping used for?

  • What is web scraping used for?
  • Pricing Intelligence Web Scraping
  • Web Scraping For Market Research
  • Use web scraping to build a data-driven product
  • Use web scraping for alternative data for finance
  • Use web scraping for brand monitoring
  • Use web scraping to automate MAP compliance
  • Web Scraping For Lead Generation
  • Web Scraping For Recruitment
  • Use web scraping for business automation
  • Using Data Extraction Tools for Efficient Website Scraping
  • Why Might a Business Use Web Scraping to Collect Data?
  • How to Scrape Images from Any Website: A Complete Guide
  • How to Scrape Search Engine Results

The New Guide to Web Scraping at Scale

  • Introduction
  • 1. A plan is a pathway to success
  • 2. Get serious about legal compliance
  • 3. The quality of your web data is of utmost importance
  • 4. Scaling and maintaining crawling and extracting solutions
  • 5. Adding AI to the web scraping stack
  • 6. The In-house vs outsourced question
  • 7. Questions to ask when scaling web scraping

Essential Web Scraping Techniques

  • TLS Fingerprint and how it blocks requests
  • How to scrape with a browser effectively
  • API First data extraction

More learn articles

Keep learning

All learn articles →
What are residential proxies bannerUse case

What is a residential proxy?

Learn what residential proxies are, how they compare to datacenter proxies, and why modern web scraping needs more than IP diversity.

10 min read

Zyte Case Studies — every customer story, in one placeUse case

How much do rotating proxies cost?

Learn how much rotating proxies cost, what affects pricing, and why total web scraping costs often go beyond proxy subscriptions.

10 min read

Zyte Case Studies — every customer story, in one placeUse case

How do rotating proxies work?

Learn how rotating proxies work, when to use them for web scraping, and why IP rotation alone is not enough for reliable data access.

10 min read

Services

Zyte Data

Coding tools & hacks straight to your inbox. Bi-weekly dosage of all things code.

Explore Zyte Data

Web Scraping API

Zyte API

Coding tools & hacks straight to your inbox. Bi-weekly dosage of all things code.

Sign Up

Developers

Zyte Developers

Coding tools & hacks straight to your inbox. Bi-weekly dosage of all things code.

Join Us
    • Zyte API
    • Ban Handling
    • AI Extraction
    • SERP
    • Enterprise
    • Scrapy Cloud
    • Agentic Web Data
    • Pricing
    • Product & E-commerce
    • Data for AI
    • Job Posting
    • Real Estate
    • News & Articles
    • Search
    • Social Media
    • Blog
    • Learn
    • Case Studies
    • Webinars
    • White Papers
    • Join our community
    • Documentation
    • Meet Zyte
    • Contact us
    • Jobs
    • Support
    • Terms and Policies
    • Trust Center
    • Do not sell
    • Cookie settings
    • Web Data Compliance
    • Open Source
    • What is Web Scraping
    • Web Scraping in Python: Ultimate Guide
    • Stop getting blocked, start scraping
  • EWDCI logoMost loved workplace certificateZyte rewardISO 27001 iconG2 rewardG2 rewardG2 reward
    XFacebookInstagramYouTubeLinkedInDiscord

    © Zyte Group Limited 2026