Before this site had any real design on it, it couldn't even stay running.
The crash loop
The first version ran astro dev straight inside a Docker container, bind-mounted to the host so I could edit files live. That worked right up until the container needed to restart — at which point it would get stuck in a loop: Astro's dev server writes a lockfile to track its own process, but because the project folder was bind-mounted, that lockfile survived every restart. Every time the container came back up, the new process saw the old lockfile and refused to start, thinking another instance was already running.
Clearing .astro and node_modules/.astro fixed it in the moment, but it kept coming back — any time a file changed and Astro tried to hot-restart itself internally, it could land in the same race condition. Sometimes it recovered after a few minutes. Sometimes it didn't.
The actual fix: stop running a dev server at all
The real problem wasn't the lockfile — it was using a live dev server as if it were a production deployment. The fix was to build once and serve static files:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
No watch process, no lockfiles, nothing to race against itself. Nginx either serves the files or it doesn't start — there's no in-between state where it's stuck arguing with a previous version of itself.
Wiring it to the domain
With the static build in place, the rest was routine: Cloudflare DNS pointed afcomp.com at the server, Nginx Proxy Manager handled the reverse proxy and TLS cert, and the router forwarded 80/443 to the box — the exact same pattern already working for the photo backup service.
The only snag: once the site moved off port 4321 (the old dev server port) to a container with no host port published, Nginx Proxy Manager couldn't reach it directly across Docker's internal networking. Publishing the new container on a host port and pointing NPM at that, same as everything else, sorted it out.
Lesson learned
If a container needs to restart itself to pick up changes, that's a sign it's still in "development" mode — even in production. Build artifacts don't need to restart; they just sit there and get served.