← Back to Blog

Hosting my own Portfolio

self-hostingcoding

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.


Code

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.

  1. Go to your Cloudflare dashboard.
  2. Navigate to Zero Trust > Networks > Tunnels.
  3. Create a new tunnel and follow the instructions.
  4. You’ll be asked to run a command that sets up another container on your system.
  5. 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]