Technology and innovation in the Red Collar Font site
We promised you some updates — it’s time for a detailed story about how we worked on the custom Red Collar Font
A practical guide to building fast, reproducible, and secure Docker environments for Python with uv, multi-stage builds, BuildKit caching, and a clear separation between development and production.
A fast local build isn't enough if CI produces a different environment or development tools end up in production. For this pipeline, the goal was to keep local iterations fast, lock dependency versions and hashes, separate development and testing from runtime, and reduce the final image without adding configuration "magic."
This article is based on a technical guide by Ivan Vyalov. We've kept the main decisions and their practical effects here; the complete version with the Dockerfile, Compose setup, and commands is linked at the end.
Our approach uses one Dockerfile for every environment, multi-stage builds, dependencies managed through uv, and no unexpected changes during the build. Development, testing, and production still have different requirements, but they share the same foundation.
uv combines fast dependency resolution and installation with a lock file that records exact package versions and hashes. Its frozen mode prevents silent dependency updates during a build, so any drift between the expected and actual environment becomes a build error.
The speed difference is especially noticeable in larger projects with dozens of packages and heavy dependencies. Builds that previously took minutes can remain predictably fast even with a cold cache, while the lock file keeps the result reproducible.
Dependency groups keep development and test tooling out of the production image. A local build can enable the development group, CI can add the test group, and production can build without either. Default development groups are disabled and enabled only in the environments where they are needed.
The virtual environment is created at a fixed location during the build and preserved as an artifact. The final stage can copy it directly from the builder instead of resolving and installing the dependency graph again.
Python itself remains controlled by the base image. Disabling interpreter management and automatic Python downloads removes uncertainty about which interpreter enters the build. Bytecode is compiled in advance to reduce cold-start latency, while copy mode avoids hard-link issues between filesystems.
An explicit uv cache makes repeated builds faster, especially in CI. Frozen dependency resolution and hash verification complete the process: the cache improves speed without changing the contents of the environment.
BuildKit cache mounts preserve downloaded artifacts between builds. Bind mounts make the project definition and lock file available during dependency installation without adding those files permanently to the layer.
The application is not installed in editable mode, so there are no live development paths inside the container. Build arguments determine which dependency groups are included, and Docker caches different group combinations separately.
Together, these decisions keep the dependency layers clean and reusable. A change to the application source doesn't require the packages to be downloaded and installed again when the dependency files remain unchanged.
The build stage handles the heavy work. It creates the virtual environment, installs the base dependencies, and adds the application source. It is also the right place to generate code, compile extensions, or prepare frontend assets, localizations, and client SDKs. Anything used only during that process remains in the builder.
The test stage extends the builder and adds the test dependency group. Running the test suite before assembling the final image catches environment and dependency issues earlier. Keeping the tests in a dedicated layer also allows CI to cache the test-group installation and reuse it during later runs.
The production stage starts from a clean Python base image and receives only the prepared environment and application code. The final image contains the interpreter, the environment, and the code required to run the service. Build tools and test artifacts stay behind, making the image faster to start, easier to scan for vulnerabilities, and simpler to update.
The entrypoint is the service's front door. It can run database migrations, validate configuration, and warm caches before handing control to the main application.
The final step needs to replace the shell with the application process. This makes the application PID 1 and allows it to receive operating-system signals correctly.
Separating the entrypoint from the main command keeps the image flexible. The preparation steps remain consistent, while the startup command can change without editing the image.
Local development prioritizes fast restarts and immediate feedback. Mounting the application source into the container makes changes available quickly, while a separate development environment file keeps local parameters apart from production secrets.
Production uses a self-contained image without mounted source volumes. A health check helps the orchestrator determine the container's state, and a restart policy allows the service to recover from brief issues.
Compose profiles keep these scenarios in a single configuration without duplication. Development, testing, production, and administrative services can be enabled independently while sharing the same underlying setup.
The CI pipeline follows the same stage structure. It prepares the base dependencies, adds the test group, runs the tests, and packages the release only after those checks succeed. Only the final image is published, which reduces the risk of extra layers entering the registry.
Caching the uv directory and separating the build stages speeds up this pipeline. We share shorter notes on development decisions, performance, and production trade-offs on Red Collar X.
Dependency updates happen before the production build. The team can check which packages are outdated, regenerate the lock file, review its diff, and run the tests before committing the change. In production, the build uses the existing lock file without modifying it.
The lock file remains the source of truth for a specific build, and package hashes are verified during installation. Dependency drift stops the build instead of introducing an accidental update into production.
The final image uses a base image without unnecessary packages and contains no development artifacts. Environment variables require strict handling, while the base image and application dependencies need regular updates and attention to relevant security bulletins. These measures keep the build deterministic while reducing the production attack surface.
Performance was also a central constraint in our Skolkovo for Business case study. We built the website around an interactive 3D model of the business district, using low-polygon modeling to reduce loading time and memory consumption. Static shadows were baked into textures, while map data processing was optimized separately.
The technologies are different, but both projects treat performance as a build constraint from the start rather than a cleanup task at the end.
A useful Docker pipeline keeps local iteration fast without compromising reproducibility. Development and test dependencies appear only where they are needed, while production receives a compact image built from verified packages.
We promised you some updates — it’s time for a detailed story about how we worked on the custom Red Collar Font
Smart farming is raising its profile with help from agencies like Red Collar
We're nominated for the second year in a row. Awesome news!
We use cookies to collect anonymous data and make our website even better