Project case study / Self-hosted infrastructure
Hosting my own website.
From Astro to a home server.
The website you’re reading is served from a repurposed ASUS laptop at home. Building it gave me a reason to connect application development, containers, networking, and deployment into one working system.
- Application
- Astro · static site
- Origin server
- ASUS · Debian
- Public access
- Cloudflare Tunnel
- Deployment
- Git + Docker Compose
01 / A portfolio that also teaches me infrastructure
I wanted a place to document my engineering work, but hosting it was an opportunity to learn too. I already had a Debian server running on an old ASUS VivoBook for my Immich family photo cloud. Hosting my website on the same machine made practical use of hardware I owned.
My reasons were a combination of learning, control, reuse, and cost. I wanted to understand the path from a source-code change to a page someone could open on the internet. Running the origin myself also avoids paying for a separate hosting server, although the domain, electricity, internet connection, and maintenance still have costs.
The result is a small deployment I can explain end to end: Astro generates the website, Docker packages it, Caddy serves it, and Cloudflare Tunnel connects it to the public domain.
02 / From a browser to the laptop at home
A visitor opens atulchavan.com over HTTPS. The public endpoint is Cloudflare, which forwards requests through the tunnel to my website’s origin. On the ASUS server, Docker publishes port 8080 to port 80 inside the Caddy container.
01 / How a visitor reaches the site
Origin: ASUS VivoBook · Debian · Docker
Domain registration, DNS, and serving the site are separate jobs
I manage the domain through GoDaddy, while its current authoritative nameservers are Cloudflare’s. That distinction matters: the account used to manage a domain does not necessarily identify the service answering its DNS queries. The public DNS and HTTPS endpoint were checked when writing this case study.
HTTPS outside, HTTP at the container
Cloudflare handles the browser-facing HTTPS connection. The tunnel carries traffic to the origin side, where this Caddy configuration listens on HTTP port 80. Caddy is serving static files here; it is not configured to obtain the public domain’s certificate. Cloudflare’s edge-certificate documentation explains the public TLS layer.
The tunnel connector initiates an outbound connection to Cloudflare, so the website’s public route does not require forwarding a router port to the server. Requests and responses then use that established connection. This is the connection model described in the Cloudflare Tunnel documentation.
Immich and the website share the same physical machine, but the website has its own container and service. The public route described here is for the website; it is not the private Tailscale access path used for the photo library.
03 / Build the application, then serve the result
This portfolio is mostly content: project summaries, case studies, and engineering notes. Astro fits that workload because the pages can be generated ahead of time. A visitor does not need a running Node application to assemble each page.
The Dockerfile has two stages. The first uses Node 24 on Alpine Linux, installs dependencies with npm ci, and runs npm run build. The second starts from Caddy 2 and copies the generated dist/ directory into /srv.
That gives the two stages distinct responsibilities. Node and Astro are build tools. Caddy is the runtime web server, serving the generated HTML, CSS, and assets. The final stage does not copy the source tree or the Node build environment.
The Caddy configuration sets the document root to /srv and enables gzip and Zstandard compression. Docker Compose declares the website service, its port mapping, and an unless-stopped restart policy. Those few files capture the core deployment configuration in the repository.
04 / Automated updates from new commits
New commits pushed to the repository now trigger an automated update flow, carrying changes through the site build and deployment. Git remains the source of truth, while Docker packages the generated website for Caddy to serve.
02 / How a change becomes a release
- Edit locallyAstro pages, components & content
- Commit & pushSource changes reach GitHub
- Update triggeredAutomated build and deployment flow
BUILD STAGE
Node 24 + Astronpm ci → npm run build → dist/RUNTIME STAGE
Caddy 2Generated site at /srv · Node build tools stay behindThe update must rebuild the image to include the changed pages. The equivalent manual server-side steps are:
git pull
docker compose up -d --build siteThe --build flag is essential when the source changes. It runs the build before starting the service from the updated image. Stopping the existing container first is unnecessary for this workflow; I also do not treat a single-container replacement as a zero-downtime deployment.
To check a release, I can inspect the local service and its logs, then open the changed page through the public domain:
curl -I http://localhost:8080/
docker compose logs --tail=50 siteAn HTTP response confirms that the service answered. Checking the actual page content separately tells me whether the intended release is being served.
05 / The first update exposed the important boundary
After adding the Immich case study, I pushed the changes, pulled them on the server, and ran docker compose down followed by docker compose up -d. The website still showed the old content.
Lesson / Source code is not the running image
Restarting a container does not rebuild the website.
The new files were in the server’s repository, but the static HTML was baked into the existing Docker image. Recreating a container from that image served the same old files. The missing step was an image rebuild.
This made the release process easier to reason about. The repository is an input, the image is a built artifact, and the container runs that artifact. A change at one layer does not automatically update the next.
There was also a small portability detail: the repository names its build file dockerfile in lowercase. Compose now names that file explicitly, avoiding reliance on the default Dockerfile spelling when building on Linux.
06 / What I gain, and what I now own
I have a public website running on existing hardware, with a deployment I can inspect and reproduce from the repository. The project connects the code I write to the infrastructure that serves it.
The trade-off is operational responsibility. The website depends on the home server, its power and internet connection, and the tunnel. Sharing a host with Immich also means sharing CPU, memory, and storage resources. Separate containers do not remove that shared dependency.
Automated updates reduce the repeated work of publishing changes. Potential improvements include additional build checks, publishing versioned images, a documented rollback procedure, and external availability monitoring.
The project has achieved what I wanted from it: a useful website, a second workload for the home server, and a much clearer understanding of what happens between pushing code and serving a page.
← Explore the other projects