Making a Crouton Server
Published 2025-4-18
This is a small project that packages a Flask app in Docker and deploys it using Docker Compose. It runs a server that returns "crouton!"
, the current time, and a visitor count that persists across restarts.
- The codebase: https://github.com/rodlaf/crouton
- You can try it here: https://crouton.rlafuente.com
- The idea was originally inspired by https://crouton.net
The app itself is trivial. But what you get, by building and deploying it properly, is a full walkthrough of how web services actually make their way from a script on your laptop to something versioned, persistent, and runnable everywhere.
It touches on containerization, orchestration, logging, port forwarding, file persistence, and shipping artifacts to Docker Hub. All in about 30 lines of Python.
The Setup
The full codebase is:
- server.py
- Dockerfile
- docker-compose.yml
- README.md
The app lives in server.py
. It reads from and writes to a counter in count.txt
. That file is stored in a Docker volume. The server itself is served with Gunicorn, and the container is run and managed with Compose.
The point isn’t just to make a working app — it’s to make a self-contained service, with all the pieces described explicitly and no hidden behavior.
Why Docker
A lot of programming advice stops at "write code that works." But once you have something that runs, the question becomes: how do you package it? How do you isolate its environment and copy it around? How do you write down, in code, all the stuff you normally forget to mention?
Docker solves this by letting you write a small spec — a Dockerfile
— that tells it how to build an environment and run your code. It gives you repeatability without guesswork.
In practice, it means what you run locally is the same as what you push to production, or what someone else runs with no setup. If your container works, that’s the whole story.
The Compose file is the next step. It wires up volumes, ports, restarts, and other runtime concerns. Taken together, these two files are your deployable.
The App
Here’s the only meaningful code:
@app.route("/")
def index():
count = get_count()
now = datetime.now().strftime("%H:%M:%S")
print(f"{datetime.now()} - Request from {request.remote_addr} | Visitors: {count}")
return f"crouton! {now} | Visitors: {count}"
The server tracks visits in a count.txt
file. It locks that file to avoid race conditions and updates it atomically. The file lives in a Docker volume, which gets mounted inside the container. That’s the key part: stateful behavior without binding to the host filesystem.
Logs are just print()
calls. With --capture-output
, Gunicorn forwards those to stdout
, and you can view them with docker logs
.
Dockerfile
FROM python:3-slim
WORKDIR /app
COPY server.py .
RUN mkdir -p data
RUN pip install flask gunicorn
CMD ["gunicorn", "-w", "8", "-b", "0.0.0.0:80", "--capture-output", "server:app"]
It installs Python, sets up the directory structure, installs Flask and Gunicorn, and runs the app using 8 worker processes. Simple, self-contained, and about as light as it gets.
docker-compose.yml
services:
crouton:
image: rodlaf/crouton:1.8
container_name: crouton-server
build: .
ports:
- "1234:80"
volumes:
- data:/app/data
restart: unless-stopped
volumes:
data:
Compose handles port binding (localhost:1234 → container:80
), volume mounting (data/count.txt
persists across runs), and restart policy (unless-stopped
, so it stays up unless you bring it down). There's no need to bind-mount any source code, since the image includes everything it needs.
Running It
docker compose up --build -d
This builds the image, starts the container, creates the volume, maps the port, and detaches. Now visit:
http://localhost:1234
Refresh a few times. The number goes up. The logs are in:
docker logs crouton-server
You’ll see visit logs with timestamps and IPs.
The state is real. If you kill and restart the container, the count continues. If you delete the volume, it resets.
Publishing the Image
Once built, the image can be published to Docker Hub:
docker tag crouton rodlaf/crouton:1.8
docker push rodlaf/crouton:1.8
Now it's globally runnable:
docker run -p 1234:80 rodlaf/crouton:1.8
No setup, no clone, no Python environment — just Docker.
What You Get From This
This isn’t about Flask. It’s about the surrounding machinery.
By building a deployment pipeline around a toy app, you get a working example of real-world practices:
- Define environments with a
Dockerfile
- Persist state using Docker volumes
- Forward logs and inspect them
- Map host ports to container ports
- Use Compose for orchestration
- Push images to a registry for reuse
All of these are foundational for modern devops work, but none require Kubernetes, cloud vendor lock-in, or YAML soup to understand. The mechanics are the same whether you're running Flask or FastAPI or a Go backend — it’s all about the shape of the system.
Try It
docker compose up --build -d
Then open http://localhost:1234.
Check logs with docker logs crouton-server
.
To reset the counter:
docker compose down -v
Live version: https://crouton.rlafuente.com