Backend Notes OCT 5, 2026 • 07 MIN READ

BUILDING A PRIVATE NETWORK
for
MY SERVER

Diagram architecture

Last week, I got to learn how to build a private network for my server with guidance from my mentor. I’m sharing what I learned.
This is my practical walkthrough based on that experience. I hope it helps if you’re setting up a private network of your own.

Pre Required

  • check_circle Docker Compose to run your apps.
  • check_circle Traefik for routing.
  • check_circle Tailscale for private access.
  • check_circle Cloudflare for DNS and protection.

Lets start with Traefik

Let’s give this setup a home. Create a folder called traefik-server this is where we’ll keep the files for our Traefik server.

Inside the folder, create a file named docker-compose.yaml. This is where we’ll describe the services Docker Compose needs to start.

docker-compose.YAML YAML
services:
  traefik:
    image: traefik:v3.7
    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

  whoami:
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
      - "traefik.http.routers.whoami.entrypoints=web"

In your terminal, run docker compose:

docker compose up -d

docker compose up -d starts the services in the background. Docker downloads any missing images, such as traefik:v3.7

docker compose up -d --build also builds images from a Dockerfile before starting the services. Use it when your Compose file includes a service with a build: section and you’ve changed its source or Dockerfile.

For the Traefik setup above, there’s no build: section, so docker compose up -d is the one to use.

docker compose ps

To see list docker compose

Docker compose ps basic

Before we connect everything, we need a domain name. I already bought mine through RumahWeb , so I’m ready for the next step!

Download and Config Tailscale

We’ll use Tailscale to connect to the server privately. First, create a Tailscale account at Tailscale . Already have one? You’re ready to move on to the installation and configuration!

Add new machines: Click menu Network → Machines → Add Device → Select Client device.

Tailscale add device
Tailscale add device

I don’t have a dedicated server PC yet, so I’m using my work MacBook for this setup. Your setup might look different, but the steps are similar.

First, download and install the Tailscale app for macOS . Open it, sign in with the account you just created, and follow the prompts to connect your MacBook to your Tailscale network. Once it shows as connected, your MacBook is on the team.

Tailscale device server

Now let’s connect a second device. I’m using my smartphone, but you can use another computer or tablet.

Download the Tailscale app on your phone, sign in with the same account, and follow the prompts to join your network. Once it’s connected, your phone and MacBook can find each other privately no need to be on the same Wi-Fi.

Tailscale device client

Once both devices are connected, Tailscale assigns each one its own private IP address. That’s how they can find and talk to each other over your private network.

Head over Cloudflare

Now let’s head over to Cloudflare and get the domain ready for the next part of the setup.

Let’s bring your domain into Cloudflare:

Click menu Domain → Overview → Add Domain → Connect a Domain

Cloudflare add domain

Click Continue and Cloudflare will show you two nameservers. We’ll need them in the next step, so keep this page open or copy both nameserver addresses somewhere.

Cloudflare nameserver

Let’s head to your Rumahweb dashboard to update the domain’s nameservers. Keep those two nameserver addresses from Cloudflare

Select menu Domain → Setting → Pengaturan nameserver

Rumahweb domain 1

Then input nameserver from Cloudflare

Rumahweb domain nameserver

Save Changes to apply the new nameservers. Now the domain can start pointing to Cloudflare. Let’s give the update a little time to take effect.

Let’s return to Cloudflare and check whether your domain is ready for the next step.

Open menu Domain and select your domain to enter its dashboard. Then head to DNS → Records and click Add record. That’s where we’ll create the DNS record for your server.

Cloudflare add record dns

Now let’s connect your domain to server MacBook. Add an A record using your domain , then enter the MacBook’s Tailscale IP as the value. This points the name to your server over your private network.

Cloudflare add record dns wildcard

Add a wildcard DNS record by entering * as the name and using the same Tailscale IP as the value. That way, new subdomains can point to your server too.

Next, we’ll create a Cloudflare API token so Traefik can work with your DNS settings. Head to your Cloudflare profile and open API Tokens

Click Manage account → Account API tokens → Create Token → Start from scratch (Custom)

Select a specific domain and select your domain.
At the DNS and Zone tab, select DNS to Edit, select your Zone to Read

That gives the token the access it needs for DNS updates on this domain.

Cloudflare add api token

Click Review token, check the permissions, then select Create token. Copy the token and save it somewhere secure, we’ll need it later, and Cloudflare may only show it once.

Using TLS Protocol

Now we’re ready to set up TLS so connections to our services can be encrypted.

Let’s ask Let’s Encrypt for a certificate using Cloudflare DNS validation. Replace YOUR_CLOUDFLARE_API_TOKEN and [email protected] with your own values, then run this command from your project folder

docker run --rm -it
    -v "$(pwd)/acme_state:/acme.sh"
    -v "$(pwd)/certs:/certs"
    -e CF_Token="YOUR_CLOUDFLARE_API_TOKEN"
    neilpang/acme.sh --issue --dns dns_cf
    -d "*.mendadak-goblok.my.id" -d "mendadak-goblok.my.id"
    --key-file /certs/tls.key
    --fullchain-file /certs/tls.crt
    --server letsencrypt
    --email [email protected]
              

This requests one certificate for both the root domain and its subdomains. When the command finishes successfully, you should find the certificate files in certs/. Keep your API token private, and don’t publish it in your article or a public repository.

Create a folder named config, then add a file inside it called traefik-dynamic.yaml

traefik-dynamic.YAML YAML
tls:
  certificates:
    - certFile: /certs/tls.crt
      keyFile: /certs/tls.key
  stores:
    default:
      defaultCertificate:
        certFile: /certs/tls.crt
        keyFile: /certs/tls.key

Your project should now look like this

Structure project

Now let’s open docker-compose.yaml and adjust it for our setup

docker-compose.YAML YAML
services:
  traefik:
    image: traefik:v3.7
    container_name: traefik
    # restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M
    command:
      # - "--api.insecure=true"
      # - "--providers.docker=true"
      # - "--providers.docker.exposedbydefault=false"
      # - "--entrypoints.web.address=:80"

      # Logging
      - "--log.level=INFO"
      - "--accesslog=true"
      # Docker provider
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false"
      - "--providers.file.filename=/etc/traefik/dynamic.yaml"
      - "--providers.file.watch=true"

      # Entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443" # use in label
      - "--entrypoints.web-public.address=:8080"

      # Timeouts
      - "--entrypoints.web.transport.respondingTimeouts.readTimeout=3600s"
      - "--entrypoints.web.transport.respondingTimeouts.writeTimeout=3600s"
      - "--entrypoints.web.transport.respondingTimeouts.idleTimeout=3600s"
      - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=3600s"
      - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=3600s"
      - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=3600s"
      - "--entrypoints.web-public.transport.respondingTimeouts.readTimeout=3600s"
      - "--entrypoints.web-public.transport.respondingTimeouts.writeTimeout=3600s"
      - "--entrypoints.web-public.transport.respondingTimeouts.idleTimeout=3600s"

      # HTTP to HTTPS redirect
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"

      # Dashboard
      - "--api.dashboard=true"
      - "--global.sendAnonymousUsage=false"
    ports:
      - "80:80"
      - "443:443"
      # - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro
      - ./certs:/certs:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`mendadak-goblok.my.id`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls=true"
    networks:
      - pandz-network

  whoami:
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=pandz-network"
      - "traefik.http.routers.whoami.rule=Host(`whoami.mendadak-goblok.my.id`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.service=whoami-svc"
      - "traefik.http.services.whoami-svc.loadbalancer.server.port=80"
    networks:
      - pandz-network

networks:
  pandz-network:
    external: true

I’m using Traefik as my reverse proxy and whoami as a simple test service. Whoami gives us an easy way to check that requests are reaching the right place.

Traefik listens on two ports:

  • check_circle Port 80 receives regular http:// traffic and redirects it to HTTPS.
  • check_circle Port 443 handles encrypted https:// traffic.

Both containers join the Docker network pandz-network, so Traefik can reach the test service. Traefik also reads Docker’s socket to discover containers automatically. I’ve mounted that socket as read-only, along with the dynamic configuration and certificate files Traefik needs.

That’s the basic flow: a request reaches Traefik, gets redirected or secured with HTTPS, and is then routed to the right service.

What happens when I visit https://mendadak-goblok.my.id?

The request arrives at Traefik on port 443. Traefik checks the hostname, matches it to the dashboard router, and sends it to its internal dashboard service, api@internal. If everything is configured correctly, the Traefik dashboard appears.

Now let’s try https://whoami.mendadak-goblok.my.id.

This request also arrives on port 443, but Traefik sees a different hostname. It matches the whoami router and forwards the request across the Docker network to the whoami container on port 80.

Run docker compose:

docker compose up -d

Docker compose ps traefik

If something isn’t working as expected, let’s peek at Traefik’s live logs. From your project folder, run:

Debug Log

docker compose logs -f traefik

New log messages will appear as they happen. Press Ctrl+C when you’re ready to stop watching.

See the Result

Lets see it working:

I connected my Mac server to Tailscale

Tailscale device mac

I’ve connected my Zenfone to Tailscale, so it can join my private network alongside my Mac server

Tailscale device zenfone

I can now reach my Mac server from my Zenfone over my private Tailscale network. One device hosts the service, the other connects and I can check in without exposing the server to the public internet.

I open https://mendadak-goblok.my.id to check that I can reach the Traefik dashboard

Result dashboard

Then I try https://whoami.mendadak-goblok.my.id and see whether Traefik routes me to the whoami test service.

Result whoami

Both links work! My Zenfone can reach my Mac server through the private network

Reference

Reference:

https://doc.traefik.io/traefik/getting-started/docker/
https://tailscale.com/docs/how-to/quickstart https://zerossl.com/p/acme-sh

READ BEYOND THE VOID

Technical blueprint background
Backend Notes

Setup New Server

Just notes to myself.

Read Entry
Abstract digital network
Backend Notes

SSH Without Password

Just notes to myself.

Read Entry
Circuit board macro
Backend Notes

N+1 Query Problem

Avoid N+1 query problem...

Read Entry