Dockerfile Contest 2025: Is This the Smallest Docker Image?
This is going to be a long and detailed article about how I wrote a Dockerfile that builds a Docker image of just 143KB. I’ll explain everything carefully, so if you’re willing to read through it, hopefully you’ll come away understanding quite a few new things. At least that’s the plan :))
Dockerfile Contest 2025 really made some waves. That day, I saw thousands of people watching the webinar live, and later the admins shared numbers showing millions of views and tens of thousands of interactions around the event. What stood out to me was that one of the experts won with a Dockerfile that produced a Docker image of just 218KB while still being able to serve a working website.
I’m definitely not here to rain on anyone’s parade. In fact, it was thanks to that expert that I realized there are some almost unbelievable ways to optimize static websites down to extremely small sizes. So the winner absolutely deserved it. If anyone disagrees, well, why didn’t you join the contest? I did join, by the way. I just failed :)))
So when the TOP Dockerfiles were published on Dockerfile Contest 2025, I started digging into them immediately. And here’s the result: a Docker image with a final size of 143KB, while still meeting the requirement of not modifying the source code.

And of course, the website still works perfectly. Otherwise, I wouldn’t dare write this article.

Once I finished it, I felt like sharing the whole thing. Who knows, maybe future activities like this will become even bigger and the community will benefit even more. Honestly, this event was great. Some people got to learn real experience for free, some won prizes, some got recognized, and some simply enjoyed sharing what they knew. I didn’t win anything either, and I still wanted to share :)))
Enough rambling. Let’s get into it. According to the admins, they wanted the contest to be accessible to the community and interesting even for people with less experience, so the project itself was intentionally quite template based. If you want to understand why it can be optimized this aggressively, you can download the source code and try it yourself here: vite-react-template.zip
Analyzing the Winning Dockerfile
First, as I mentioned, I had to analyze the winning Dockerfile to understand it more deeply and explore the ideas behind it.
When I looked at the winning Dockerfile, the first thing that stood out was how well the author understood the problem. They used node:alpine for the build stage, lipanski/docker-static-website to serve the static site, added pre-compression, used a minimal health check, and most importantly, placed every step in the builder stage carefully so Docker caching could be used effectively.
To be honest, if I hadn’t seen this Dockerfile, I probably wouldn’t have thought about pushing the optimization this far. It’s one of those examples where you can immediately tell the author really understands how to build a Vite based static site properly.
What Did I Learn from the Winning Dockerfile?
There were three things I found especially valuable:
- A well separated build stage and runtime stage: Build with Node Alpine, which is lightweight while still providing the full toolchain. Then switch to an extremely small static server for runtime.
- Pre-compress all output with
gzip -9: This is genuinely valuable. Vite produces quite a lot of.js/.cssfiles, andgzip -9can reduce their size significantly. - A sensible base image for static websites:
lipanski/docker-static-websiteis stripped down as far as possible. It provides a runtime environment of less than 100KB while still running reliably.
Those three things make the winning Dockerfile a very solid configuration that is hard to criticize.
Building a Docker Image of Just 143KB
But I still had one question:
“What if I remove the base image entirely, build the server myself, and run everything completely from scratch? How small could it get?”
The lipanski/docker-static-website base image is already extremely lightweight, but it is still a prebuilt image. I wanted to see what would happen if I wrote my own HTTP server in C, compiled it fully statically, stripped it, compressed it aggressively with UPX, and then ran everything on FROM scratch.
Could that structure go even smaller?
That’s why I chose this approach:
- No base image.
- Runtime completely from scratch.
- A single binary to serve files.
- All Vite static assets compressed while keeping their original filenames, with the server returning the
Content-Encoding: gzipheader.
Why Write an HTTP Server in C?
This is the main reason the image became so small that even I was surprised.
BusyBox httpd is already tiny. But what if I build an HTTP server that:
- only supports GET
- does not support directory listing
- does not support range requests
- does not handle multithreading
- does not support a full set of MIME types
- has no runtime flags
- only reads gzip files and pushes them directly to the socket
Then the final binary can be reduced to just a few dozen KB after:
- using
gcc -Osto optimize for size - using
strip --strip-allto remove symbols - using
upx --lzmato compress the binary as much as possible
This is basically a zero dependency approach.
And yes, it really does get smaller.
Trimming Down dist
The way I processed dist in my Dockerfile was completely inspired by the original author:
- Remove sourcemaps
- Remove images, fonts, and media files if they are not needed at runtime
- Remove
manifest.jsonand favicon - Keep only
index.htmland the.jsbundle - gzip every file in place using
gzip -cso the filename does not get renamed to.gz
I only changed one thing: my server always sends Content-Encoding: gzip, so I only need to keep the compressed files. There is no need to keep the raw versions as well, which saves a decent amount of space.
The Biggest Difference: Runtime from Scratch
In my Dockerfile, the runtime contains only:
- One C binary of a few dozen KB
- One gzip compressed
distdirectory - No shell
- No OS
- No dynamically linked libc because everything is fully static
- No TLS
- No dependency other than the kernel
- As a result, the image is reduced about as far as it can reasonably go
Dockerfile That Builds a Docker Image of Just 143KB
Here is the complete Dockerfile I wrote:
# syntax=docker/dockerfile:1.7
############################
# Stage 1: build tiny static HTTP server
############################
FROM alpine:3.22.2 AS http-server-builder
WORKDIR /src
RUN apk add --no-cache build-base upx
# Write and build a C HTTP server that serves files from ./ on port 3000
RUN <<EOF
set -e
cat > tiny_httpd.c <<'SRC'
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static void die(const char *msg) {
perror(msg);
exit(1);
}
static const char *guess_type(const char *path) {
size_t len = strlen(path);
if (len >= 5 && strcmp(path + len - 5, ".html") == 0) return "text/html; charset=utf-8";
if (len >= 4 && strcmp(path + len - 4, ".css") == 0) return "text/css; charset=utf-8";
if (len >= 3 && strcmp(path + len - 3, ".js") == 0) return "application/javascript; charset=utf-8";
if (len >= 5 && strcmp(path + len - 5, ".json") == 0) return "application/json; charset=utf-8";
if (len >= 4 && strcmp(path + len - 4, ".svg") == 0) return "image/svg+xml";
if (len >= 4 && strcmp(path + len - 4, ".txt") == 0) return "text/plain; charset=utf-8";
return "application/octet-stream";
}
static void send_simple(int client, int code, const char *reason, const char *body) {
char buf[512];
size_t body_len = strlen(body);
int n = snprintf(buf, sizeof(buf),
"HTTP/1.1 %d %srn"
"Content-Type: text/plain; charset=utf-8rn"
"Content-Length: %zurn"
"Connection: closern"
"rn"
"%s",
code, reason, body_len, body);
send(client, buf, n, 0);
}
static void handle_client(int client) {
char req[2048];
ssize_t r = read(client, req, sizeof(req) - 1);
if (r <= 0) {
close(client);
return;
}
req[r] = '