This guide covers how to run tests and builds locally so your contributions pass the same checks as CI. The Makefile is the canonical entry point for every common task. It also shows how to simulate the GitHub Actions pipeline exactly before you push.
The easiest way to get a consistent dev environment is the VS Code Dev Container. It gives you a pre-configured Docker environment with all dependencies and extensions already installed.
- Open the project in VS Code.
- Install the Dev Containers extension.
- Click "Reopen in Container" when prompted (or run
Dev Containers: Reopen in Container from the command palette).
- Pre-installed tools: Python 3.10+, Node.js, pnpm, and Docker CLI
- Extensions: Python, Docker, ESLint, Playwright, and more
- Port forwarding: port
3001 (app) and 5000 (backend) are forwarded automatically
- Docker-in-Docker: you can build and run containers inside the dev container, which is required for integration and E2E tests
2. Make Targets (canonical entry point)#
All everyday dev tasks run through the Makefile at the repo root. Helper scripts live under ./scripts/ and the Makefile wires them up so you don't have to remember paths or flags.
β οΈDocker Hub login required (starting v0.7.0)
Starting with the v0.7.0 release, imgcompress builds on top of Docker Hardened Images (DHI) for a reduced OS attack surface and strict non-root runtime. DHI base images are pulled from a private registry on Docker Hub, so before running any make target that builds an image (build, build-local, scan, deploy-local, deploy-local-dev-mode, simci) you need to be logged in:
Use a Docker Hub account that has access to the hardened image repository. If you skip this step the build will fail with an unauthorized pull error on the base image.
The Makefile exposes a few variables you can override on the command line:
For working on the app end-to-end (frontend + backend, exactly as users see it), use:
This builds the image with build-local and runs it through ./scripts/runLocalDockerBuildTester.sh, so what you see locally matches the published image. You can pass the same environment overrides the script accepts:
Once running, open http://localhost:8080 (or whatever PORT_HOST you set, default 80).
π‘Why this path is preferred
The Docker path bundles the built frontend assets into the image and runs the production server, so route behaviour, static-asset serving, and runtime config all behave the same as in production. The raw split-process scripts below skip that bundling step and require everything to be set up just right.
make deploy-local-dev-mode starts the container with DEV_MODE=true. When this flag is on, an amber DEV pill appears in the top-left of the UI. Clicking it opens a Developer Tools panel with four buttons used to verify error rendering paths:
Developer Tools panel with synthetic error triggers, shown when DEV_MODE is on
- Trigger API Error / Trigger API Error (long stack) β render a synthetic API failure through the API error widget, with either a short message or a one-page traceback.
- Trigger Runtime Error / Trigger Runtime Error (long stack) β throw inside the React tree to verify the global error boundary, with either a short message or a one-page traceback.
Clicking a runtime trigger takes you to the global error boundary screen so you can verify scroll behaviour, wrap-on-long-paths, the diagnostics download, and the "Try Again" recovery path:
Runtime Error boundary screen with a one-page synthetic stack trace
You can also pass the env var manually instead of going through Make:
Dev mode is off by default. The published image does not enable it on its own, but the flag is a plain env var, so anyone self-hosting can opt in by setting DEV_MODE=true on their docker run command or in their docker-compose.yml. It is intended for development and error-rendering verification, not for end-user features.
If you want hot-reload on the frontend while iterating, you can run the backend and frontend as separate processes. This path needs your local Python venv and Node toolchain to be ready, and is more brittle than make deploy-local.
Prerequisites:
- Python 3.10+ with the project installed in a venv (
pip install -e ".[dev]")
- Node.js & pnpm for the frontend
- Docker for integration tests
Run each in its own terminal from the repo root:
- Backend:
./scripts/runStartLocalBackend.sh
- Frontend:
./scripts/runStartLocalFrontend.sh
- E2E (optional):
./scripts/run-e2e.sh
β οΈKnown limitation
The split-process flow doesn't bundle the frontend into the backend the way the production image does, so some scenarios (static asset routing, runtime config injection) won't match what users see. If you hit unexpected behaviour, switch to make deploy-local to confirm whether the issue reproduces against the production-style stack.
You can run any test suite directly through Make without standing up the app:
See the Playwright E2E Testing Guide for debugging tips, targeted runs, and reports.
This is the one command that mirrors the full GitHub Actions pipeline end-to-end: builds the devcontainer, runs lint and tests inside it, builds the app image, and runs E2E against it. If this passes locally, CI will pass.
If you need to debug a single CI step in isolation (for example, to repro a failing unit-test run inside the devcontainer image), you can invoke the underlying Docker commands directly. Prefer make simci for normal use.
Show manual Docker commands
Build the Dev Container
Backend Unit Tests
Backend Integration Tests
End-to-End (E2E) Tests
-
Build the ImgCompress image:
-
Start the app:
-
Run E2E:
Dependabot PRs that touch dependencies can leave a corrupted pnpm-lock.yaml when multiple lockfile changes are merged without a rebase.
β οΈWarning
You may see an error like:
This means the lockfile has invalid YAML, usually from duplicated dependency entries.
Multiple Dependabot lockfile edits merged together without rebasing. pnpm validates YAML structure and fails immediately on duplicates.
The fastest path is to ask imgcompress-chan, the repo's helper bot, to handle it for you. Drop a comment on the affected PR with /chan-fix somewhere in it. She loves a polite ask:
Hey chan, can you /chan-fix it please? π
She'll refresh the branch from main, regenerate frontend/pnpm-lock.yaml with the pinned pnpm version, and push the repair commit so CI re-runs. Full details, allowed users, and the manual fallback live on her page: imgcompress-chan.
Rebase Dependabot PRs that modify pnpm-lock.yaml before merging, and let CI run pnpm install on the branch to catch issues early.