35 comments

[ 5.7 ms ] story [ 66.6 ms ] thread
This just seems like several unnecessary layers of complexity to me.

For making a static site that you're personally deploying, exactly why is Docker required? And if the Docker process will have to bring in an entire Linux image anyway, why is obtaining Python separately better than using a Python provided by the image? And given that we've created an entire isolated container with an explicitly installed Python, why is running a Python script via `uv` better than running it via `python`? Why are we also setting up a virtual environment if we have this container already?

Since we're already making a `pyproject.toml` so that uv knows what the dependencies are, we could just as well make a wheel, use e.g. pipx to install it locally (no container required) and run the program's own entry point. (Or use someone else's SSG, permanently installed the same way. Which is what I'm doing.)

Author—though not OP—here. I’ll try to broadly address the questions, which are all fair.

Broadly speaking, I explicitly wanted to stay in the Coolify world. Coolify is a self-hostable PaaS platform—though I use the Cloud service, as I mentioned—and I really like the abstraction it provides. I haven’t had to SSH into my server for anything since I set it up—I just add repos through the web UI and things deploy and show up in my browser.

Yes, static sites certainly could—and arguably even should—be done way simpler than this. But I have other things I want to deploy on the same infrastructure, things that aren’t static sites, and for which containers make a whole lot more sense. Simplicity can be “each thing is simple in isolation”, but it can also be “all things are consistent with each other”, and in this case I chose the latter.

If this standardization on this kind of abstraction weren’t a priority, this would indeed be a pretty inefficient way of doing this. In fact, I arrived at my current setup by doing what you suggested—setting up a server without containers, building sites directly on it, and serving them from a single reverse proxy instance—and the amount of automation I found myself writing was a bit tedious. The final nail in the coffin for that approach was realizing I’d have to solve web apps with multiple processes in some other way regardless.

Simple answer is to package it up with all its system dependencies ans not worry about anything.
(comment deleted)
Looking forward to the follow up:

Static sites with HTML, CSS, Apache and Linux.

My guess is that when you are self taught and don't know what the hierarchy of technologies looks like, you can learn several advanced technologies without knowing the basic technologies that they are built upon and the challenges the basic tech can't solve.

So you just solve all problems with advanced tools, no matter how simple. You get into tech by learning how to use a chainsaw because it's so powerful and you wanted to cut a tree, now you need to cut some butter for a toast? Chainsaw!

> why is obtaining Python separately better than using a Python provided by the image?

I mostly work in a different domain than webdev, but feel strongly about trying to decouple base technologies of your OS and your application as much as possible.

It's one thing if you are using a Linux image and choose to grab their Python package and other if their boot system is built around the specific version of Python that ships with the OS. The goal being if you later need to update Python or the OS they're not tethered together.

At my work we use docker for everything now. Makes no sense. Literally we have a dedicated server for each application. Instead of copying python files to the server in 5 seconds we take 30 minutes to build a docker container, copy to repo, scan it, deploy.
I created a static site generator for my wife's business using go + js + git. She has no tech skills whatsoever, but had very specific ideas about how the website should work.

Once the basic functionality was worked out in js (with a simple JSON schema and Lourm Ipsum text), I created a Samba share on the network, mapped it to her computer.

Her first post was an edit of the Lorum Ipsum markdown and some new images, and from then on she had a pattern to follow: add a directory with a markdown file and assets.

The NAS runs a Go program that generates a JSON file for the static site js, starts a http server for testing. She can access the http server over the network to preview the static site. If it's ok, she can trigger a git commit and push (with a click). She didn't have to learn anything (like Hugo or jinja), because markdown is kinda obvious. She has exactly the website she wants.

Of course, I had more work to do initially, coming up with the static site, the JSON schema and the go generator, but it was over a week of evenings. She's been happily adding to it regularly without my interaction.

And this, my friends, is why I (a mere DevOps / Cloud guy) "vibe code" with Claude.

The best way to make static sites is to install nginx/caddy or whatever basic static webserver from your repos. Then put the .html and files in directories on your filesystem in the web root folder. Done. No overhead, no attack surface, no problems with software changing (deps, etc, etc), lasts forever. Super easy interface (it's your filesystem!).

This project seems more like something you'd do to demonstrate your skills with all these tools that do have use in a business/for-profit context working with groups but they have absolutely no use or place hosting a personal static website. Unless you're doing it for kicks and enjoy useless complexity. That's fair. No accounting for taste in recreation.

At this point, why not use a wordpress container? With a minimalist theme, it would be way easier and faster to deploy, and still be blazingly responsive.

This level of complexity would've been acceptable if this was about deploying one's own netlify type of service for personal use. Otherwise, it's just way too complicated.

I'm currently working on a Django app, complete with a database, a caching layer, a reverse-proxy, a separate API service, etc. and still much simpler to deploy than this.

it's true, wordpress is self contained - it does bundle in a lot of overhead (database, php) - but it has a good story for quickly getting a presentable site. Static sites require learning about template engines, git (possibly), FTP...
I think the author would do well to front load "the why". Seems very over the top, sometimes you want to do things just because. Totally valid, but helps contextual the blog.
This is a snapshot of what's gone wrong in acutely, web development culture, and broadly, software development culture over the past few decades. Complexity, provincialization, and discarding improvements in computing hardware.
Why don't you just upload the HTML/CSS/JS files to a folder and point Apache or Nginx to that folder?
this is satire right?
And I deploy that using Ansible! Well, in my case a truly static HTML file and a bunch of CSS files. But yes, Caddy is great for serving static pages. If you have set it up once, you can apply the whole thing as one setup (playbook).
Adding fuel to the fire of "this is over engineering" but this is overkill right?! I'm not in the web development field but my own site is just deployed with Emacs (specifically HTML generated from org-mode).
I don't really have an opinion on using caddy in a container to serve a static site. That's fine, really.. However, the way the container is built is done in the worst possible way:

  # copy all files
  COPY . .

  # install Python with uv
  RUN uv python install 3.13

  # run build process
  RUN uv run --no-dev sus
This adds the entire repository to the first layer, then installs python, then runs the build which I assume will only then install the dependencies. This means that changing any file in the repository invalidates the first layer, triggering uv reinstalling python and all the dependencies again. The correct Dockerfile would be something like

  # install Python with uv
  RUN uv python install 3.13

  # copy info for dependencies
  COPY pyproject.toml uv.lock .

  # Install dependencies
  RUN uv whatever

  # Copy over everything else
  COPY . .

  # run build process
  RUN uv run --no-dev sus
I see the Docker stuff here but Kubernetes is missing. Lacking required complexity for a simple _static_ personal blog.

Also while using Kubernetes, please use event-driven PubSub mechanisms to serve files for added state-of-the-art points.

/pun

Is this satire?

html file -> ftp -> WWW html file -> mv /var/www/public -> WWW

Possibly SSG -> html -> etc.

It's... a static site. Generate the output (use docker if you want, doesn't really matter), and just dump the result to a directory on an ordinary server.
My current personal site (https://knlb.dev) is built with a single 500 line python file that starts with

  #!/usr/bin/env -S uv run --script
  # -*- mode: python -*-
  #
  # /// script
  # requires-python = ">=3.12"
  # dependencies = [
  #    "pyyaml", "flask", "markdown-it-py",
  #    "linkify-it-py", "mdit-py-plugins"
  # ]
  # ///
The HTML templates & CSS are baked into the file which is why it's so long. flask so that I can have a live view locally while writing new notes.

uv's easy dependency definition really made it much easier to manage these. My previous site was org exported to html and took much more effort.

(With the conceit that the website is a "notebook" I call this file "bind").

Almost every other comment in this thread is people complaining this is too complex and over-engineered.

I had the opposite reaction when I read this post: I thought it was a very neat, clean and effective way to solve this particular problem - one that took advantage of an excellent stack of software - Caddy, Docker, uv, Plausible, Coolify - and used them all to their advantage.

Ignoring caching (which it sounds like the author is going to fix anyway, see their other comments) this is an excellent Dockerfile!

  FROM ghcr.io/astral-sh/uv:debian AS build
  WORKDIR /src
  COPY . .
  RUN uv python install 3.13
  RUN uv run --no-dev sus
  
  FROM caddy:alpine
  COPY Caddyfile /etc/caddy/Caddyfile
  COPY --from=build /src/output /srv/
8 lines is all it takes. Nice. And the author then did us the favor of writing up a detailed explanation of every one of them. I learned a few useful new trick from this, particularly around using Caddy with Plausible.

This one didn't strike me as over-engineering: I saw it as someone who has thought extremely carefully about their stack, figured out a lightweight pattern that uses each of the tools in that stack as effectively as possible and then documented their setup in the perfect amount of detail.

People are complaining because

  make && make deploy
where the default target is simply `uv run --no-dev sus` and the deploy target is simply `rsync -avz --delete ./dist/ host:/path/to/site/` is hell a lot more neat, clean, effective, and lightweight? (And if you care about atomic deployment it's just another command in the deploy target.)

I have ~60 static websites deployed on a single small machine at zero marginal cost. I use nginx but I can use caddy just the same. With this "lightweight pattern" I'd be running 60 and counting docker containers for no reason.

My minor suggestion would be to not to use `COPY . .` as it could slow down the build process if it has to copy everything in the context that's not needed. Also a potential privacy/sec risk if private/secret data is copied the final docker image, but probably not applicable in a multi-stage builds where it's in an aux stage.

If you don't want to have multiple `COPY`s, you can add a `.dockerignore` file (https://docs.docker.com/build/concepts/context/#dockerignore...) with the `COPY . .` directive and effectively configure an allowlist of paths, e.g.,

  *
  !src/
  !requirements.txt
Imagine Caddy had a built-in feature to manage redirects with a very simple syntax. Maybe it could be called redir? That'd help removing the whole "run python to compile a bunch of HTML files" part of this.

Oh wait …

Maybe I'm just venting but you can probably eject Docker from this setup. If your dependencies are pretty well-defined then you don't really need a base image; just install Caddy and let uv handle the Python dependencies. Among us on Hacker News you'd think that there's a bunch of Docker hate but I really think that you don't need it if your dependencies are really simple and you don't need a specific version of things.
You won't have to sit and do any of this manually once my production grade python application generator goes live. At the moment it works with poetry and i am working on a uv version of it. Let me explain what the poetry one does

- Setup a docker container - Install python 3.x (specified by you) - Install poetry x.y (specified by you) - Setup README - Setup gitignore - Install black, isort, flake8, tox, pre-commit, mypy, commitizen, darglint, xdoctest, pytest with src and tests directory with a lot of inputs from you with pinned dependencies - Add nginx / caddy / traefik with Github Actions CI / CD and templates for pull requests, issues, feature requests, questions etc - Verify everything works by running all commands in docker - Give you a downloadable version of this production grade python application - As dependencies update, this whole pipeline updates immediately

Is this container containing the website inside a Linux OS running inside an outer docker container that is contained in the coolify container inside a web app in the cloud or is it just shipped in the container?
@author: I'm disappointed by all the negativity in the comments.

People are mocking you without even trying to understand what you did, why, and also the actual work that went in writing the article.

That's not everyday that someone takes the time to explain every layer of their Dockerfile.

Even if I would have went a different way, I found it interesting and it also forced me to dig deeper in uv, which I wrongly assumed I understood.

Thank you for writing it and please don't make the bad comments have any significant impact on you (maybe just put a big disclaimer in your intro next time so they'll find someone else to pick on)