Skip to main content

A Secure Docker Compose Starter Stack for Homelab Services

Secure Docker Compose Homelab Stack with Caddy Reverse Proxy


Last updated: August 2026

Tested on: Ubuntu 24.04 LTS, Docker Engine [add your tested version], Docker Compose v2 [add your tested version], Caddy 2, Uptime Kuma 2, LAN-only server at 192.168.1.20. Replace this line with your real test environment before publishing.

Most Docker Compose examples are written to get the service running quickly.

That is fine when you are testing something on a spare evening. It is less fine six months later, when the home server has ten containers, five open ports, two forgotten dashboards, one mystery database volume and no clear restore process.

This guide shows how I would build a secure Docker Compose homelab stack before adding Jellyfin, Paperless, Gitea, Home Assistant, dashboards, media tools, databases or anything else that slowly turns a Linux box into “production” at home.

This is not enterprise Kubernetes. It is not a polished corporate platform. It is a practical Docker Compose homelab baseline built around a simple idea:

Publish one reverse proxy. Keep backend services private. Make the stack boring enough to maintain later.

Boring is good. Boring survives reboots, updates, tired evenings and those “why is this admin panel exposed on port 9443?” moments.


Quick answer: what is the safest Docker Compose homelab pattern?

The safest beginner-friendly pattern for a Docker Compose homelab is to expose only a reverse proxy on ports 80 and 443, keep application containers on private Docker networks, avoid privileged: true, avoid mounting the Docker socket by default, store persistent data under a clear folder such as /srv/docker, and back up the Compose files plus application data before updates.

For this guide, the stack looks like this:

LAN or internet
  -> router / firewall
      -> Caddy reverse proxy on ports 80 and 443
          -> private Docker network
              -> Uptime Kuma
              -> future homelab services

The key habit is simple:

Use ports for the reverse proxy. Use expose or private Docker networking for backend web services.

For most homelab dashboards, this is the pattern to avoid:

ports:
  - "8080:80"

This is usually the safer pattern:

expose:
  - "80"
networks:
  - proxy

The application stays reachable to the Caddy reverse proxy inside Docker, but it is not directly published on the host network.


Who this guide is for

This guide is for you if you run, or plan to run, self-hosted services on a Linux home server and want a cleaner starting point than a pile of random Compose files copied from tutorials.

It fits setups like:

  • an old Dell, HP workstation, ThinkCentre, mini PC, NUC or laptop used as a home server;
  • a Debian or Ubuntu server running Docker Compose;
  • a home network where some services are LAN-only;
  • a small VPS used like a homelab server;
  • a Docker host that already has too many published ports;
  • a new self-hosting setup where you want the baseline to be sane from day one.

If the Linux host itself is not hardened yet, start with the broader Linux Home Server Security Guide and the Linux Home Server Security Checklist. This Docker stack should sit on top of a patched, firewalled and backed-up host.


Table of contents

  1. What this secure homelab stack includes
  2. Why use Caddy as the reverse proxy?
  3. Security rules for this starter stack
  4. Prerequisites before you begin
  5. Build the secure Docker Compose homelab stack
  6. How to add new services safely
  7. A safer database pattern
  8. What I would not add to a starter stack
  9. Backups and restore notes
  10. Monthly maintenance checklist
  11. Common mistakes
  12. Frequently asked questions

What this secure homelab stack includes

This Docker Compose starter stack includes:

  • Caddy as the only published web entry point;
  • Uptime Kuma for basic service monitoring;
  • whoami as a temporary reverse proxy test container;
  • a clean /srv/docker/starter-stack folder layout;
  • a local .env file for non-secret settings;
  • private Docker networking for backend services;
  • no direct published port for Uptime Kuma;
  • no Docker socket mount;
  • no privileged containers;
  • a simple backup and restore pattern.

The idea is not to include every useful homelab tool. The idea is to build a foundation you can understand later.

A starter stack should not be impressive. It should be clear.


Why use Caddy as the reverse proxy?

I am using Caddy here because it is a good default for a small homelab:

  • the configuration is short;
  • HTTPS is simple for real domains;
  • the Caddyfile is easy to keep with the stack;
  • there is no management dashboard to expose;
  • basic reverse proxy use does not require the Docker socket;
  • it keeps the setup understandable.

This does not mean Caddy is always the best reverse proxy. If you prefer Nginx Proxy Manager because you want a web UI, or Traefik because your homelab is heavily label-driven, that is fine.

The security model should stay the same:

One public or LAN-facing entry point. Backend containers stay private unless they genuinely need a host port.

For a deeper comparison, see the existing reverse proxy guide for home servers.


Security rules for this starter stack

Before touching the terminal, these are the rules for this stack.

1. Caddy is the only service with published web ports

Caddy gets host ports 80 and 443. Uptime Kuma does not get 3001:3001. The test container does not get a host port either.

2. Backend services stay on a private Docker network

The backend services are reachable by Docker service name from Caddy, not by random ports exposed on the server.

3. No Docker socket mount by default

This stack does not mount:

/var/run/docker.sock

Some tools need it. Many tutorials ask for it casually. That does not make it harmless. If a container can talk to the Docker daemon, treat it like a high-trust component.

4. No privileged containers

This starter stack does not use:

privileged: true

If a normal dashboard or web app needs privileged mode, I want to know why before it goes anywhere near the server.

5. Keep persistent data obvious

The data lives under:

/srv/docker/starter-stack

That is easy to find, easy to back up and easy to restore.

6. Backups are part of security

A stack is not finished just because the dashboard loads. A stack is finished when you know how to restore it.

For a deeper backup plan, see Backing Up Docker Containers: The Homelab Disaster You Can Avoid.


Prerequisites before you begin

This guide assumes:

  • a Linux server, usually Debian or Ubuntu;
  • Docker Engine installed;
  • Docker Compose v2 installed;
  • UFW or another host firewall configured;
  • a LAN IP such as 192.168.1.20;
  • local DNS or a public domain name;
  • a backup location;
  • basic comfort editing files in the terminal.

If SSH is not hardened yet, fix that first. The SSH hardening guide for home servers covers keys, firewall rules and safe remote access.

If your firewall rules are still guesswork, read UFW Firewall Rules for Home Servers before exposing anything.


Build the secure Docker Compose homelab stack

This is the practical build section. Do not skip the testing steps. The whole point is to confirm that only the intended services are reachable.

1. Plan the stack and DNS names

Decide whether this stack is:

  • LAN-only, reachable only inside your home network;
  • VPN-only, reachable through WireGuard, Tailscale or another VPN;
  • public, reachable from the internet on ports 80 and 443.

For many homelabs, LAN-only or VPN-only is the better default.

Example values used in this guide:

Server IP:       192.168.1.20
Uptime Kuma:     uptime.example.com
Test service:    test.example.com
Timezone:        Europe/Lisbon

For a LAN-only setup, you might use local names instead:

uptime.home.arpa
test.home.arpa

Your local DNS must point those names to the server IP. Caddy cannot fix DNS that points nowhere.

2. Create the Docker Compose folder layout

I keep homelab Docker services under /srv/docker. It is not magic. It is just easy to remember.

Create the folder structure:

sudo mkdir -p /srv/docker/starter-stack/{caddy/data,caddy/config,uptime-kuma/data}
sudo chown -R "$USER:$USER" /srv/docker/starter-stack
cd /srv/docker/starter-stack

The layout will look like this:

/srv/docker/starter-stack/
  compose.yml
  Caddyfile
  .env
  RESTORE.md

  caddy/
    data/
    config/

  uptime-kuma/
    data/

This keeps the recipe and the data together:

  • compose.yml describes the containers;
  • Caddyfile describes reverse proxy routing;
  • .env keeps local values out of the Compose file;
  • RESTORE.md explains how to rebuild the stack;
  • service folders hold persistent application data.

When something breaks, clarity matters.

3. Create the environment file

Create the .env file:

nano .env

Example:

TZ=Europe/Lisbon
BIND_IP=192.168.1.20
DOMAIN_UPTIME=uptime.example.com
DOMAIN_TEST=test.example.com

Change these values for your setup.

BIND_IP should normally be the server LAN IP for a LAN-first homelab. If you intentionally want Caddy to listen on every interface, use:

BIND_IP=0.0.0.0

Do not do that by habit. Do it only when that is really the exposure you want.

Protect the file:

chmod 600 .env

An .env file is not a secrets vault. It is just a file. Do not commit it to a public Git repository.

If you keep the stack in Git, add this:

echo ".env" >> .gitignore

4. Create the Docker Compose file

Create the Compose file:

nano compose.yml

Paste this:

name: homelab-starter

x-common: &common
  restart: unless-stopped
  security_opt:
    - no-new-privileges:true
  logging:
    driver: local

services:
  caddy:
    <<: *common
    image: caddy:2
    ports:
      - "${BIND_IP}:80:80"
      - "${BIND_IP}:443:443"
    environment:
      TZ: ${TZ}
      DOMAIN_UPTIME: ${DOMAIN_UPTIME}
      DOMAIN_TEST: ${DOMAIN_TEST}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/data:/data
      - ./caddy/config:/config
    networks:
      - proxy

  uptime-kuma:
    <<: *common
    image: louislam/uptime-kuma:2
    expose:
      - "3001"
    volumes:
      - ./uptime-kuma/data:/app/data
    networks:
      - proxy

  whoami:
    <<: *common
    image: traefik/whoami:v1.10
    expose:
      - "80"
    read_only: true
    cap_drop:
      - ALL
    networks:
      - proxy

networks:
  proxy:
    name: homelab_proxy

The important part is not the specific demo service. The important part is the exposure model.

Caddy is the only service with:

ports:

Uptime Kuma and whoami use:

expose:

That documents the internal container ports without publishing them on the host.

In plain English:

  • Caddy listens on the server.
  • Caddy talks to backend containers by service name.
  • The backend containers do not get random host ports.

That is the pattern you should reuse when adding more homelab services.

5. Create the Caddy reverse proxy configuration

Create the Caddyfile:

nano Caddyfile

For a real domain, use this pattern:

{
    email admin@example.com
}

{$DOMAIN_UPTIME} {
    encode zstd gzip
    reverse_proxy uptime-kuma:3001
}

{$DOMAIN_TEST} {
    reverse_proxy whoami:80
}

Replace admin@example.com with your real admin email.

For a LAN-only setup using internal names, you can use Caddy internal TLS:

{
    email admin@example.com
}

uptime.home.arpa {
    tls internal
    encode zstd gzip
    reverse_proxy uptime-kuma:3001
}

test.home.arpa {
    tls internal
    reverse_proxy whoami:80
}

For LAN-only names, make sure your local DNS points to the server:

uptime.home.arpa  -> 192.168.1.20
test.home.arpa    -> 192.168.1.20

If you use Caddy internal TLS, browsers may require you to trust the internal CA certificate. That is normal. Do not confuse local certificate trust with a broken reverse proxy.

6. Validate and start the stack

Before starting anything, inspect the final Compose configuration:

docker compose -f compose.yml config

This catches simple mistakes and shows the resolved configuration after variables are applied.

Pull the images:

docker compose -f compose.yml pull

Start the stack:

docker compose -f compose.yml up -d

Check the containers:

docker compose -f compose.yml ps

Then check published ports:

docker ps --format "table {{.Names}}\t{{.Ports}}"

You want output roughly like this:

NAMES                          PORTS
homelab-starter-caddy-1        192.168.1.20:80->80/tcp, 192.168.1.20:443->443/tcp
homelab-starter-uptime-kuma-1  3001/tcp
homelab-starter-whoami-1       80/tcp

The names may differ. The important part is this:

  • Caddy publishes 80 and 443.
  • Uptime Kuma does not publish 3001 on the host.
  • whoami does not publish a host port.

If Uptime Kuma shows something like this, stop and fix it:

0.0.0.0:3001->3001/tcp

That means it is directly published. That is not the design of this stack.

7. Configure UFW firewall rules

For a LAN-only homelab, I would start with rules like this:

sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 443 proto tcp
sudo ufw status verbose

Change 192.168.1.0/24 to your real LAN range.

For a public web service, the web rules change:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose

Keep SSH restricted if possible. A common pattern is public HTTP/HTTPS, but SSH only from the LAN or VPN.

There is an important Docker warning here. Docker published ports may not behave the way beginners expect when UFW is active. That is why this stack avoids publishing backend ports in the first place.

Do not rely on UFW to rescue a messy Compose file. Fix the Compose file.

For more detail, see Docker and UFW: Why Your Firewall Rules Do Not Protect Published Containers.

8. Test published ports from another machine

Do not only test from the Docker host. Test from another computer on the LAN.

For a public or normal HTTPS domain:

curl -I https://uptime.example.com
curl -I https://test.example.com

For a LAN-only internal TLS setup:

curl -k -I https://uptime.home.arpa
curl -k -I https://test.home.arpa

Now test that Uptime Kuma is not directly reachable by IP and port:

curl -I http://192.168.1.20:3001

From another LAN machine, that should fail.

If it works, check published ports again:

docker ps --format "table {{.Names}}\t{{.Ports}}"

Something is probably publishing 3001.

9. Configure Uptime Kuma monitoring

Open Uptime Kuma through the reverse proxy:

https://uptime.example.com

Create the admin account.

Then add a few useful monitors:

  • the Uptime Kuma URL through Caddy;
  • the temporary test URL;
  • your router IP;
  • your server SSH port from the LAN;
  • important self-hosted services;
  • DNS checks for local or public names;
  • certificate expiry checks where useful.

Do not monitor everything every ten seconds. That creates noise.

For a normal home server, sensible intervals are usually:

  • critical local services: 30 to 60 seconds;
  • normal dashboards: 60 to 120 seconds;
  • public websites: 60 to 300 seconds;
  • non-critical devices: 300 seconds or more.

Uptime Kuma is there to catch boring failures early: a container stopped, a proxy route broke, a service is slow, a certificate is close to expiring or DNS points to the wrong place.

For a full monitoring setup, see Uptime Kuma for Home Servers.

10. Remove the temporary test container

The whoami container is useful while testing. It should not live forever.

After the proxy works, remove this service from compose.yml:

  whoami:
    <<: *common
    image: traefik/whoami:v1.10
    expose:
      - "80"
    read_only: true
    cap_drop:
      - ALL
    networks:
      - proxy

Then remove the test route from Caddyfile:

{$DOMAIN_TEST} {
    reverse_proxy whoami:80
}

Apply the change:

docker compose -f compose.yml up -d --remove-orphans

Check again:

docker compose -f compose.yml ps
docker ps --format "table {{.Names}}\t{{.Ports}}"

Old test services are how “temporary” becomes “why is this still running?” three years later.

11. Back up the stack and write restore notes

This folder should be included in your normal backup plan:

/srv/docker/starter-stack/

At minimum, back up:

  • compose.yml;
  • Caddyfile;
  • .env;
  • RESTORE.md;
  • caddy/data;
  • caddy/config;
  • uptime-kuma/data;
  • future app folders.

Example backup:

sudo rsync -aAX --info=progress2 \
  /srv/docker/starter-stack/ \
  /mnt/backup/homeserver/starter-stack/

Caddy data may include certificates and keys. Treat the backup accordingly.

Now create a restore note:

nano RESTORE.md

Example:

# Restore notes for homelab starter stack

1. Install Docker Engine and Docker Compose v2.

2. Copy this folder back to:

   /srv/docker/starter-stack

3. Check ownership:

   sudo chown -R USER:USER /srv/docker/starter-stack

4. Check .env values:

   BIND_IP
   DOMAIN_UPTIME
   TZ

5. Validate the Compose file:

   docker compose -f compose.yml config

6. Start the stack:

   docker compose -f compose.yml up -d

7. Check published ports:

   docker ps --format "table {{.Names}}\t{{.Ports}}"

8. Open Uptime Kuma through the reverse proxy.

9. Confirm monitors, notifications and certificates are present.

This file is not glamorous. That is why it works.

When a server fails, you do not want to rebuild your setup from old browser tabs and memory.


How to add new services safely

Once the starter stack works, this is the pattern I would use for most new web services:

  myapp:
    <<: *common
    image: example/myapp:1.2.3
    expose:
      - "8080"
    volumes:
      - ./myapp/config:/config
      - ./myapp/data:/data
    networks:
      - proxy

Then add a Caddy route:

myapp.example.com {
    reverse_proxy myapp:8080
}

Notice what is missing:

ports:
  - "8080:8080"

That is intentional.

Only publish host ports when the service genuinely needs to be reachable directly from outside Docker. Most web dashboards do not.

A good Docker Compose homelab should not turn into this:

service1  -> 0.0.0.0:3000
service2  -> 0.0.0.0:8080
service3  -> 0.0.0.0:9000
service4  -> 0.0.0.0:9443
service5  -> 0.0.0.0:5000

That works until nobody remembers what half the ports do.

A cleaner setup is:

Caddy:
  80 and 443

Backend containers:
  private Docker network

For the general hardening principles behind this, read Docker Security for Homelab Beginners.


A safer database pattern for Docker Compose

Databases should not be casually published to the LAN.

This is usually wrong for a homelab application database:

ports:
  - "5432:5432"

If only one application needs PostgreSQL, keep the database on a private internal Docker network.

Example pattern:

services:
  app:
    image: example/app:1.2.3
    expose:
      - "8080"
    networks:
      - proxy
      - app_internal

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    volumes:
      - ./postgres/data:/var/lib/postgresql/data
    networks:
      - app_internal

secrets:
  postgres_password:
    file: ./secrets/postgres_password

networks:
  proxy:
    name: homelab_proxy
  app_internal:
    internal: true

Create the secret file:

mkdir -p secrets
openssl rand -base64 32 > secrets/postgres_password
chmod 600 secrets/postgres_password

Not every image supports _FILE environment variables. Check the image documentation before assuming it works.

The point is not that every home server needs a complicated secrets setup on day one. The point is that database passwords do not belong pasted into public screenshots, old Git repos or backup notes with no thought.


Bind mounts: useful, but not harmless

This starter stack uses bind mounts because they make the file layout obvious:

volumes:
  - ./uptime-kuma/data:/app/data

That means the data lives here:

/srv/docker/starter-stack/uptime-kuma/data

Easy to find. Easy to copy. Easy to restore.

But bind mounts are also powerful. A container with write access to a host folder can modify that folder.

Good pattern:

volumes:
  - ./app/data:/data
  - ./app/config:/config:ro

Bad pattern:

volumes:
  - /:/host
  - /etc:/etc
  - /home:/home

Mount the smallest path that works. Use :ro when the container only needs to read a file or folder.


What I would not add to a secure starter stack

Some tools are useful later. That does not mean they belong in the first stack.

Portainer

Portainer can be useful, but it usually needs serious Docker access. I would learn the Compose files first, then add a management UI later if I still wanted one.

Watchtower

Automatic updates are convenient until they break an important service while you are not paying attention.

For important services, I prefer:

docker compose pull
docker compose up -d

Do it after checking backups and release notes.

Docker log dashboards with socket access

Tools like log dashboards can be useful, but many need Docker socket access. That may be acceptable in a controlled setup. It is not a default starter-stack choice for me.

Databases with published ports

PostgreSQL, MariaDB and Redis should not be published just because a tutorial did it.

If only containers need them, keep them inside Docker networks.

Anything with privileged mode

If a service needs this:

privileged: true

understand why before adding it.

Do not normalize privileged containers in a beginner stack.


Updating the stack

A simple manual update routine:

cd /srv/docker/starter-stack

docker compose -f compose.yml pull
docker compose -f compose.yml up -d
docker image prune

For important services, back up first.

I do not like blind auto-updates for a home server that stores real data.

A broken container is annoying. A broken container with no backup is a lesson you did not need.


Backups for a Docker Compose homelab

A good Docker backup plan is not about backing up containers. Containers are usually disposable.

You back up:

  • Compose files;
  • .env files;
  • reverse proxy configuration;
  • application data;
  • named volumes, if used;
  • database dumps, if needed;
  • restore notes;
  • scripts used to rebuild the stack.

For this starter stack, the easiest target is:

/srv/docker/starter-stack/

Example:

sudo mkdir -p /mnt/backup/homeserver/docker/

sudo rsync -aAX --info=progress2 \
  /srv/docker/starter-stack/ \
  /mnt/backup/homeserver/docker/starter-stack/

Be careful with secrets. Backups often contain the most sensitive version of your server: configuration files, credentials, certificates and private data all in one place.

If you copy backups to cloud storage, encrypt them first.


Monthly maintenance checklist

Once a month, run through this:

cd /srv/docker/starter-stack

docker compose -f compose.yml ps
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
docker system df
sudo ufw status verbose
df -h

Ask:

  • Is Caddy still the only published web entry point?
  • Did a new service accidentally expose a port?
  • Are old test containers gone?
  • Is disk space healthy?
  • Are backups running?
  • Have I tested a restore recently?
  • Are important images wildly outdated?
  • Does Uptime Kuma still alert properly?
  • Do DNS names still point where expected?
  • Are there any temporary firewall rules that became permanent?

This is enough for many small home servers.

Not perfect. Not enterprise. Much better than ignoring it.


Final checklist for the secure homelab stack

  • Caddy is the only service publishing ports 80 and 443.
  • Uptime Kuma is reachable through the reverse proxy.
  • Uptime Kuma is not reachable directly on server-ip:3001.
  • The .env file is not committed to Git.
  • The .env file has restricted permissions.
  • No container uses privileged: true.
  • No container mounts /var/run/docker.sock.
  • No container mounts / from the host.
  • UFW rules match the intended exposure.
  • DNS records point to the server.
  • Backups include /srv/docker/starter-stack.
  • RESTORE.md exists.
  • Uptime Kuma monitors the important services.
  • The temporary whoami service has been removed.

Common mistakes with Docker Compose homelabs

Publishing every container port

This is the classic mistake:

ports:
  - "8080:80"

Sometimes it is necessary. Often it is just copied from an example. For web services behind a reverse proxy, ask whether the container really needs a host port.

Trusting UFW without testing Docker exposure

UFW may say incoming traffic is denied, but Docker-published ports can still surprise people. Always test from another machine.

Leaving test containers running

A test service should have a removal date. If it has been running for months, it is no longer a test. It is undocumented infrastructure.

Using latest everywhere

This is convenient:

image: someapp:latest

It can also surprise you later.

For important services, consider pinning at least the major version:

image: postgres:17
image: caddy:2
image: louislam/uptime-kuma:2

You still update. You just choose when.

Not knowing where the data lives

If you cannot answer where a service stores its data, you do not have a backup plan yet.

Backing up after the update breaks

The backup that matters is the one from before the risky change.


Frequently asked questions

Is Docker Compose safe for a homelab?

Docker Compose can be safe enough for many homelabs if you understand what it is doing. The biggest problems usually come from publishing too many ports, using privileged containers, mounting dangerous host paths, exposing admin dashboards and not backing up persistent data.

Should I use Caddy, Nginx Proxy Manager or Traefik?

Use Caddy if you want simple text configuration and easy HTTPS. Use Nginx Proxy Manager if you want a friendly web UI. Use Traefik if your homelab is heavily Docker Compose-based and you want routing to live in labels. The best choice is the one you can maintain without guessing.

Should Uptime Kuma be exposed to the internet?

Usually no. For a homelab, I prefer Uptime Kuma to be LAN-only, VPN-only or behind a reverse proxy with strong protection. It is a monitoring dashboard, not something I want casually exposed.

Do I need expose in Docker Compose?

Containers on the same Docker network can often reach each other by service name even without expose. I still like using expose in beginner-friendly Compose files because it documents the intended internal port without publishing it to the host.

Is an .env file secure?

No. An .env file is just a file. It is useful for keeping local values out of the Compose file, but it still needs restricted permissions, careful backups and should not be committed to public Git repositories.

Do I need Docker secrets in a small homelab?

Not always, but they are useful when images support them. For simple services, a protected .env file may be enough. For databases and API keys, secrets are cleaner than putting passwords directly into Compose files.

Can I use this stack with Nginx Proxy Manager instead of Caddy?

Yes. The core idea is the same: publish the reverse proxy, keep backend services private, back up the proxy configuration and avoid direct random host ports. The Compose file and proxy configuration would change, but the security model stays the same.

Should I use Watchtower for automatic updates?

I would not start with automatic updates for important services. Manual updates with a backup first are less convenient but easier to understand. Automatic updates are acceptable only when you accept the risk and know how to roll back.

What is the first thing to check if a service does not work?

Check DNS, then Caddy logs, then container status, then published ports.

docker compose -f compose.yml ps
docker compose -f compose.yml logs caddy --tail=100
docker ps --format "table {{.Names}}\t{{.Ports}}"

Most problems are boring: wrong DNS, wrong service name, wrong internal port or a container that did not start.


This post should sit inside the Docker and security cluster. These are the natural next reads:


Official references


Final thoughts

A secure Docker Compose homelab stack is not about adding every hardening option you can find.

It is about starting with a layout that does not create chaos.

One reverse proxy. Private backend services. Clear folders. No random published ports. No Docker socket by default. No privileged containers unless there is a very good reason. Backups before experiments. Monitoring before surprises.

That is the kind of setup I want on a home server.

Not because it is fancy.

Because it is easy to understand when something breaks.

Written by MS. MS is a Linux homelab and cybersecurity enthusiast who documents practical experiments with home servers, Docker, firewalls, backups, monitoring, honeypots and old hardware. The guides on IT Random Stuff are based on hands-on testing, real configurations and lessons learned from running Linux systems at home.

Comments

Popular posts from this blog

OpenCanary Honeypot on Ubuntu: 2026 Home Lab Setup Guide

I first wrote about OpenCanary years ago, and that old version badly needed an update. The tool is still useful, but the way I would deploy it in a home lab today is different: newer Ubuntu versions, Python 3, Docker as an option, cleaner logging, and no copy-pasted example credentials that look like something someone might actually reuse. This guide is the updated version: how I would set up an OpenCanary honeypot on Ubuntu in 2026 for a small home network or homelab. The goal is not to trap random people on the internet. The goal is to create a quiet internal warning system: a fake service that should never be touched, so any connection to it deserves attention. Important: this is a defensive security guide. Run OpenCanary only on systems and networks you own or have permission to monitor. Do not expose a honeypot to the public internet unless you understand the logging, legal, abuse and maintenance implications. If you are building a Linux home server security setup fro...

Lenovo ThinkPad X250 on Linux: Tweaks, Undervolting, Battery Life and 2026 Update

I wanted a cheap, small, serviceable Linux laptop. Something light enough to carry, easy enough to repair, and inexpensive enough that upgrades would still make sense. The Lenovo ThinkPad X250 was a good candidate because it has a 12.5-inch form factor, a proper ThinkPad keyboard, SSD upgrade options, replaceable parts, Ethernet, docking support and generally good Linux compatibility. I found one on eBay for around 130€ : an Intel Core i5-5300U model with 8GB RAM , a 128GB SSD , two batteries and an HD screen with a small bruise. The plan was simple: clean it, repaste it, upgrade the SSD, install Linux Mint, undervolt it and see how useful it could still be. This post started as my original 2019 notes about tweaking the Lenovo X250 in Linux. I have now updated it with a 2026 perspective, cleaner instructions, better internal links and a more realistic look at whether this old ThinkPad is still worth using. Related posts: Linux Home Server Security Checklist Docker Secu...

Backing Up Docker Containers: The Homelab Disaster You Can Avoid

Nobody thinks about Docker backups when everything is working. The dashboard loads. The media server streams. The reverse proxy behaves. The database container has a cute little green “healthy” status. Life is good. Then one day you run an update, delete the wrong volume, lose a disk, break a Compose file, or discover that the “temporary” container from six months ago was actually storing something important. That is when Docker stops feeling magical and starts feeling like archaeology. This post is a practical guide to backing up Docker containers on a Linux home server . Not enterprise disaster recovery. Not Kubernetes. Not a 40-page policy document. Just a realistic backup plan for a homelab running Docker Compose, bind mounts, named volumes and a few services that became more important than expected. The main idea is simple: You do not really back up containers. You back up the things needed to recreate them, plus the data they would destroy your weekend by losing. ...