step-by-step to build a project (code + config)

here’s a copyable starter project you can build end to end.

It gives you:

  • a tiny Node app in Docker
  • Traefik in front of it
  • hostname-based routing on localhost
  • a GitHub Actions workflow that builds the image
  • a path to deploy the same stack to a server later

This matches Traefik’s current Docker provider pattern, where Traefik watches Docker and picks up routing config from container labels. (Traefik Labs Documentation)


Project structure

devops-starter/
├── app/
│ ├── package.json
│ └── server.js
├── .github/
│ └── workflows/
│ └── docker.yml
├── .dockerignore
├── Dockerfile
└── compose.yml

1) app/package.json

{
"name": "devops-starter",
"version": "1.0.0",
"description": "Simple Node app behind Traefik",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"license": "MIT"
}

2) app/server.js

const http = require("http");
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
const body = {
ok: true,
message: "Hello from the app behind Traefik",
method: req.method,
url: req.url,
hostname: req.headers.host,
time: new Date().toISOString()
};
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(body, null, 2));
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

3) Dockerfile

This uses an official Node image and a fixed major version tag, which is in line with GitHub’s Dockerfile guidance. (GitHub Docs)

FROM node:20-alpine
WORKDIR /app
COPY app/package.json ./
RUN npm install --omit=dev
COPY app/server.js ./
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

4) .dockerignore

node_modules
npm-debug.log
.git
.github
Dockerfile
compose.yml

5) compose.yml

This follows the same core idea as Traefik’s Docker Compose examples: enable the Docker provider, disable exposing containers by default, define an HTTP entrypoint, and add labels to the app container so Traefik creates the router automatically. (Traefik Labs Documentation)

services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
ports:
- "80:80"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
app:
build:
context: .
dockerfile: Dockerfile
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.localhost`)"
- "traefik.http.routers.app.entrypoints=web"

A couple of notes:

  • api.insecure=true is fine for learning locally, but not for a public server. Traefik’s dashboard docs treat this as something to secure for real deployments. (Traefik Labs Documentation)
  • Because both services are in the same Compose stack, Docker networking handles connectivity between Traefik and the app. That is the same pattern used in Docker and Traefik quick-start examples. (Traefik Labs Documentation)

6) .github/workflows/docker.yml

GitHub’s docs show Docker builds in Actions using actions/checkout and docker/build-push-action. This workflow keeps it simple: it builds on every push to main, and you can later extend it to push to Docker Hub or GHCR. (GitHub Docs)

name: Build Docker image
on:
push:
branches: ["main"]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: false
tags: devops-starter:latest

7) Run it locally

From the project root:

docker compose up -d --build

Then open:

  • http://app.localhost
  • http://localhost:8080 for the Traefik dashboard

Traefik’s Docker quick-start uses the same localhost-style host rule pattern, and the dashboard is commonly exposed on port 8080 in the getting-started setup. (Traefik Labs Documentation)

To stop:

docker compose down

To view logs:

docker compose logs -f

8) What’s happening

When you visit http://app.localhost:

  1. your browser sends a request to port 80
  2. Traefik receives it
  3. Traefik checks Docker-discovered labels
  4. the router rule Host(\app.localhost`)` matches
  5. Traefik forwards the request to the app container

That “dynamic config from Docker labels” model is a central part of Traefik’s configuration overview and Docker provider docs. (Traefik Labs Documentation)


9) Make it feel more real

Add a second app to prove routing works.

Update compose.yml like this:

services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
ports:
- "80:80"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
app:
build:
context: .
dockerfile: Dockerfile
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.localhost`)"
- "traefik.http.routers.app.entrypoints=web"
whoami:
image: traefik/whoami
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
- "traefik.http.routers.whoami.entrypoints=web"

Then:

  • http://app.localhost → your app
  • http://whoami.localhost → sample Traefik test service

That mirrors Traefik’s own examples for exposing services with Docker labels. (Traefik Labs Documentation)


10) How to deploy this later

For a simple first deployment:

  • get a Linux VM
  • install Docker and Docker Compose
  • copy this project to the server
  • point a domain at the server IP
  • swap localhost routing for your real domain
  • add HTTPS with Traefik + Let’s Encrypt

Traefik documents Docker standalone setup, HTTPS entrypoints, and ACME/Let’s Encrypt support as part of its normal production path. (Traefik Labs Documentation)

Your production router label would look more like:

- "traefik.http.routers.app.rule=Host(`app.yourdomain.com`)"

11) Resume-worthy version of this project

Once this is live, you can honestly describe it like this:

Built and deployed a containerized Node.js service using Docker and Traefik with hostname-based routing and automated image builds via GitHub Actions.

That is a real DevOps project, not tutorial-only practice.


12) Best next upgrades

After this works, do these in order:

  1. add /healthz endpoint
  2. add a test job to GitHub Actions
  3. push built images to GHCR or Docker Hub
  4. deploy on a small cloud VM
  5. add HTTPS with Let’s Encrypt
  6. add Prometheus/Grafana later

GitHub’s Actions docs already provide the build-and-publish direction if you want to turn your build-only workflow into a registry-pushing workflow. (GitHub Docs)


13) The shortest possible checklist

Create files → run:

docker compose up -d --build

Visit:

http://app.localhost
http://localhost:8080

Push to GitHub → Actions builds the image definition automatically.


Leave a comment