Skip to project
pedromartins.tech
MIT licensed
Docker / Backend / FastAPI / DevOps2026

Docker Email Tracker

Separated fast atomic counting from durable auditing in a three-service tracking stack with explicit privacy and failure boundaries.

Role
Backend and DevOps engineer
Environment
Local container simulation
Ownership
FastAPI endpoint · Redis counters · PostgreSQL audit · Docker networks · Health checks · Privacy controls
Open live project
Docker Email Tracker project visual
tracker-request-lab5 system boundaries
Email Client
FastAPI
Redis
PostgreSQL
3orchestrated services
1×1transparent GIF response
AtomicRedis counter update
0PII values embedded in the URL
01 / Interactive system

Move through the system layer by layer

tracker-request-labLocal container simulation · Read-only
MIT licensed
System map

Boundaries and responsibilities

Public or untrustedAuthenticated boundaryInternal-only dependency
READ-ONLYLocal container simulationtool: systemselection: email

Email Client selected.

02 / Context

The problem behind the system

A tracking pixel looks trivial until counting, durable history, caching, privacy, and partial failure arrive together.

The project uses a small request path to demonstrate clear service responsibilities: FastAPI handles the contract, Redis handles the hot counter, and PostgreSQL keeps the durable audit record.

Read the full project overview

An independent pixel tracking system for monitoring email engagement. It captures "email open" metrics by serving a 1x1 transparent image that triggers a backend event when loaded by a client.

The infrastructure uses Docker Compose to orchestrate three services: FastAPI for logic, Redis for high-performance counting, and PostgreSQL for persistent logging. Key features include isolated networks for security, persistent volumes for data integrity, and custom healthchecks to manage service startup order.

03 / Security

Threats, controls, and what remains

No control is presented as total risk elimination.

Privacy

Tracking URL exposes recipient identity

Control
Opaque token with no PII in the path
Residual risk
The token still acts as a correlatable identifier.
Data

Client IP becomes sensitive history

Control
Hash before storage and minimize request context
Residual risk
Hashes of low-entropy addresses can still be sensitive.
Integrity

Concurrent opens lose increments

Control
Atomic Redis pipeline
Residual risk
Counter and audit stores can temporarily diverge during failure.
04 / Operations

The unhappy path is part of the design

01

What if Redis is unavailable?

The atomic counter cannot be updated.

Signal
Internal connection and health checks fail.
Response
Return a bounded response policy and restore the counter service before claiming the event.
02

What if PostgreSQL is slow?

Durable auditing becomes the long pole in the request path.

Signal
Audit latency rises independently of the Redis increment.
Response
Preserve explicit failure semantics; do not silently present an incomplete audit as durable.
05 / Decisions

The trade-offs that shaped the build

Decision

Split counter and audit

Use each store for the behavior it serves best.

Reason
Fast atomic updates and durable history have different access patterns.
Trade-off
The service must make partial failure explicit.
Revisit when
When one datastore can satisfy both workloads without losing clarity.
Decision

Opaque token only

Keep identity details out of the URL.

Reason
URLs leak into logs, screenshots, and referrers.
Trade-off
Token lookup or mapping is required elsewhere.
Revisit when
Never for direct PII; only the token format may evolve.
Decision

Harden the tiny response

A one-pixel asset still deserves correct headers.

Reason
Caching and content sniffing can change observed behavior.
Trade-off
The response path carries a few more explicit rules.
Revisit when
When browser behavior or privacy policy changes.
06 / Implementation

How the decisions appear in the build

app/main.py
01# 1x1 pixel beacon. Atomic counting, isolated network, no PII in the URL.02from fastapi import FastAPI, Response, Request03import redis.asyncio as redis04 05app = FastAPI()06r = redis.from_url("redis://redis:6379", decode_responses=True)  # internal net only07 08PIXEL = bytes.fromhex("47494638396101000100800000000000ffffff21f9040100"09                      "0000002c00000000010001000002024401003b")10 11@app.get("/o/{token}.gif")12async def open_pixel(token: str, request: Request) -> Response:13    async with r.pipeline(transaction=True) as pipe:      # atomic multi-op14        await (pipe.hincrby(f"evt:{token}", "opens", 1)15                   .hset(f"evt:{token}", "last_ip_hash", _hash(request.client.host))16                   .execute())17    await audit.log(token=token, event="open")            # durable in Postgres18    return Response(19        content=PIXEL, media_type="image/gif",20        headers={21            "Cache-Control": "no-store",22            "X-Content-Type-Options": "nosniff",23            "Content-Security-Policy": "default-src 'none'",24        },25    )
Representative / sanitized excerpt

Atomic Redis counting, hashed client IP, hardened response headers, no PII in the URL.

PythonFastAPIuvicornHTMLPostgreSQLRedis
07 / Evidence

Proof, source, and inspectable outcomes

08 / Results

What the project demonstrates

Fast counting and durable auditing have explicit responsibilities

The public URL contains no direct recipient PII

Container health and network boundaries are part of the application design

What worked
  • The small system makes datastore trade-offs easy to inspect.
  • Privacy choices are visible in the request contract.
Next iteration
  • Explicit consent and policy documentation
  • More failure-injection tests
  • Bounded reconciliation between counter and audit
Professional signal
  • FastAPI backend design
  • Container orchestration
  • Privacy-aware event systems