Skip to main content
Mini PC Lab logo
Mini PC LabMini PCs for Homelabs
tutorials

Docker Compose Homelab Starter Stack

By Max · October 7, 2026

A compact home server beside three simple service blocks connected by a network cable

A Docker homelab does not need a giant stack on its first day. It needs a clear reason for every service, a place for every file, and enough spare memory to recover when one container has a bad day.

This starter stack has three always-on services:

  • Caddy for local HTTPS and reverse proxying
  • Uptime Kuma for checking whether services answer
  • Homepage for a simple launch page

That is enough to establish the habits that matter. Every service has a persistent location, every container has a restart policy, and optional services can stay off until you actually need them. The example is designed for a small Linux host with Docker Engine and Compose already installed. If you need the host setup first, use our Docker on a mini PC guide.

The short answer

Start with a 4GB mini PC only when this is genuinely a small stack. The host operating system, Docker, filesystem cache, and monitoring overhead need room before any container receives a limit. A 8GB host gives a more comfortable starting point for the three services below. A 16GB host gives you room for a database-backed app or a few heavier services without turning every update into a memory exercise.

Those are planning thresholds, not capacity guarantees. Docker says containers have no resource constraints by default, so a process can consume as much host memory as the kernel allows. Docker also warns that a host running out of memory can affect the daemon and other important processes. The right limit comes from observing the workload and leaving room for the host, not from adding up a table and assuming the result is exact. See the Docker resource constraints documentation for the underlying behavior.

The first stack should therefore be boring:

ServiceJobStarting memory budgetStorage patternCPU pattern
CaddyRoutes local names to services and manages certificates128 to 256 MBSmall config plus certificate dataLow most of the time, short bursts during certificate work
Uptime KumaChecks pages, ports, and other endpoints256 to 512 MBSmall application database that grows with historyLow most of the time
HomepageGives you one local launch page128 to 256 MBSmall configuration filesLow
Docker and host reserveKernel, filesystem cache, image work, and recovery roomAt least 2 GB on an 8GB hostDocker image store and logsVaries with image pulls and updates

The three service figures are conservative planning bands. They are not measurements from this site, and they are not promises about a particular release or configuration. Keep the first limits generous, watch actual use with docker stats, then adjust one service at a time.

What this stack is for

This setup fits a personal LAN where you want several web services behind friendly names such as status.home.arpa and photos.home.arpa. It gives you a central entry point and a way to notice when something stops responding.

It does not provide:

  • A public edge security system for an exposed business application
  • A backup by itself
  • A high-availability cluster
  • GPU scheduling for artificial intelligence or video transcoding
  • A substitute for Home Assistant OS when you need appliance-style hardware support
  • A safe way to expose the Docker socket to every dashboard

If you need to expose services to the internet, treat that as a separate security project. A reverse proxy can route traffic, but it does not make an unsafe application safe.

Create one directory per stack

Keep the baseline in a directory with a name that describes its job:

mkdir -p ~/services/core/{caddy,homepage}
cd ~/services/core

The shell brace expansion above creates the two configuration directories used by the example. Uptime Kuma keeps its application data in a named volume so its database is not mixed with the other configuration files.

The resulting layout is simple:

~/services/core/
├── compose.yaml
├── .env
├── caddy/
│   └── Caddyfile
└── homepage/
    ├── bookmarks.yaml
    ├── services.yaml
    └── settings.yaml

Keep .env readable only by the account that runs the stack when it contains private values. Do not put passwords or API tokens in it just because Compose can read the file. Use Compose secrets when the image supports file-based secrets, and keep the secret source outside version control.

A small Compose baseline

The following file gives Caddy access to the edge network, while the two internal services are not published directly on the host. The example uses major image tags so it remains readable. Before a long-lived deployment, replace them with the exact reviewed tags or digests you intend to operate and record those choices with your update notes.

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - edge
    mem_limit: 256m
    cpus: "0.50"
    logging:
      options:
        max-size: "10m"
        max-file: "3"

  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    volumes:
      - uptime_kuma_data:/app/data
    networks:
      - edge
    mem_limit: 512m
    cpus: "1.00"
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3001 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    logging:
      options:
        max-size: "10m"
        max-file: "3"

  homepage:
    image: ghcr.io/gethomepage/homepage:latest
    restart: unless-stopped
    volumes:
      - ./homepage:/app/config:ro
    networks:
      - edge
    mem_limit: 256m
    cpus: "0.50"
    logging:
      options:
        max-size: "10m"
        max-file: "3"

networks:
  edge:
    name: homelab_edge

volumes:
  caddy_data:
  caddy_config:
  uptime_kuma_data:

The mem_limit values are ceilings for this example, not a claim that the services require that much memory. The Compose service reference documents mem_limit and cpus as service settings, while Docker’s resource guide explains the consequences of hard memory ceilings and CPU caps. A limit that is too low can make a service restart or fail during an upgrade, so check logs and observed use before reducing it.

The logging blocks matter on a small boot drive. They cap the size and number of JSON log files for each service. This does not replace application-level retention, and it does not back up the logs. It prevents an ignored error loop from quietly consuming the operating system disk.

Add the Caddy configuration

Caddy needs a route for each service. Use local DNS names that resolve to the mini PC on your LAN. Do not copy public hostnames into the example and accidentally expose the administrative pages.

{
    local_certs
}

status.home.arpa {
    reverse_proxy uptime-kuma:3001
}

start.home.arpa {
    reverse_proxy homepage:3000
}

The local_certs setting tells Caddy to use locally trusted certificates for the internal names. You will need to trust Caddy’s local certificate authority on the devices that visit those names, or use another certificate and DNS arrangement that fits your network.

Do not publish the Uptime Kuma or Homepage ports on the host just to make initial setup easier. Keeping them reachable through the Compose network reduces the number of listening ports. If you need temporary direct access for setup, bind the port to the loopback address and remove it afterward.

Add the Homepage files

Homepage reads configuration files from the mounted directory. Start with a small configuration and add services as you create them.

homepage/settings.yaml:

title: Mini PC home lab
theme: light
color: slate
layout:
  Core:
    style: row
    columns: 2

homepage/services.yaml:

- Core:
    - Uptime Kuma:
        href: https://status.home.arpa
        description: Service health and alerts

homepage/bookmarks.yaml:

- Useful:
    - Docker documentation:
        - abbr: DOCS
          href: https://docs.docker.com/

The dashboard is a convenience layer. Do not treat a green dashboard tile as proof that an application is healthy. Uptime Kuma should check the actual URL or service behavior, and the application should have its own backup and recovery path.

Bring up the baseline safely

Before starting anything, ask Compose to render and validate the file:

docker compose config

This catches malformed YAML, missing variable substitutions, and many path mistakes before containers are created. Then pull and start the services:

docker compose pull
docker compose up -d
docker compose ps

Open Uptime Kuma through its Caddy name and create monitors for the services you care about. Add a monitor for the host itself if you have another device that can check it. An internal monitor cannot report that the entire mini PC has lost power.

Check resource use after the first day and again after an image update:

docker stats --no-stream
docker system df
docker compose logs --tail=100 caddy

The first command shows current container use, the second shows image and volume consumption, and the third helps confirm that the proxy is not looping over a bad route. These are observations for your host, not universal service requirements.

Why these three services belong in the baseline

Caddy

Caddy is the front door. It gives you one place to define local routes and certificate behavior, so each application does not need its own published host port. Its storage is small, but the data volume is still important because certificates and state must survive container replacement.

The main cost is operational rather than computational. A bad proxy rule can make several otherwise healthy services appear broken. Keep the Caddyfile short, change one route at a time, and validate the route from a client on the LAN.

Uptime Kuma

Uptime Kuma turns “I think the service is running” into a check you can inspect. It can watch HTTP endpoints, TCP ports, and other checks that fit your setup. Its history database grows with the number of monitors and retention period, so a long history has a storage cost even when CPU use stays low.

The 256 to 512 MB planning band is intentionally broad. Add a memory limit only after you understand the version and monitor count you will run. If the container is killed during a migration or database maintenance, the monitor itself is not giving you useful coverage.

Homepage

Homepage is a launch page, not a monitoring system. It keeps the stack easy to navigate without requiring a privileged container. Keep its configuration read-only in the container, and edit the files on the host before recreating the service.

If you do not want a dashboard, leave it out. A service that does not solve a real problem is not free just because its idle CPU use is low. Omitting it makes the baseline smaller and the recovery checklist shorter.

Use networks as boundaries

The example has one edge network because Caddy needs to reach the two web services. When you add an application and a database, give them an internal network as well:

services:
  app:
    image: example-app:1
    restart: unless-stopped
    networks:
      - edge
      - app_internal
    depends_on:
      database:
        condition: service_healthy

  database:
    image: postgres:18
    restart: unless-stopped
    networks:
      - app_internal
    volumes:
      - app_database:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

networks:
  edge:
    external: true
    name: homelab_edge
  app_internal:

volumes:
  app_database:

This block is a pattern, not a complete application deployment. Replace the application image and its documented settings only after reading that application’s current guide. The important ideas are that Caddy reaches the app on edge, the app reaches PostgreSQL on app_internal, and the database is not published as a host port.

Compose starts dependencies in order, but it normally waits only until a container is running. Docker’s startup-order guidance says a dependent service should use condition: service_healthy when it needs the dependency to pass a health check first. That avoids one common startup race, but it does not prove that the application is configured correctly.

Do not put every container on every network for convenience. A shared reverse-proxy network is reasonable for web services. Databases, queues, and administrative services should be reachable only by the containers that need them.

Add a database only when an application needs one

PostgreSQL is not a free “homelab essential.” It is a useful dependency for an application that requires it, but it introduces a durable data directory, credentials, upgrades, backup work, and recovery testing.

Use a separate Compose project when the application has its own lifecycle. That makes it possible to update the app without recreating the core services, and it gives the database a clearly named backup target. If several applications truly share one database service, document the ownership and restore order before combining them.

A practical planning band for a small PostgreSQL service is 512 MB to 1 GB of memory before the application workload is counted. Treat that as a starting budget only. Search, indexing, imports, and maintenance can need more. A database that is starved by an arbitrary limit can turn a normal operation into a failed migration.

Redis is similar. Add it only when the application documents it as a dependency. A small Redis service may fit in a 128 to 256 MB planning band, but cache policy and queue behavior determine whether that is safe. Never assume a cache is disposable until you know what the application stores there.

Keep credentials out of the YAML when the image supports secrets. Compose makes a secret available to a service as a file under /run/secrets, which is different from placing the value in ordinary environment variables. Read the application documentation before choosing the variable or file name it expects. The Docker Compose secrets guide explains the supported pattern.

Profiles keep optional work off the boot path

Compose profiles are a good fit for services that you use occasionally or that need more resources. A development database, image-processing worker, or diagnostic tool does not need to start every time the host reboots.

services:
  adminer:
    image: adminer:5
    profiles:
      - debug
    ports:
      - "127.0.0.1:8088:8080"
    networks:
      - app_internal

Start the normal project without the debug service:

docker compose up -d

Enable the profile only when needed:

docker compose --profile debug up -d adminer

Remove it after use:

docker compose stop adminer
docker compose rm adminer

The Compose profiles documentation describes how profile selection changes which services Compose starts. Profiles are not a security boundary, so keep administrative tools bound to loopback and stop them when finished.

Set limits with evidence

Docker’s default is unlimited access to host CPU and memory. That is convenient during exploration, but it lets a runaway process compete with the operating system. A hard memory limit can protect the host, while a CPU limit caps how much CPU time a container can use.

Use these rules when you begin tuning:

  1. Leave the host reserve outside every container total. On an 8GB host, do not allocate 8GB of combined hard limits.
  2. Watch peak use during imports, upgrades, backups, and image processing. Idle use is not enough.
  3. Set a memory ceiling above observed peak use, with room for the application’s own behavior.
  4. Set CPU caps for noisy background work before restricting small always-on services.
  5. Treat an out-of-memory kill as a sizing signal. Do not hide it by repeatedly restarting the container.

For a small host, a starting plan might reserve 2GB for the host and Docker, allow up to 256 MB for Caddy, 512 MB for Uptime Kuma, and 256 MB for Homepage, then leave the rest for one application. That arithmetic is a planning example, not a capacity test. A database, media indexer, photo machine-learning worker, or game server needs its own evidence.

Persistence is not backup

Named volumes keep data when you recreate a container, but they do not protect that data from a dead drive, accidental deletion, ransomware, or a bad migration. A starter stack needs a backup plan before it needs more services.

Back up these layers separately:

  • The Compose files and Caddy configuration
  • Homepage configuration and any other bind-mounted files
  • Uptime Kuma’s named volume
  • Database dumps for database-backed applications
  • Secret sources and the instructions needed to restore them
  • The host configuration that makes Docker start and the storage mount available

Do not back up only the YAML. A Compose file can recreate containers, but it cannot recreate the contents of a named volume unless you copied that data elsewhere. Do not back up only the Docker volume either. Without the Compose file and service-specific settings, the data may be difficult to use.

Keep at least one copy away from the mini PC. Our NAS backup strategy guide explains why a second copy on the same host is not enough. If the stack runs inside a VM, also decide whether you need a VM-level backup. Application backups and VM snapshots serve different recovery goals.

Updates should be deliberate

For a small stack, a careful update is usually simpler than an unattended update tool:

docker compose config > compose.rendered.yaml
docker compose ps
docker compose pull caddy
docker compose up -d --no-deps caddy
docker compose ps
docker compose logs --tail=100 caddy

The rendered file is useful for review, but treat it as sensitive if it contains resolved values. Do not commit it when it includes credentials.

Update one service, check its health and route, then continue. Take an application-aware backup before changing a database image or a service that has a documented migration. Keep the previous image reference in your notes until the new version has been accepted.

Docker’s production guidance supports layering a second Compose file for production-specific changes. That can be useful when a simple base file is shared between a test host and a long-lived host. It is not a reason to hide important settings in a web interface where they cannot be reviewed with the rest of the project.

Avoid making automatic image updates part of the baseline. An update can change a data format, remove a configuration option, or restart a dependency at an inconvenient time. Automation becomes reasonable only after you have a tested backup, an alerting path, a maintenance window, and a rollback plan.

Recovery checks worth practicing

The stack is not ready just because docker compose up -d returned successfully. Practice these checks while the original host is still healthy:

  1. Copy the Compose files and service configuration to another device.
  2. Record the volume names, network names, image references, DNS entries, and secret locations.
  3. Confirm that the backup contains Uptime Kuma data and any certificates or Caddy state you intend to keep.
  4. Start the stack on a spare host or isolated virtual machine when possible.
  5. Confirm that the services start after a reboot.
  6. Confirm that Caddy routes reach the right container and that internal services are not exposed on unexpected host ports.
  7. Trigger a test monitor failure and verify that the notification path works.
  8. Restore one application volume or database dump and verify the application’s own recovery instructions.

For a capacity check, the VM capacity estimator can help you reason about headroom when Docker is running inside a virtual machine. For an always-on host, compare the extra services against the power cost calculator before adding a second machine just to separate a low-demand workload.

When this starter is the wrong fit

Do not use this three-service baseline as a universal answer. Choose a different layout when:

  • You need a public service with stronger isolation, formal monitoring, or a defined recovery time
  • You need GPU access for video transcoding or machine-learning inference
  • You need USB radios, special kernel modules, or host networking that a simple container does not handle well
  • You are building a media stack whose downloads, indexing, and transcoding have different failure and storage patterns
  • You are running a large database or photo library that deserves its own resource and backup plan
  • You need multiple users with strict separation between applications
  • A service vendor provides an appliance image or VM with a documented lifecycle that is safer for your hardware

Home Assistant is a good example of a workload that needs a deliberate installation choice. A container can work, but radio access, updates, backups, and add-ons may make Home Assistant OS or a dedicated VM a better boundary. Our best mini PC for Home Assistant guide covers the hardware side of that decision.

Final recommendation

Begin with the smallest stack that solves a current problem. Caddy, Uptime Kuma, and Homepage give you routing, visibility, and a convenient entry point without consuming the whole host’s attention. Add a database only for an application that needs one. Add authentication, media processing, photo indexing, or hardware access as separate decisions with separate budgets.

The useful Compose habit is not memorizing a long list of images. It is being able to answer four questions for every service:

  • What does it do?
  • What data must survive replacement?
  • Which containers can reach it?
  • How will I know that I can restore it?

If you can answer those questions and still have host memory left after a busy update, your mini PC is ready for the next service.

Sources and further reading