Adding a Website

To add a website to Joes Home Server we need to get it ready to use Github Actions as also be served via Docker.

1. Create the site in your Home Server Repo

Ok if your website is called "mywebsite.co.uk" then it's Docker naming convention will be "mywebsite-co-uk" so create a folder in the "stacks/10-sites/" folder called "mywebsite-co-uk" and inside that create a file called "docker-compose.yml" with the following content:

stacks/10-sites/mywebsite-co-uk/docker-compose.yml

services:
  mywebsite-co-uk:
    image: ghcr.io/yourgituser/mywebsite-co-uk:prod
    container_name: mywebsite-co-uk
    restart: unless-stopped
    env_file:
      - stack.env
    networks:
      - edge
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
networks:
  edge:
    external: true

2. Also create a stack.env file in the same folder with the following content:

stacks/10-sites/mywebsite-co-uk/stack.env

# Example Private Vars - Server only e.g. in the /pages/api/ folder etc
NODE_ENV=production # Just an example, optional if you want

Note: NEXT_PUBLIC_* Vars

These are a bit annoying because we need to "bake" them into our build process during the Github actions build step. So we need to add them all to 3 places.

  1. In .github/workflows/prod.yml file (see step 3)
  2. To the Github Repo Secrets for the website repo (see step 4)
  3. In the Dockerfile as build args and ENV vars (see step 5)

3. Create the Github Actions Workflow file

Now in your Home Server Repo create a new file in ".github/workflows/" called "prod.yml" with the following content:

.github/workflows/prod.yml

name: Build + Push Docker Image (prod)
on:
  push:
    branches: ["prod"]
  workflow_dispatch: {}
permissions:
  contents: read
  packages: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Log in to GHCR using GITHUB_TOKEN
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: |
            ghcr.io/yourgituser/mywebsite-co-uk:prod
            ghcr.io/yourgituser/mywebsite-co-uk:${{ github.sha }}
          build-args: |
            NEXT_PUBLIC_TEST=${{ vars.NEXT_PUBLIC_TEST }}
            # Add all your other NEXT_PUBLIC_* vars here too

Note: See the last line this is where all your NEXT_PUBLIC_* vars need to go.

4. Add the NEXT_PUBLIC_* variables to your Website Repo Secrets or Vars

In Github, go to your Repository for your website and go to "Settings" > "Secrets and Variables" > "Actions" and add all your NEXT_PUBLIC_* variables can be added to either the Variables or the Secrets tab.

If they are NEXT_PUBLIC_* they can be in either, but generally if they are public you may as well add them to Variables, if they are private then add them to Secrets.

Github Secrets and Vars

5. Add a Dockerfile to your Repo and add its NEXT_PUBLIC_* variables

Add this Docker file to your websites root, and update the NEXT_PUBLIC_* variables as needed.

Look for the ARG NEXT_PUBLIC_TEST and the ENV NEXT_PUBLIC_TEST=$NEXT_PUBLIC_TEST sections below

Dockerfile

# ---- deps ----
FROM node:20-alpine AS deps
WORKDIR /app

# If you use sharp (Next image optimization), this helps on alpine
RUN apk add --no-cache libc6-compat

COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./

# Choose the right install based on lockfile
RUN if [ -f package-lock.json ]; then npm ci; \
    elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
    elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --frozen-lockfile; \
    else echo "No lockfile found" && exit 1; fi

# ---- build ----
FROM node:20-alpine AS builder
WORKDIR /app
ENV NODE_ENV=production

# Decalre build args for NEXT_PUBLIC_* vars
ARG NEXT_PUBLIC_TEST

# Set ENV for NEXT_PUBLIC_* vars
ENV NEXT_PUBLIC_TEST=$NEXT_PUBLIC_TEST

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build Next.js
RUN npm run build || yarn build || (corepack enable && pnpm build)

# ---- runner ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Recommended for Next standalone output
ENV PORT=3000
EXPOSE 3000

# Create non-root user
RUN addgroup -S nextjs && adduser -S nextjs -G nextjs

# If you enable output: 'standalone', Next generates .next/standalone
# We'll try to use it; if you don't have it yet, see step 1.2.
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/standalone ./

USER nextjs
CMD ["node", "server.js"]

6. Set output: 'standalone' in next.config.js

In your next.config.js file add the following:

next.config.js

const nextConfig = {
  output: 'standalone',
};

This is so that when Next builds it creates a standalone version of the app that can be run in a Docker container.

7. Create a branch called "prod" and push your code

We have defined our Github Actions workflow to run when code is pushed to the "prod" branch, so create that branch and push your code to it.

This will mean that when you or any of your team push code to the "prod" branch it will automatically build and push a new Docker image to Github Container Registry, and then our Linux home server will automatically pull the new image and restart the container.

8. Once pushed to prod - open up Github Actions and make sure the build runs successfully

It might take 2 to 3 minutes to complete but you should see a new workflow run appear in the Actions tab for your repo and build successfully.

Github Actions

9. Deploy the Website on your Home Server

Now on your Home Server run the following commands to start the new website container:

cd ~/Develop/ubuntu-home-server/stacks/10-sites/mywebsite-co-uk
docker compose up -d
docker logs mywebsite-co-uk --tail 50 -f

Your website should now be running on your Home Server. But next we need to set up Nginx Proxy Manager to route traffic to it through cloudflare.

10. Nginx Proxy Manager (internal routing)

Create a new Proxy Host:

  • Domain Names: mywebsite.co.uk (and any other domains you want to add)
  • Scheme: http
  • Forward Hostname / IP: mywebsite-co-uk
  • Forward Port: 3000

Optional internal test:
docker exec -it nginx-proxy-manager sh -lc "curl -I http://mywebsite-co-uk:3000 | head"

Nginx Proxy Manager

11. CloudFlare Tunnel

Add a new Ingress Rule to your CloudFlare Tunnel for the new domain pointing to your Nginx Proxy Manager instance.

CloudFlare --> Zero Trust --> Network --> Connectors --> Create Tunnel or select your existing Tunnel and click Edit

CloudFlare Tunnel

CloudFlare Tunnel

CloudFlare Tunnel

Click on "Published Application Routes" tab, and then the "+ Add a published application route" button and add the following:

CloudFlare Tunnel

  • Subdomain: www
  • Hostname: mywebsite.co.uk
  • Service Type: http
  • Service URL: nginx-proxy-manager:80
  • Then test: `https://www.mywebsite.co.uk

12. Then test your URL

I know it's a lot of steps, but essentially what we are doing is similar to what Vercel or Amplify are doing under the hood.

If your website is up and running then you now have a completely free website running from Joes Local Server!

13. Push a new change to "prod" branch to test the full CI/CD pipeline

When you push to prod, Github Actions should build and push a new Docker image, then your Home Server should pull the new image and restart the container automatically using the WatchTower docker container. WatchTower checks for new images every 60 seconds by default.

Was this page helpful?