This blog entry describes how I self-hosted this portfolio on my own hardware.
Note: This post only covers the hosting aspect, not the portfolio’s codebase.
What am I actually running?
I'm running a Qwik webpage, routed through a Cloudflare Tunnel.
It’s nothing too complex. There are no databases involved, as I currently have no need for them—though I might add one later.
How to do it yourself
Setting up the project
Let’s take a look at how I got this website up and running.
First, we need to create a Qwik project. I won’t go into too much detail here, since I’ll likely cover it in a future post.
Just in case you’re unfamiliar: Qwik is a TypeScript framework with the unique selling point of serving as little JavaScript as possible to the client.
To start a new Qwik project, run:
npm create qwik@latest
or you adapt the command to your project needs.
This will run you through the process of setting up the project. Once you are through, you need to initialize the site by running i on your package manager, in my case:
pnpm i
If this completes without errors, you can start the development server:
pnpm dev
and your page is up and running on localhost:5173.
Let's go live
To make your website publicly accessible, we need to prepare it for production.
We'll run it inside Docker, which means we'll need two files: a Dockerfile
and a docker-compose.yml
.
Both should be placed in the root of your project (not in a subfolder).
Dockerfile docker-compose.yml
Once you created them, we need to fill them with the actual instructions. Let's start with the Dockerfile since it will be running our project, quite similar to how we ran "pnpm i" and "pnpm dev". Before that you will probably need to add express using:
qwik add
and selecting the right menu point.
# ---------- Intermediate Build Stage ----------
FROM node:20-alpine AS build
WORKDIR /usr/src/app
# Install pnpm
RUN npm install -g pnpm
# Copy lock files
COPY ./package.json ./
COPY ./pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install
# Copy the full source
COPY ./ ./
# Build the app
RUN pnpm run build
# ---------- Production Stage ----------
FROM node:20-alpine AS production
WORKDIR /usr/src/app
RUN npm install -g pnpm
# Copy only what is needed
COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/server ./server
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/src/app/package.json ./package.json
# Expose the default port
EXPOSE 3000
# Start the Qwik app (adjust if your actual entry point differs)
CMD ["node", "server/entry.express.js"]
Now let's go through what this Dockerfile does. In a nutshell this will move any needed files to a new folder called app which will run instead of the hole project. It then gets build and exposed at port 3000 which is importend for the docker-compose file which we will look at next.
version: '3.8'
services:
qwik-portfolio:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
networks:
cloudflareTunnel:
ipv4_address: 172.18.0.12
volumes:
- ./:/app
networks:
cloudflareTunnel:
external: true
This tells Docker to build and run the container on port 3000.
It also connects it to a custom Docker network named cloudflareTunnel
, assigning it a fixed IP (172.18.0.12
).
This ensures the container always gets the same IP, even after restarts.
To build and run the container in the background:
docker compose up -d --build
Connecting to the internet
At this point, the container is running—but it’s not accessible from the internet yet.
We’ll fix that by setting up a tunnel with Cloudflare.
- Go to your Cloudflare dashboard.
- Navigate to Zero Trust > Networks > Tunnels.
- Create a new tunnel and follow the instructions.
- You’ll be asked to run a command that sets up another container on your system.
- Make sure this tunnel container is also on the
cloudflareTunnel
Docker network.
Once done, map the container IP and port (172.18.0.12:3000
) to your domain in Cloudflare.
Your site should now be live!
I'll extend this guide in the future to include automatic updates from GitHub.
If you have any questions, feel free to reach out at: [email protected]