Docker Deployment for Self-Hosted Placeholder Image Workers
Run a docker placeholder image api worker locally or in CI with a single container. Covers image sizing, caching headers, reverse proxy setup, and production readiness.
A docker placeholder image api container gives teams a fully isolated, reproducible image service that mirrors what fallback.pics serves at the edge — useful for air-gapped environments, CI pipelines where external network calls are blocked, or internal tooling that must not depend on third-party uptime.
This guide walks through writing a minimal Dockerfile for a Node-based SVG generator, wiring cache headers, setting up a reverse proxy, and connecting it to the same URL shape your production code already expects.
Use case
When you need a self-hosted docker placeholder image api
Most teams reach for self-hosting when CI jobs run in a sandbox with no external internet access. Playwright or Cypress suites that render product grids will show broken-image icons if they call fallback.pics and the network is blocked. Replacing every test fixture with a local static file is tedious and hard to maintain at scale.
Internal enterprise tools present a second reason. Some security policies prohibit loading assets from domains outside the corporate trust boundary. A containerized placeholder service satisfies the policy without forcing every frontend team to maintain their own static asset library.
The third scenario is cost control at very high volume. The edge service handles the vast majority of production traffic efficiently, but teams occasionally prototype features that hammer the API hundreds of thousands of times in load tests. A local container absorbs that traffic without touching rate limits.
Dockerfile
Minimal Node container for SVG placeholder generation
The worker itself needs only a URL parser, a string template for SVG, and an HTTP server. Node's built-in `http` module plus a small routing function is enough. Avoid image-processing libraries like Sharp unless you need raster output — they add hundreds of megabytes to the image and make layer caching slower.
Pin the Node version to an LTS release and use a slim or alpine base. The final image should be under 100 MB. Set `NODE_ENV=production` to prevent dev-only dependencies from being installed.
# Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/index.js"]
# src/index.js (minimal SVG server)
import http from 'node:http';
import { parse } from 'node:url';
const PORT = process.env.PORT ?? 3000;
http.createServer((req, res) => {
const { pathname, query } = parse(req.url, true);
const match = pathname.match(/^\/api\/v1\/(\d+)x(\d+)/);
if (!match) {
res.writeHead(400); res.end('Bad request'); return;
}
const [, w, h] = match;
const bg = query.bg ?? '7C3AED';
const fg = query.fg ?? 'FFFFFF';
const text = query.text ?? `${w}×${h}`;
const svg = `<svg xmlns="http://www.w3.org/2000/svg"
width="${w}" height="${h}">
<rect width="100%" height="100%" fill="#${bg}"/>
<text x="50%" y="50%" fill="#${fg}"
font-family="system-ui,sans-serif"
font-size="${Math.min(+w,+h)*0.12}"
text-anchor="middle" dominant-baseline="middle">
${text}
</text>
</svg>`;
res.writeHead(200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
});
res.end(svg);
}).listen(PORT); Compose setup
docker-compose for local development and CI
A compose file gives developers a one-command start and lets CI services declare the placeholder container as a dependency with `depends_on`. Map port 3000 inside the container to whatever the test suite expects — commonly 3001 to avoid colliding with the app's own dev server.
Set a health check so CI does not try to run tests before the server is ready. A `curl` to the `/api/v1/1x1` route is the simplest probe.
# docker-compose.yml
services:
placeholder:
build: .
ports:
- "3001:3000"
environment:
PORT: 3000
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/v1/1x1"]
interval: 5s
retries: 3
# In your test env, point image requests here:
# NEXT_PUBLIC_PLACEHOLDER_BASE=http://localhost:3001/api/v1 Reverse proxy
Nginx cache and path rewriting for production containers
If you deploy the container behind Nginx, add a proxy_cache zone so the SVG generator process only runs once per unique URL. Subsequent requests are served from disk. This mirrors the CDN layer that fallback.pics uses in production.
Rewrite the path if your internal URL convention differs from `/api/v1/{w}x{h}`. A single `rewrite` rule at the proxy layer means application code never needs to know about the container's routing logic.
# nginx.conf (relevant block)
proxy_cache_path /var/cache/nginx
levels=1:2 keys_zone=placeholders:10m
max_size=1g inactive=1y use_temp_path=off;
server {
location /api/v1/ {
proxy_pass http://placeholder:3000;
proxy_cache placeholders;
proxy_cache_valid 200 1y;
add_header X-Cache-Status $upstream_cache_status;
}
} CI integration
Wiring the container into GitHub Actions
GitHub Actions supports Docker Compose services directly via the `services` key. Declare the placeholder container there and it will start before your test step. Use `PLACEHOLDER_BASE_URL` as an environment variable in tests so you can swap between local, CI, and production without code changes.
For Playwright specifically, set `baseURL` in `playwright.config.ts` to use an environment variable and pass the container's address in CI. That way visual regression baselines match whether a developer runs tests locally against fallback.pics or in CI against the container.
# .github/workflows/test.yml (excerpt)
services:
placeholder:
image: ghcr.io/your-org/placeholder-api:latest
ports:
- 3001:3000
env:
PLACEHOLDER_BASE_URL: http://localhost:3001/api/v1
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test Production checklist
What to harden before deploying the container to production
Set `max-age=31536000, immutable` in Cache-Control. Without this, every request hits the Node process and the value of the container over a CDN drops to zero. Add an `ETag` header derived from the URL parameters so conditional GETs return 304s without regenerating SVG.
Restrict accepted dimensions to prevent abuse. A request for a 50000×50000 SVG can be served trivially but some downstream clients may try to rasterize it. Add an upper bound — 4096 pixels per side is reasonable for most use cases.
Run the container as a non-root user. Add `USER node` to the Dockerfile after dependencies are installed. Log to stdout so your container orchestrator can forward logs to your observability stack without needing a file-based log drain.
Related resources
Further reading on placeholder infrastructure
If the self-hosted route adds more operational overhead than your team can justify, the managed fallback.pics service provides the same URL shape with a global CDN, zero ops, and immutable cache semantics out of the box.
# Managed alternative (no container required)
https://fallback.pics/api/v1/400x300/7C3AED/FFFFFF?text=Product
# Docs
https://fallback.pics/docs/
https://fallback.pics/placeholder-image-api/
# Related posts
https://fallback.pics/blog/self-hosted-vs-managed-placeholder-api/
https://fallback.pics/blog/cloudflare-cdn-cache-generated-images/ Key takeaways
What to standardize before shipping
- Use a containerized placeholder service in CI environments where external network access is blocked.
- Keep the Docker image under 100 MB by avoiding heavy raster-processing libraries for SVG-only output.
- Add Nginx proxy caching to avoid regenerating SVGs for every request in high-traffic deployments.
- Cap accepted dimensions (e.g. 4096px) and run the container as a non-root user before going to production.
- Pass the placeholder base URL via environment variable so tests work against both the container and the managed service.
Production fallback layer
Use fallback.pics anywhere an image URL is accepted.
Start with one deterministic URL and standardize fallback behavior across your design system.