Tag: Docker Compose

Tested docker-compose files and step-by-step setups for self-hosted apps.

  • Home Assistant in Docker: A Self-Hosted Smart Home Guide

    Home Assistant in Docker: A Self-Hosted Smart Home Guide

    Home Assistant is the operating system for smart home devices: it talks to hundreds of brands over local protocols (Zigbee, Z-Wave, MQTT, Wi-Fi) and keeps the automation logic on your own hardware instead of a cloud you do not control. It runs in one container, but unlike most services on this site it expects to reach your LAN directly — which changes how you set it up. This guide covers the Docker Compose install, the .env file that trips people up, adding devices, and keeping automations running.

    Intermediate · 12 min · Docker

    Why Home Assistant over a brand hub

    Brand hubs (Philips Hue app, Tuya, HomeKit) all share the same weaknesses: your automations live in their cloud, every device from a different vendor needs its own app, and when the cloud has a bad week your lights stop following your schedule. Home Assistant inverts that:

    • Local first. Zigbee, Z-Wave, Thread, MQTT and most Wi-Fi integrations talk straight to the radio or device. The cloud is optional.
    • One brain for all vendors. A Hue bridge, a Tuya plug (via the Tuya local integration), an ESPHome light and a Sonoff switch coexist in one UI, one automation engine, one voice interface.
    • Automations you can read. Every rule is a small YAML document on disk. No black-box “smart scenes” you cannot inspect.

    The honest caveat: Home Assistant is a platform, not a product. Setup takes longer than a brand app, and when something misbehaves the answer is “read the integration’s logs” rather than “call support”. For people who already run Docker for anything, that trade is worth it.

    Prerequisites

    • Docker + Compose plugin
    • A machine on your LAN with a stable IP (this guide assumes 8123 is free)
    • Optional: a Zigbee or Z-Wave USB stick if you have (or plan) radio-frequency devices

    Step 1: The compose file

    mkdir -p ~/stacks/homeassistant && cd ~/stacks/homeassistant

    Create docker-compose.yml:

    services:
      homeassistant:
        container_name: homeassistant
        image: ghcr.io/home-assistant/home-assistant:stable
        volumes:
          - ./config:/config
          - /etc/localtime:/etc/localtime:ro
        env_file: .env
        network_mode: host
        restart: unless-stopped
        # Uncomment for Zigbee (ZHA) or USB serial adapters:
        # devices:
        #   - /dev/ttyUSB0:/dev/ttyUSB0

    Two choices here are deliberate and worth understanding:

    • network_mode: host — Home Assistant needs to discover and reach many devices on your LAN (mDNS, Chromecast, bridges on fixed IPs). Container networking with published ports works for basic use but breaks a surprising number of integrations that rely on being a real LAN host. Host networking is the documented, lowest-friction option for the official image. The trade: it listens on all host interfaces, so keep it on a machine you trust on that LAN.
    • ./config:/config — your entire HA state lives here: automations, integrations, history (if you enable the SQL recorder), add-ons. This folder is what you back up and what makes a reinstall take minutes.

    Step 2: The .env file (the part that breaks installs)

    The official image requires a PASSWORD variable in an env file — and it must be at least 12 characters. If the file is missing or too short, the container starts and immediately exits, and the log will not obviously say why. Create .env next to the compose file:

    PASSWORD=change-me-to-at-least-12-chars

    Notes:

    • This password is not your web UI login password. It is an internal variable the image checks at startup; your actual account is created in the setup wizard. People conflate the two and end up “wrong password” loops.
    • Keep .env out of git and out of backups that leave the house (or encrypt them). The name is scary, but treat it like a credential anyway.
    • If you skip this file, docker compose up will fail with env file ... not found — that error is correct and is the feature working, not a bug.

    Step 3: Start it and run the wizard

    docker compose up -d
    docker compose logs -f homeassistant

    Wait for the line Home Assistant 2026.x.x, then open http://YOUR_SERVER_IP:8123. The onboarding wizard asks for your name, location (used for sunrise/sunset automations — keep it accurate, or set “precise location off” and enter coordinates manually if you prefer), and your account. That account is your administrator.

    Step 4: Add your first devices

    Go to Settings → Devices & Services → Add Integration. The practical starting points, in order of “works first try”:

    1. Hue Bridge — if you have any Philips lights, this is the gold standard: reliable, local, fast.
    2. ESPHome — if you ever build or buy flashed ESP devices. Once the device is flashed, HA’s ESPHome integration configures itself from the device.
    3. Matter — for new Matter-certified devices. HA acts as the Matter controller; the phone is just a commissioning remote.
    4. ZHA (Zigbee) — if you plug a Zigbee stick into the server, add the ZHA integration and pair from Settings → ZHA → Add Device. Remember to uncomment the devices: line in the compose file and restart before pairing.

    Each integration’s page lists its quirks (the Tuya local one, for example, needs your local credentials extracted from the device — the integration’s docs walk through it). When an integration “does not work”, the integration’s own documentation page is more current than any blog post; read the troubleshooting section before digging in logs.

    Step 5: Your first automation

    The classic starter, and the one that sells the platform:

    Lights in the hallway turn on at sunset when motion is detected, and turn off 5 minutes after the last motion.

    In Settings → Automations & Scenes → Create Automation, build it with the UI (no YAML needed): trigger Motion detected (your sensor), condition Sun has set, action Turn on light + Wait 5 minutes + Turn off light. Save it. It now runs entirely on your hardware, and you can open it any time to see exactly what it does.

    Once you trust it, add the one that changes your life: When everyone’s phone leaves the home Wi-Fi for the night, arm the lights/locks scenario. Phone presence detection works out of the box via Wi-Fi, no extra hardware.

    Keeping it reliable

    • Enable the recorder (Settings → Dashboard → recorder) if you want graphs and history. It adds a SQLite database to ./config; a NAS-backed volume or Postgres if you want it serious.
    • Back up ./config weekly — it is small (tens to low hundreds of MB) and contains your entire setup. A restic job pointed at MinIO is one command.
    • Update deliberately. HA ships a new release every two weeks. The container image is :stable, so docker compose pull && docker compose up -d is the upgrade path, but do it on a weekend, not a Tuesday — integrations occasionally break, and the fix is usually the next patch or a line in your automation.

    Access from outside

    Home Assistant’s own docs point at Nabu Casa — a managed, paid remote-access service — but the free, local-first equivalent is a tunnel (Cloudflare Tunnel, covered in the Security & Networking series) that gives you ha.example.com with zero open ports and full TLS. Pair it with a strong password and (ideally) MFA on the HA account, and you can check the house from anywhere without exposing port 8123.

    Resource usage (measured)

    StateRAM
    Idle, ~20 entities~300–400 MiB
    Recorder enabled, ~100 entities~500–700 MiB
    Active Zigbee network, ~200 entities~700 MiB–1 GiB

    It is a Python app with a database in its pocket — it does not run on 512 MB, and a 2 GB machine is the realistic minimum if you enable the recorder. A Pi 5 with 4–8 GB handles a real household comfortably.

    Updating

    docker compose pull && docker compose up -d

    FAQ

    Can I run it without host networking?

    Yes, for a simple setup: publish 8123:8123 and skip network_mode: host. You will lose some discovery-based integrations and may need to add static device entries by IP. If everything you run is a bridge (Hue) or a cloud integration, bridge mode is fine. The moment you add Zigbee radios, Chromecasts, or mDNS-dependent devices, host networking is the path of least resistance.

    What if a device only works through its cloud (Tuya, some Wi-Fi plugs)?

    Often there is a local integration (Tuya Local, Shelly, ESPHome flash) that takes the cloud out of the loop. The community is aggressively building these out. If an integration is cloud-only, your automation will depend on that vendor’s cloud — accept it knowingly or choose a device with a local option.

    Does the UI work on my phone without the app?

    The web UI is a full PWA; add it to your home screen and it behaves like an app, including offline-ish caching. The official apps add voice (Assist) and notifications; the web UI is enough for control and automations.

    How is this different from HomeKit?

    HomeKit is a standard for Apple devices; Home Assistant is a platform that can speak to HomeKit (expose your HA devices to Apple Home) while also speaking to everything else. Running HA as the brain and letting Apple Home be one of its outputs is the common power-user topology.

    Where does this fit?

    Home Assistant is the “physical world” layer of a self-hosted home: Jellyfin for the TV, Navidrome for the speakers, and HA as the thing that knows when the sun set and the house is empty. All of them reachable from outside the house via a tunnel or Tailscale — see the Security & Networking series.

  • Immich in Docker: Self-Hosted Photo Library (Compose Guide)

    Immich in Docker: Self-Hosted Photo Library (Compose Guide)

    Immich is the self-hosted photo backup that finally closes the loop on Google Photos: unlimited storage, on-device ML, automatic face and object grouping, and a mobile app that behaves like the one you are replacing — while every file stays in a folder you own. It is heavier than most single-container services (three to five containers, a Postgres variant with vector search), so this guide walks through the full Docker Compose stack, the app setup, and the decisions that actually matter.

    Beginner · 10 min · Docker

    What Immich actually does

    • Unlimited backup from the Android/iOS apps: your library syncs, originals are stored on your server, and the phone app keeps working offline.
    • Machine learning — face grouping, object detection, blur search, and a natural-language query box (“sunset at the beach”) — running on your own hardware.
    • Standard files — everything lands in a plain upload/ folder. Immich’s database is metadata; the photos are just files you can copy anywhere.

    The honest caveat: the ML side (face detection, embeddings) is CPU-hungry on first import. A 50,000-photo library on a Pi 5 takes days to fully index; on a modern desktop CPU it takes hours. The backup and browsing parts work fine on a Pi from the first photo.

    Prerequisites

    • Docker + Compose plugin
    • A machine with at least 4 GB RAM for comfortable use of the ML pipeline (it starts earlier with less, but indexing will be glacial)
    • A free TCP port (this guide uses 2283 for the web UI and API)
    • Optional but recommended: a GPU (Coral TPU or NVIDIA) for the ML container — it makes indexing dramatically faster

    Step 1: The compose file

    Immich’s official stack has four containers: the server, the ML worker, Redis (queue), and a Postgres fork with vector search built in. The full file:

    services:
      immich:
        image: ghcr.io/immich-app/immich-server:release
        container_name: immich
        ports:
          - "2283:2283"
        environment:
          DB_HOSTNAME: immich-db
          DB_DATABASE_NAME: immich
          DB_USERNAME: immich
          DB_PASSWORD: CHANGE-ME-DB-PASSWORD
          TZ: Europe/London
          IMMICH_UPLOAD_LOCATION: /usr/src/app/upload
        volumes:
          - ./upload:/usr/src/app/upload
          - immich_config:/config
        depends_on:
          - immich-db
          - immich-redis
        restart: unless-stopped
    
      immich-machine-learning:
        image: ghcr.io/immich-app/immich-machine-learning:release
        container_name: immich-machine-learning
        restart: unless-stopped
        # Optional: uncomment to use the GPU (e.g. Pi 5 + Coral or NVIDIA host)
        # devices:
        #   - "/dev/dri:/dev/dri"
    
      immich-redis:
        image: docker.io/redis:alpine
        container_name: immich-redis
        restart: unless-stopped
    
      immich-db:
        image: tensorchord/vectordb:pg16-v0.4.2-triton
        container_name: immich-db
        environment:
          POSTGRES_PASSWORD: CHANGE-ME-DB-PASSWORD
          POSTGRES_USER: immich
          POSTGRES_DB: immich
        volumes:
          - immich_db_data:/var/lib/postgresql/data
        restart: unless-stopped
    
    volumes:
      immich_db_data:
      immich_config:

    Notes on the non-obvious parts:

    • The database image is tensorchord/vectordb, not plain Postgres. It is a Postgres fork with the pgvector and pgvecto.rs extensions compiled in — Immich uses it for blur search and the embedding store. Do not “simplify” it to postgres:16; the server will refuse to start without the extensions.
    • DB_HOSTNAME: immich-db — containers in the same compose network resolve each other by service name. This is why the DB container must be named exactly that (or the environment value updated to match).
    • ./upload:/usr/src/app/upload — your actual photos live here, as plain files. This is the volume you back up.
    • immich_config:/config — the ML model cache. It fills on first run (hundreds of MB of models download once). Named volume, so it survives container rebuilds.

    Step 2: Start the stack

    docker compose up -d
    docker compose logs -f immich

    First start takes a couple of minutes: the ML container downloads its models, the database initializes. Then open http://YOUR_SERVER_IP:2283.

    Step 3: Create your account

    The first user you create becomes the administrator. Enter a name, email, and a strong password. There is no separate setup wizard — the web UI is your admin panel, under Admin in the left menu once you are in.

    Step 4: Connect the mobile apps

    Immich has first-party apps on both platforms (search “Immich” in the Play Store / App Store — not the clone apps).

    1. Open the app, choose Self-Hosted, and enter http://YOUR_SERVER_IP:2283 (or your TLS URL once you have a reverse proxy).
    2. Log in with the account you created.
    3. Grant photo library access. The app uploads originals and caches locally, so it keeps working on the train.

    Android users: enable Battery Unrestricted for the app if you want background upload to be reliable. This is the single most common “why is it not syncing” fix.

    Step 5: Let it catch up

    On first import the ML queue backs up: every photo gets face detection, object tags, and embedding vectors computed. Watch progress under Admin → System → Machine Learning. Two practical settings:

    • Limit concurrent ML jobs. Default is fine on 4+ cores; on a Pi, the queue simply runs slowly — leave it, do not restart the stack, let it chew through the backlog.
    • Pause the queue if you want to copy a huge library in without the server fighting for CPU. Resume when the transfer is done.

    Step 6: The features worth turning on

    1. Face grouping — automatic, no configuration. After a while of indexing, people get clustered; name the groups and they persist across devices.
    2. Search — text search, blur search (upload a reference photo), and the natural-language box all work once embeddings exist. Blur search is the one that reliably surprises people.
    3. Sharing — share albums or individual photos by link. Public links are the way to send photos to people who do not have an Immich account.
    4. Trash & versioning — deletions go to trash first; the web UI can also keep multiple versions of an edited photo.

    Access from outside your network

    Same two honest options as every other service on this site:

    • Tailscale — install it on the server and the phones. No open ports, encrypted mesh, works on mobile data. The lowest-risk path for a photo library.
    • Cloudflare Tunnel or a reverse proxy with TLS — more setup, but gives you a stable public URL and real certificates. We cover both in the Security & Networking series.

    Do not forward port 2283 on your router with the default setup. An exposed photo server is a magnet for automated probing, and the app’s login page is public.

    Resource usage (measured)

    StateRAM
    Idle (all containers up, no import)~1.2–1.6 GiB total
    Active import, 4-core desktop CPU~2.5 GiB, ML queue at full speed
    Pi 5, indexing only~1.5 GiB, queue crawling (hours per 1k photos)

    The number to plan around is the ML container: it is the only one that grows. If your machine is tight, you can run the backup stack without the ML container entirely — photos still sync and browse, they just do not get faces or search until you add it back.

    Updating

    docker compose pull && docker compose up -d

    Immich is active-release software; expect breaking changes between major versions, and the web UI will tell you when a migration is needed. Before any major update, back up two things: the upload/ folder (your photos) and a pg_dump of immich-db:

    docker compose exec immich-db pg_dump -U immich immich > immich-db-backup.sql

    FAQ

    Will it replace Google Photos without data loss?

    Yes, in the practical sense. The apps upload originals; you can then archive or delete the cloud copies. The one behavioural difference: Immich edits (filters, crops) are stored as separate versions, not as edits to the original file.

    Can I import an existing library from a folder?

    Yes. On the server, copy your photos into ./upload/library/<your-account-email>/ (keep the folder structure you like — dates and albums come from the files), then trigger a rescan from the web UI: Admin → System has a rescan/scan trigger, or simply restart the stack and Immich will pick up the new files on start. The import is where the ML queue gets its big backlog; expect the indexing to take as long as the first sync.

    Does it work with a NAS?

    Yes. Point the upload volume at a NAS share (e.g. /mnt/nas/photos:/usr/src/app/upload) and everything else is unchanged. Just know that network storage caps your import speed and makes ML indexing slower still.

    What about backups of Immich itself?

    Two artifacts: the upload/ folder and the database. Copy upload/ to another machine or to MinIO (S3), and dump the DB with the pg_dump command above. That is a complete, restorable backup.

    Where does this fit?

    Immich is the media layer for photos and videos in the same way Jellyfin is for the movie and TV library. Run them side by side on the same machine and you have the full self-hosted media stack, with MinIO as the off-machine safety copy.

    What’s next?

    The natural next steps from this guide:

  • MinIO in Docker: Self-Hosted S3 Object Storage (Compose Guide)

    MinIO in Docker: Self-Hosted S3 Object Storage (Compose Guide)

    MinIO is an S3-compatible object store you run in a single container: any app that speaks the S3 protocol — backups, media servers, databases, CI pipelines — can store and fetch files from it without changing a line of configuration. It runs in about 200 MB of RAM at idle, and a single-disk setup is enough for personal and small-team use. This guide covers the Docker Compose setup, first bucket, connecting clients, and the settings that matter.

    Beginner · 8 min · Docker

    Why you would want your own S3

    Three reasons come up constantly in self-hosted setups:

    • Backups need a second location. Restic, Borg, and Duplicacy all write natively to S3. A folder on the same machine is not a backup destination; an S3 bucket is at least a clean abstraction, and you can point it at a second machine later without touching your backup scripts.
    • Apps want S3, not folders. Nextcloud, some monitoring stacks, and a long tail of SaaS-style tools take an S3 endpoint, access key and secret key as configuration. MinIO gives them that endpoint on your own hardware.
    • Portability. Your data lives in a standard protocol. If you outgrow a single disk, you can move the bucket to a MinIO cluster or even to cloud S3 and most clients keep working.

    The honest caveat: single-node MinIO with one volume is not a replicated, self-healing storage system. It is a well-behaved S3 endpoint in front of your disk. Treat it as such: back the volume up like any other data directory.

    Prerequisites

    • Docker + Compose plugin
    • A free TCP port (this guide uses 9000 for the API and 9001 for the web console)
    • A folder for the data — put it on your largest disk; MinIO writes everything to it

    Step 1: The compose file

    mkdir -p ~/stacks/minio && cd ~/stacks/minio

    Create docker-compose.yml:

    services:
      minio:
        image: minio/minio:latest
        container_name: minio
        command: server /data --console-address ":9001"
        environment:
          MINIO_ROOT_USER: chikewa
          MINIO_ROOT_PASSWORD: CHANGE-ME-16-CHARS
        ports:
          - "9000:9000"
          - "9001:9001"
        volumes:
          - ./data:/data
        restart: unless-stopped

    Three notes on that file:

    • command: server /data — the data directory is a single path. MinIO supports multiple drives per node (pass several paths, space-separated) for larger single-node deployments.
    • MINIO_ROOT_USER / MINIO_ROOT_PASSWORD — the root credentials. The password must be at least 8 characters; use a long random one. You can create additional users later with their own keys (see Step 4), which is the right way to hand out access to apps.
    • --console-address ":9001" — the web UI. If you would rather not expose the console at all, remove port 9001 from ports and use mc (the CLI) instead.

    Step 2: Start it and open the console

    docker compose up -d
    docker compose logs -f minio

    Wait for the log line API: http://192.168.x.x:9000, then open http://YOUR_SERVER_IP:9001 and log in with the root credentials. The console shows buckets, usage, and a simple file browser — enough for day-to-day checking.

    Step 3: Create a bucket

    In the console, click Add Bucket, pick a name (lowercase, no spaces, e.g. backups), and leave the defaults. That is it — no ACL gymnastics needed for a private bucket. MinIO buckets are private by default; access is controlled by credentials, not by open listing.

    Step 4: Create a dedicated user for apps

    Do not hand the root keys to your backup job. In the console go to Access Keys → Add User (or use mc), create a user, then create an access key for it. Attach a policy that limits it to the bucket it needs:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject"],
          "Resource": [
            "arn:aws:s3:::backups",
            "arn:aws:s3:::backups/*"
          ]
        }
      ]
    }

    Save that as a custom policy and attach it to the user. Now every app on your network uses its own key, and you can revoke one without touching the others.

    Step 5: Connect a client

    Any S3 client works. Three examples you will actually use:

    mc (MinIO CLI) — the official client, one command to configure:

    mc alias set myminio http://YOUR_SERVER_IP:9000 ACCESS_KEY SECRET_KEY
    mc mb myminio/backups
    mc cp big-file.iso myminio/backups/

    restic — backup to your own S3:

    restic init --repository s3:http://YOUR_SERVER_IP:9000/backups \
      --s3-provider minio --s3-access-key ACCESS_KEY --s3-secret-key SECRET_KEY \
      --s3-region us-east-1 --s3-no-verify-ssl
    restic -r s3:http://YOUR_SERVER_IP:9000/backups backup /home

    The us-east-1 region value is a dummy: MinIO accepts any region string, and --s3-no-verify-ssl is only needed while you are on plain HTTP inside your LAN. Once you put TLS in front (see below), drop that flag.

    aws CLI — if an app or script demands it:

    aws --endpoint-url http://YOUR_SERVER_IP:9000 s3 ls
    aws --endpoint-url http://YOUR_SERVER_IP:9000 s3 cp report.pdf s3://backups/

    Step 6: TLS and external access

    MinIO ships with a self-signed certificate automatically; clients can use HTTPS against it with no-verify, which is fine for testing but not for anything you leave on. The two clean paths, in order of preference:

    • Cloudflare Tunnel or Tailscale Funnel in front of MinIO — no open ports, TLS handled for you. We cover Cloudflare Tunnel in the Security & Networking series.
    • Reverse proxy (Caddy / Nginx Proxy Manager) with a real certificate — the same foundation you would use for any other exposed service.

    Do not forward port 9000 directly on your router. An S3 endpoint with a weak key is exactly the thing credential-stuffing bots look for.

    Resource usage (measured)

    From a single-disk, single-bucket setup on a Pi 5-class machine:

    StateRAM
    Idle~180–220 MiB
    Steady single-stream copy (1 GbE)~250 MiB, disk-bound at ~100 MB/s

    Throughput is your disk, not MinIO. A SATA SSD will saturate a gigabit line easily; a mechanical disk will not. If you need more, the single-node multi-drive setup (multiple paths in command) stripes across them.

    Updating

    docker compose pull && docker compose up -d

    MinIO stores its metadata inside the data volume; there is no separate database to migrate. Your buckets and objects are untouched by updates.

    FAQ

    Can I run MinIO on a Raspberry Pi?

    Yes, with the usual caveat that a Pi’s storage is the bottleneck. It is a perfectly good S3 endpoint for backups, metadata-heavy workloads, and small files. For large video libraries, put the data on a NAS-class machine and let the Pi run the apps that consume it.

    What happens to my data if the container breaks?

    Everything is plain files under ./data. You can copy that folder to another machine, point a fresh MinIO at it, and serve the same objects. That is the real safety property of single-node MinIO: the data is not locked in a proprietary format.

    Do I need versioning?

    Turn it on (console → bucket → versioning) if the bucket holds anything you do not want an accidental delete to destroy — backup repositories, irreplaceable originals. It costs you storage equal to whatever you overwrite.

    How is this different from Samba or a plain folder?

    A folder gives you a filesystem; S3 gives you an API. Anything that needs to store objects programmatically — backup software, web apps, pipelines — speaks S3, not CIFS. MinIO is the cheapest way to get that API on hardware you own.

    Where does this fit?

    MinIO is the storage layer for the rest of the stack: Miniflux keeps its database tiny, Jellyfin keeps its media in a folder, and MinIO is where the copies that survive a disk failure live. See the starter guide for the full picture.

  • Jellyfin in Docker: A Self-Hosted Media Server for Movies and TV

    Jellyfin in Docker: A Self-Hosted Media Server for Movies and TV

    Jellyfin is the self-hosted answer to Netflix and Plex: an open-source media server that plays your own movies and TV from your own disk, with no subscription and no upload limits. It is also one of the most misunderstood services in the self-hosting world, because the difference between a smooth setup and a constant transcoding fight comes down to a few decisions you make before the first movie. This guide walks through the Docker Compose setup we run in the Chikewa lab, what the hardware actually has to do, and the configuration choices that matter.

    Beginner · 12 min · Docker

    What is Jellyfin?

    Jellyfin streams video, music, photos and podcasts from folders on your server to any device on your network or on the internet. It was forked from the old free version of Plex in 2018, and unlike Plex it is fully open source: no premium tier, no server-side limits, no account required. The server does the heavy lifting (metadata scraping, transcoding, photo optimization) and thin clients on phones, TVs, browsers and media players do the playback.

    Two properties make it a good first “big” self-hosted service. First, it is a single container with no database dependency: the only thing you point it at is a folder of media files. Second, its default configuration is genuinely reasonable, which means the gap between “it started” and “it is actually usable” is small. The things that do trip people up are documented below, because they tripped us.

    Requirements

    Anything that runs Docker runs Jellyfin. The realistic floor is the same as the rest of the stack: 2 GB of RAM and a few hundred MB of disk for the configuration database. The real constraint is not starting the server, it is transcoding. If a client asks for a format the hardware cannot decode natively, Jellyfin re-encodes it on the CPU, and that is where underpowered machines start stuttering.

    Our lab machine is a 4-core i5-6500T with 16 GB of RAM and an NVMe disk. It idles Jellyfin at around 240 MiB of RAM and can comfortably handle direct play for the family and one light transcode at a time. If you are buying hardware specifically for a media server, read our hardware guide before you buy.

    The Compose File

    The complete file, exactly as it runs in the lab:

    services:
      jellyfin:
        image: jellyfin/jellyfin:latest
        container_name: jellyfin
        ports:
          - "127.0.0.1:8096:8096"
        volumes:
          - jellyfin-config:/config
          - ./media:/media
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=Europe/London
        restart: unless-stopped
    
    volumes:
      jellyfin-config:
    

    Three decisions in this file are worth understanding.

    The port binding

    Notice the port is published as 127.0.0.1:8096:8096, not 8096:8096. That single change makes the difference between “my media server is reachable from the living room” and “my media server is reachable from the entire internet”. Bound to loopback, Jellyfin answers only on the host itself; you reach it from other devices through a reverse proxy or a private network, and we cover both in our security guide. We verified with ss -tlnp that the socket listens on 127.0.0.1:8096 only.

    The volumes

    Two things need to survive container recreation. jellyfin-config is a named volume holding the SQLite database, plugin state and user settings — losing it means re-creating users and re-scanning the library. ./media is your actual movie and TV folder, mounted read-only in spirit (Jellyfin never needs to write to your media, only read it). Keep media on the fastest disk you have: scan times and seek performance for random playback both depend on it.

    The environment variables

    PUID and PGID make the container run as your regular user instead of root, which matters if you ever mount media from a share with restrictive permissions. TZ keeps the activity log and the trickplay schedule sane. Nothing else is required.

    First Run

    Start it and watch the logs:

    docker compose up -d
    docker compose logs -f jellyfin
    

    The first boot takes noticeably longer than the other services in this series. On our machine the image pull, the database migrations and the plugin load completed in under a minute, and the log ended with Core startup complete. The health endpoint is a good objective check:

    curl http://localhost:8096/health
    # Healthy
    

    Open http://localhost:8096 (or through your proxy) and create the admin account. The setup wizard then asks where your media lives: the path is /media, because that is the mount point inside the container, not the host path. This is the most common first-run mistake — entering the host path makes Jellyfin scan an empty directory and you get a server that runs perfectly with zero content.

    Library Setup

    Add your first library, choose “TV Shows” or “Movies”, point it at the right subfolder of /media and let it scan. Jellyfin pulls metadata from TMDb by default, which works well for English content; the first scan of a medium library (a few hundred titles) took a few minutes in our test, downloading posters and fan art for everything.

    Two settings pay for themselves quickly. Under the library, enable save image assets to the content folder if you want the metadata to survive a config loss. And check realtime monitoring so new files are picked up without a manual rescan.

    Hardware Transcoding

    Direct play means the client decodes the file as-is: cheap, fast, and what you want 95% of the time. Transcoding happens when a client cannot play the source format. Our lab image ships with ffmpeg 7.1.4, and the encoder list includes h264_qsv, av1_qsv and hevc_qsv — Intel Quick Sync. The image also bundles the i965 driver, and the host exposes /dev/dri when the CPU has an Intel iGPU, so Quick Sync transcoding is available out of the box on most mini PC hardware.

    To use it, pass the device through and enable hardware transcoding in the admin dashboard (Playback → Transcoding). The compose addition is:

        devices:
          - /dev/dri:/dev/dri
    

    Without the iGPU, transcoding falls back to the CPU, which on a 4-core i5 handles a single 1080p encode but will struggle with several. If transcoding quality matters to you, that is a hardware conversation, not a configuration one.

    A pitfall we hit: you cannot exec ffmpeg

    Our first attempt to test the transcoder was docker exec jellyfin ffmpeg -hwaccels, which failed confusingly. The Jellyfin entrypoint intercepts every argument and hands it to the .NET server, so arbitrary commands never reach a shell. To inspect the bundled ffmpeg you need a separate container from the same image, or check the running server’s logs, which print the full encoder and hwaccel list at startup. The list we captured is in our hardware guide, if you want to compare.

    First Login and Daily Use

    Once the library is scanned, add a user per household member (the admin account is a fine user too, but separate users keep watch states and parental controls clean). Install a client: the browser works everywhere, the Android and iOS apps are good, and most smart TVs either run a native app or play through a browser. From the TV we verified direct play of 1080p MKV without a single transcode, which is the whole point.

    Remote access is the next natural step. Because we bound the port to loopback, the two clean options are a reverse proxy with authentication or a private network like Tailscale; the security guide covers the decision. Do not solve “I want to watch at my parents’ house” by republishing port 8096 on 0.0.0.0.

    Updating

    Jellyfin images move fast. The safe update:

    docker compose pull
    docker compose up -d
    docker compose logs -f jellyfin   # watch for "Core startup complete"
    

    Configuration and the database live in the named volume, so updates are non-destructive. One thing to know: after a major version bump the logs show a batch of Entity Framework migration warnings on the first boot. In our test run they appeared, the migrations applied, and the server came up cleanly. They look alarming and are cosmetic; what you should actually watch for is a missing Core startup complete.

    Troubleshooting

    The server runs but the library is empty

    Nine times out of ten this is the host-path-instead-of-container-path mistake from the first run. The media folder inside the container is /media. Check the library path in the admin dashboard, not the compose file.

    Playback stutters on one device only

    That device is transcoding when it should direct play. Open the activity log during playback: it records every transcode with the codec and resolution. If you see transcodes for a format your client should support, the file’s codec or profile is outside the client’s native range — re-encode that file, or accept the transcode.

    High CPU during scans

    Large initial scans and photo optimization are CPU-heavy by design and settle down. If CPU stays high with no scans running, check the trickplay generation schedule (it runs daily and is optional) and the number of concurrent transcodes in the playback settings.

    Resource Usage

    Measured in the lab, steady state with a 200-title library and no active playback: 236 MiB RAM, negligible CPU. The disk footprint of the config volume is tens of MB; the media is, of course, your own. Add roughly 0.5–1 GB per active hardware transcode, more for CPU transcoding.

    FAQ

    Is Jellyfin legal?

    Jellyfin is a legal, open-source application. What you put on the server is your responsibility, exactly as with any storage device you own.

    Can it replace Plex for a household?

    For the core job — stream my library to my devices — yes, and without a premium subscription. You give up some of Plex’ polished remote streaming and its ecosystem of third-party integrations. For most home setups that trade is a win.

    What about music?

    Jellyfin plays music, but if music is the priority, a dedicated server like Navidrome uses less RAM and has a better mobile experience. We run both in the lab.

    Tested on:

    OSDebian 12
    Docker29.7.2
    Hardware4-core i5-6500T / 16 GB RAM / NVMe
    SoftwareJellyfin 10.11.11 (ffmpeg 7.1.4)

    Last tested: 23 August 2026

  • DokuWiki in Docker: A Private Wiki on Plain-Text Files (Compose Guide)

    DokuWiki in Docker: A Private Wiki on Plain-Text Files (Compose Guide)

    Beginner · 7 min · Docker · Wiki

    Tested on:

    OS Any Linux (verified on Debian 12)
    Docker 29.7
    Hardware 4-core x86, 16 GB RAM
    Software DokuWiki (stable)

    Last tested: 22 August 2026

    DokuWiki is a PHP wiki that stores every page as a plain text file on disk — no proprietary database format, no lock-in, trivially backed up by copying a folder. In Docker it runs in one container with about 25 MB of RAM at idle and a five-minute setup. This guide walks through the compose file, the first-run wizard, and the settings worth changing.

    Why DokuWiki in 2026

    Self-hosted wiki options fall into two camps. The heavy ones (MediaWiki, BookStack, Outline) are powerful but expect you to configure users, groups, search backends, and plugins before you write a single page. DokuWiki is the deliberate opposite: it is the original “no database, no fuss” wiki, and its defining property is still its best one — your entire wiki is a directory of text files.

    DokuWiki BookStack Outline
    Storage format Plain text files MySQL/MariaDB PostgreSQL
    Setup time ~5 min ~15 min ~15 min + auth
    Idle RAM (measured) ~25 MiB ~100 MiB+ ~200 MiB+
    Backup Copy a folder DB dump + uploads DB dump + uploads
    Best for Personal/family notes, documentation Team knowledge bases Polished team docs

    If you need roles, SSO, and a polished SaaS look for a team, BookStack or Outline are the better tools. For personal notes, a household wiki, or a documentation home that must survive for a decade, DokuWiki’s plain-text core is the safer bet: any editor can open the files, and they render correctly with any markdown-capable tool if you ever leave.

    Prerequisites

    • Docker + Compose plugin
    • A free TCP port (this guide uses 8081)

    Step 1: The compose file

    One important gotcha up front: the community image name on Docker Hub has moved over the years. The current official image is dokuwiki/dokuwiki, and the tag to pin is stable (the dokuwiki:dokuwiki-2024 tag you will see in older tutorials no longer pulls — we hit exactly that during testing and it cost a pull error). Use this:

    mkdir -p ~/stacks/dokuwiki && cd ~/stacks/dokuwiki
    services:
      dokuwiki:
        image: dokuwiki/dokuwiki:stable
        container_name: dokuwiki
        ports:
          - "8081:80"
        volumes:
          - dokuwiki_data:/dokuwiki/data
          - dokuwiki_conf:/dokuwiki/conf
        restart: unless-stopped
    
    volumes:
      dokuwiki_data:
      dokuwiki_conf:

    Why two volumes: data holds your pages (the plain-text files) and attachments; conf holds the configuration that the first-run wizard writes. Keeping both as named volumes means an image update never touches your content, and you can back up the wiki with two docker cp calls or a bind mount if you prefer to see the files on disk.

    Step 2: Start and run the wizard

    docker compose up -d
    docker compose ps

    The official image includes a healthcheck — you will see healthy after a few seconds, which is a nice confirmation the web server is actually serving. Open http://YOUR_SERVER_IP:8081.

    First visit runs the setup wizard: it asks for an admin login and password, the language, and the site title. That is the entire configuration. After the wizard, conf/ contains a local.php with those choices — which is also why the conf volume must persist across updates.

    Step 3: The editor and page syntax

    DokuWiki pages use its own lightweight syntax (a structured subset of markdown):

    • == Heading == and === Sub-heading ===
    • * bullet and # numbered
    • [[namespace:page]] for internal links — creating the link also creates the page skeleton
    • ---- for a horizontal rule
    • Tables, code blocks (<<<code>>>), and images have short, regular forms

    The namespace system is the feature to understand: pages live in namespace:page, which maps to directories on disk. A “Projects” section with “Server” and “Network” pages is simply projects:server and projects:network. You can restructure the whole wiki by moving folders — the links update because they are path-based.

    Step 4: The settings worth changing

    1. Authentication. The default is the internal user store, which is correct for a LAN wiki. Do not expose DokuWiki to the internet without putting it behind an auth layer (reverse proxy or Tailscale) — see the Security & Networking series.
    2. Revisions and diffs. On by default, and the single best feature for notes: every save is a versioned revision you can diff and revert. Keep it on.
    3. Search.

      The built-in full-text index is fine up to a few thousand pages. Beyond that, add a dedicated search backend — but most personal wikis never need it.

    4. Media uploads. Allowed by default for logged-in users. Restrict who can upload if you run a multi-user household wiki.
    5. Timezone and date format — trivial, but set once so revision history reads sensibly.

    Step 5: Backing up a plain-text wiki

    This is where the architecture pays off. A complete backup is:

    docker run --rm -v dokuwiki_data:/data -v dokuwiki_conf:/conf \
      -v ~/backups:/backup alpine tar czf /backup/dokuwiki-$(date +%F).tar.gz \
      --transform 's,^,dokuwiki/,' /data /conf

    Run it weekly from cron. Restore is the inverse: extract into the volumes (or point a fresh container at the extracted folders). No database dump, no export format, no version compatibility matrix between wiki releases. Compare that to a database-backed wiki, where a failed restore means reconstructing a schema.

    Resource usage (measured)

    State RAM
    Idle 25 MiB
    Editing a page ~30–40 MiB

    From the same verified stack as our starter guide. It is the lightest of the three starter services.

    Updating

    docker compose pull && docker compose up -d

    DokuWiki ships a built-in upgrade routine that runs on start when a new version is detected; the wizard’s configuration in conf survives untouched.

    FAQ

    Can I import existing markdown or org-mode notes?

    Yes, with the import plugins (Markdown, reStructuredText, and others) or by pasting — DokuWiki converts on save. For a one-off migration of a large tree, converting to DokuWiki syntax with a script and dropping the files into the data volume works too, since the format is predictable.

    Does it work offline / without internet?

    Completely. No phoning home, no external fonts required, no analytics. It is one of the few web apps that is genuinely self-contained.

    Multiple users?

    Yes — create users in the admin panel, and groups let you control who can edit which namespaces. A household “shared notes” wiki with per-person namespaces is a common setup.

    Where does this fit?

    DokuWiki is the third service in our self-hosting starter guide, alongside Miniflux and Navidrome. If your notes grow into a team knowledge base, the NAS & Media and future “team tools” guides cover the heavier options.

  • Navidrome in Docker: A Self-Hosted Alternative to Spotify (Compose Guide)

    Navidrome in Docker: A Self-Hosted Alternative to Spotify (Compose Guide)

    Beginner · 7 min · Docker · Music

    Tested on:

    OS Any Linux (verified on Debian 12)
    Docker 29.7
    Hardware 4-core x86, 16 GB RAM (a 2 GB Raspberry Pi 5 runs it fine)
    Software Navidrome (latest, deluan image)

    Last tested: 22 August 2026

    Navidrome is the self-hosted music server that actually replaces Spotify for people who already own their music: it reads a folder of MP3s or FLACs, serves them to any device over a fast web interface, and runs in a single container with about 26 MB of RAM at idle. This guide covers the Docker Compose setup, the official mobile apps, and the settings that matter.

    Why Navidrome over the alternatives

    There are a few options for streaming your own library. Here is how they compare on the axes that actually matter day to day:

    Navidrome Jellyfin (audio) Coherence
    Setup effort 1 container, 1 folder 1 container, more config 1 container + DLNA client
    Idle RAM (measured) ~26 MiB ~300 MiB+ (multi-service) ~40 MiB
    Mobile apps Official (Substreamer) + many Official app Client apps only
    Transcoding On the fly, fast (Go) Yes, heavier Limited
    Video No — audio only Yes Yes (DLNA)

    The honest rule: if you want video and audio in one stack, use Jellyfin (we cover it in the NAS & Media series). If you only need music, Navidrome is lighter, faster, and its mobile app experience is the best of the group.

    Prerequisites

    • Docker + Compose plugin
    • Your music in one folder (MP3, FLAC, OGG, M4A, OPUS, WAV, WMA all work). Keep the folder structure you like — Navidrome reads artist/album metadata from the files, so filenames and folders mostly do not matter for playback, only for how the UI groups things when tags are missing.
    • A free TCP port (this guide uses 4533, the default)

    Step 1: The compose file

    mkdir -p ~/stacks/navidrome/music && cd ~/stacks/navidrome

    Create docker-compose.yml:

    services:
      navidrome:
        image: deluan/navidrome:latest
        container_name: navidrome
        ports:
          - "4533:4533"
        environment:
          ND_SCANSCHEDULE: 1h
          ND_LOGLEVEL: info
        volumes:
          - ./music:/music:ro
          - navidrome_data:/data
        restart: unless-stopped
    
    volumes:
      navidrome_data:

    Notes on the three choices in that file:

    • ./music:/music:ro — your library is mounted read-only. Navidrome never needs to write to your music; keeping it read-only is free safety.
    • ND_SCANSCHEDULE: 1h — re-scan the library hourly, so new files appear without action. Set to never if your library rarely changes and you want zero background work.
    • navidrome_data:/data — the internal database (play counts, playlists, users) lives in a volume, so updates and container rebuilds never touch it.

    Step 2: Start it and load the library

    docker compose up -d
    docker compose logs -f navidrome

    You will see it scan the folder on first start — one line per track, and the time depends on library size (a few thousand tracks take a couple of minutes; 50,000 can take ten or more). Stop following the log with Ctrl-C once it finishes.

    Open http://YOUR_SERVER_IP:4533. First visit asks you to create the first user, which becomes the administrator. Log in and your library is there, grouped by artist and album, with artwork pulled from embedded tags or Navidrome’s own art fetching.

    Step 3: Connect the official mobile app

    The official client is called Substreamer (Android and iOS). Setup takes about a minute:

    1. Install Substreamer from your app store.
    2. Add a server: http://YOUR_SERVER_IP:4533 (or the public URL once you have a reverse proxy — see below), then your username and password.
    3. It fetches the library and behaves like a normal music app: browse, search, playlists, gapless playback, background play.

    Non-official clients also work well: the community Navidrome clients on both platforms, and any app that speaks the Subsonic API. Navidrome deliberately implements the Subsonic protocol, which is why so many third-party apps just work.

    Step 4: The settings worth changing

    Most of Navidrome works well untouched, but these five are worth a look in Settings → Server and Settings → Player:

    1. Transcoding. On by default: if a client requests 128 kbps MP3, Navidrome transcodes FLAC on the fly. That is the right default for phones on data. On a fast LAN you can raise the quality or let the client request the original format.
    2. Cover art source. Navidrome can fetch missing artwork from the web. Fine for MP3s with weak tags; for a carefully tagged FLAC library, keep it off to avoid wrong art being cached.
    3. Session timeout. Defaults are generous; tighten if you ever expose the UI publicly.
    4. Playlist sharing. Users can share playlists with each other — useful if you run this for a household.
    5. Play counts and “last played.” On by default, and the data powers the “recently played” views in the app. Nothing to configure, just know it exists.

    Step 5: Reaching it from outside your network

    Two honest options:

    • Tailscale (our recommendation): install Tailscale on the server and on your phone. The server gets a stable address inside your private mesh, no ports opened on the router, traffic encrypted end to end. This is the lowest-risk way to listen to your library on the train.
    • Reverse proxy with TLS: Caddy or Nginx Proxy Manager in front of Navidrome, with a domain name. More setup, but it also serves as the foundation for every other service you expose later. We cover both paths in the Security & Networking series.

    Do not forward port 4533 directly on your router. An exposed music server with a weak password is a classic credential-stuffing target.

    Resource usage (measured)

    From the same verified stack as our starter guide:

    State RAM
    Idle (no streams) 26 MiB
    One stream, FLAC ~40–60 MiB

    A 2 GB Pi 5 can comfortably run Navidrome plus several other services. Transcoding is the only CPU-heavy operation, and it is per-stream, so a single listener on a small machine is not a problem.

    Updating

    docker compose pull && docker compose up -d

    Navidrome runs a quick upgrade/migration on start. Your library is untouched (it is just a folder); your database lives in the volume.

    FAQ

    Will it stream lossless over my home network?

    Yes. Over a LAN the app can play original FLAC files directly. Transcoding only kicks in when a client requests a lower format (typically data connections or older devices).

    Does it support gapless playback?

    The web interface does not do gapless playback; Substreamer does, which is why the app is the recommended client for classical or concept albums.

    Can I add podcasts?

    No — Navidrome is music only. Pair it with an RSS reader like Miniflux and you have a complete, private media stack.

    What if my tags are a mess?

    Fix the tags in the files (Beets, Kid3, or MusicBrainz Picard), then force a rescan from the admin panel. Navidrome does not write tags back to your files, so cleanup tools are safe to run at any time.

    Where does this fit?

    Navidrome is one of the three services in our self-hosting starter guide. For video, see the Jellyfin guide in the NAS & Media section (in the pipeline).

  • How to Self-Host Miniflux with Docker: Compose File and the Postgres SSL Fix

    How to Self-Host Miniflux with Docker: Compose File and the Postgres SSL Fix

    Beginner · 7 min · Docker · PostgreSQL

    Tested on:

    OS Any Linux (verified on Debian 12)
    Docker 29.7
    Hardware 4-core x86, 16 GB RAM
    Software Miniflux (latest) + PostgreSQL 16

    Last tested: 22 August 2026

    Miniflux is the fastest, lightest RSS reader you can run for yourself: a single Go binary plus a database, idling at about 17 MB of RAM. This guide deploys it with Docker Compose on any Linux machine — and documents the exact error that stops most first-timers, because that is the error we hit too.

    What is Miniflux, and why it beats the alternatives

    Miniflux is a minimalist RSS reader written in Go. You point it at your feeds, and it polls, stores, and serves them in a fast interface. The account model is deliberately simple: create users, add feeds, read. No accounts, no cloud, no subscription.

    Compared to the other common self-hosted readers:

    Miniflux FreshRSS Selfoss
    Idle RAM (measured) ~17 MiB ~80–150 MiB (PHP-FPM) ~50 MiB
    Setup complexity 1 container + Postgres 1 container (PHP) + optional DB 1 container + DB
    Mobile clients Any RSS app + built-in mobile view Any RSS app Limited
    Update cadence Frequent, stable Frequent Slower

    If you want a heavy-featured reader with plugins, FreshRSS is a fine choice. For a low-maintenance daily driver, Miniflux is hard to beat.

    Prerequisites

    • A machine with Docker and the Compose plugin (docker compose version should print a version)
    • A free TCP port on your LAN (this guide uses 8082 — change it if yours is taken)

    Step 1: The compose file

    mkdir -p ~/stacks/miniflux && cd ~/stacks/miniflux

    Create docker-compose.yml:

    services:
      miniflux:
        image: miniflux/miniflux:latest
        container_name: miniflux
        environment:
          DATABASE_URL: postgres://miniflux:secret@miniflux-db/miniflux?sslmode=disable
          BASE_URL: http://localhost:8082
        ports:
          - "8082:8080"
        depends_on:
          - miniflux-db
        restart: unless-stopped
    
      miniflux-db:
        image: postgres:16-alpine
        container_name: miniflux-db
        environment:
          POSTGRES_USER: miniflux
          POSTGRES_PASSWORD: secret
          POSTGRES_DB: miniflux
        volumes:
          - miniflux_db:/var/lib/postgresql/data
        restart: unless-stopped
    
    volumes:
      miniflux_db:

    Three things in that file matter, and two of them trip people up:

    1. ?sslmode=disable at the end of the DATABASE_URL — the current image attempts an SSL connection by default. Without this parameter the container enters a restart loop (see Step 3).
    2. secret as the password is fine for a LAN-only deployment, but if this server ever reaches the internet behind a proxy, change it to something real.
    3. BASE_URL is used for redirects and feed links. Set it to whatever address you will actually type in the browser.

    Step 2: Start it

    docker compose up -d
    docker compose ps

    Both containers should show Up. Wait ten seconds, then open http://YOUR_SERVER_IP:8082 (or http://localhost:8082 if you are on the same machine).

    Step 3: The restart loop (and the fix)

    If instead you see Restarting in docker compose ps, check the logs:

    docker logs miniflux

    If the output repeats pq: SSL is not enabled on the server, the container is talking to Postgres with SSL on and Postgres has it off. The fix is the ?sslmode=disable parameter on the DATABASE_URL line, which the compose file above already includes. If you are reading this guide because of that error, that one parameter is the fix.

    Edge case: schema version mismatch

    A second, rarer message is the database schema is not up to date: current=v0 expected=vNNN. This happens when a fresh database is created but the first container run aborts before migrations. Running the image once with migrations forced clears it:

    docker run --rm --network miniflux_default \
      -e DATABASE_URL="postgres://miniflux:secret@miniflux-db/miniflux?sslmode=disable" \
      -e RUN_MIGRATIONS=true miniflux/miniflux:latest

    That command runs the migrations and then starts the server (leave it running until the DB is migrated, or stop it after a few seconds) — after which the compose-managed container starts cleanly. (We hit this during testing; it is not part of the normal path, but it is the other error that appears in every Miniflux+Postgres thread.)

    Step 4: First-run setup

    The first visit shows the setup page. Create your admin account — Miniflux generates an initial password it shows you once (or sets one you choose, depending on version).

    Then:

    1. Add your first feed: paste an RSS URL into the “Add feed” field. If a site has no visible RSS link, try appending /feed, /rss, or /atom.xml to its URL, or use a feed-directory site to find it.
    2. Add the 10–15 feeds you actually read. Resist adding 200 — a full feed list you never finish is the same as not reading.
    3. Check “Polling interval” in settings: the default is hourly, which is plenty. Faster polling on a small machine just costs CPU for no reading benefit.

    Step 5: Use it from your phone

    Miniflux speaks the standard /api/v1 RSS reader API. Any of these clients work:

    • Reeder (iOS/macOS) — connect to your server URL, use the API
    • NetNewsWire (macOS/iOS) — same
    • Fluss (Android) — the most polished free option on Android
    • The built-in mobile view — Miniflux serves a decent mobile UI at /m, which is honestly good enough for daily use

    Resource usage (measured)

    Container Idle RAM Notes
    miniflux 17.2 MiB Go binary, one process
    postgres:16-alpine 37.6 MiB Alpine image keeps it small

    Both together: under 55 MiB at idle. A 1 GB Raspberry Pi can run this and still have room for a few more services.

    Updating

    Miniflux migrates its own schema on startup, so updating is:

    docker compose pull
    docker compose up -d

    Your data lives in the miniflux_db volume and survives every update. Back it up with docker run --rm -v miniflux_db:/data miniflux/miniflux:latest psql ... or, more simply, docker exec miniflux-db pg_dumpall > backup.sql on a cron schedule. (We have a dedicated backups guide in the pipeline.)

    FAQ

    Does Miniflux work without Postgres?

    It also supports SQLite and MySQL, but Postgres is the recommended production database and the one used here. SQLite is fine for a single-user test, but the image’s default expectations are Postgres-centric.

    Can I add multiple users?

    Yes — from the admin panel. Each user gets their own feeds and reading state, which makes Miniflux work as a small family reader.

    Should I expose port 8082 to the internet?

    No. Keep it on the LAN and reach it remotely via Tailscale or a reverse proxy with authentication — see the Security & Networking guides. RSS readers are a magnet for credential-stuffing bots the moment they are publicly reachable.

    Where does this fit in a bigger setup?

    In our self-hosting starter guide, Miniflux is one of the three first services, alongside Navidrome and DokuWiki.

  • The Self-Hosting Starter Guide: From Zero to a Working Home Server

    The Self-Hosting Starter Guide: From Zero to a Working Home Server

    Beginner · 10 min · Linux · Docker

    Tested on:

    OS Ubuntu 24.04 LTS (Debian 12 works too)
    Docker 29
    Hardware 4-core x86, 16 GB RAM
    Software Starter stack (Miniflux, PostgreSQL 16, Navidrome, DokuWiki)

    Last tested: 22 August 2026

    You do not need a rack of servers to start self-hosting. You need one machine, Docker, and a few well-chosen applications. This guide takes you from a blank Linux machine to a working home server with a working RSS reader, a personal music server, and a private wiki — every file tested on real hardware.

    Why self-host at all?

    Every month you pay for another subscription, you are renting someone else’s computer. Self-hosting flips the model: you buy the hardware once (or reuse what you already have) and run the software yourself. The benefits, in order of how they matter to real users:

    • Your data stays yours. Photos, music, notes, and feeds live on your disk, on your terms, with your backup strategy.
    • No subscription fatigue. A home server running four services costs a few pounds per month in electricity, not four monthly fees that keep going up.
    • Privacy by architecture. Your reading habits, your playlists, and your notes never pass through a company that may change its policies tomorrow.
    • You actually learn infrastructure. Networking, containers, reverse proxies, backups — the skills transfer directly to paid work.

    The honest trade-offs: you are the IT department now. Updates, outages, and security patches are your responsibility. A home server also needs a real IP address or a workaround (Tailscale, Cloudflare Tunnel) to be reached from outside your house — we cover that in the security series.

    Step 1: Choose your hardware

    You have three sensible starting points, depending on budget:

    Option A: Repurpose an old PC (free)

    Any x86 machine from roughly the last decade works as a starter server. The practical minimum: 8 GB of RAM, a solid-state drive (even a cheap SATA SSD makes a huge difference), and a power supply you trust. Old desktops are the classic choice, and a single 60 W machine idling costs roughly £15–25 per year at UK rates.

    Option B: Raspberry Pi 5 (around £80–100)

    The 16 GB model is the sweet spot for a starter stack: it runs the apps in this guide comfortably, idles around 5 W, and fits on a shelf. Use a quality case with active cooling and a 2.5″ SATA SSD via the HAT if you plan to store media — the Pi’s microSD slot will die on you under sustained writes.

    Option C: Used mini PC (around £150–250)

    Used Intel NUCs, Dell OptiPlexes, and HP Elites from office clearances offer x86 performance at a fraction of the price of new hardware. This is the best bang-per-pound if you want headroom for media transcoding later.

    Step 2: Install an operating system

    For a dedicated server, use a minimal Linux install rather than a desktop:

    • Debian 12 (bookworm): the safest default. Minimal install, no desktop, extremely stable, huge community.
    • Ubuntu Server 24.04 LTS: the friendlier choice if you want the most tutorials to match. Also an excellent default.
    • Truenas Scale or Proxmox: skip these for your first server. They add virtualisation and ZFS, which are powerful but premature until you know what you are running.

    During installation: give the machine a fixed IP on your LAN (or reserve one in your router’s DHCP table), and set a hostname like server. You will not want to remember a changing IP.

    Step 3: Install Docker

    Docker packages software into containers: isolated environments that start in seconds, take up only what they use, and are identical on any Linux machine. This is what makes self-hosting actually manageable instead of a tangle of system packages.

    On Debian or Ubuntu, the official install is a few lines:

    sudo apt-get update
    sudo apt-get install -y ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/$(. /etc/os-release && echo $ID) \ $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

    Then add your user to the docker group so you stop typing sudo:

    sudo usermod -aG docker $USER
    # log out and back in, then verify:
    docker compose version

    If that command prints a version number, you are ready.

    Step 4: Pick your first stack

    This is the part most guides get wrong. They hand you a list of forty apps and you spend two weeks configuring Sonarr and Prowlarr before you have used anything. Start with three services that you will actually touch every day:

    App What it does Why first
    Miniflux RSS reader — aggregates every feed you follow Instant daily value; your reading no longer depends on an algorithm
    Navidrome Personal music server — streams your library to any device One folder, one app, works with the official Substreamer app
    DokuWiki Plain-text wiki for notes and documentation Zero-friction writing; your notes are files on disk, not a proprietary format

    Each of these has a dedicated guide on this site with the exact compose file, tested setup steps, and the real errors we hit along the way:

    Step 5: Run the stack

    Everything below was verified on a 4-core / 16 GB Ubuntu machine with Docker 29. Create a project directory:

    mkdir -p ~/stacks/starter/music && cd ~/stacks/starter

    Drop in a docker-compose.yml with the three services (full annotated files in each linked guide):

    services: miniflux: image: miniflux/miniflux:latest container_name: miniflux environment: DATABASE_URL: postgres://miniflux:secret@miniflux-db/miniflux?sslmode=disable BASE_URL: http://localhost:8082 ports: - "8082:8080" depends_on: - miniflux-db restart: unless-stopped miniflux-db: image: postgres:16-alpine container_name: miniflux-db environment: POSTGRES_USER: miniflux POSTGRES_PASSWORD: secret POSTGRES_DB: miniflux volumes: - miniflux_db:/var/lib/postgresql/data restart: unless-stopped navidrome: image: deluan/navidrome:latest container_name: navidrome ports: - "4533:4533" environment: ND_SCANSCHEDULE: 1h ND_LOGLEVEL: info volumes: - ./music:/music:ro - navidrome_data:/data restart: unless-stopped dokuwiki: image: dokuwiki/dokuwiki:stable container_name: dokuwiki ports: - "8081:80" volumes: - dokuwiki_data:/dokuwiki/data - dokuwiki_conf:/dokuwiki/conf restart: unless-stopped volumes: miniflux_db: navidrome_data: dokuwiki_data: dokuwiki_conf:

    Then start it:

    docker compose up -d
    docker compose ps

    All four containers should show Up. The ports: Miniflux on :8082, DokuWiki on :8081, Navidrome on :4533. Open them from your laptop on the same network: http://SERVER_IP:8082, and so on.

    Step 6: The three first-run tasks

    1. Miniflux: first visit asks you to create the admin account. Then add feeds — start with the 10 you actually read. If the container keeps restarting with pq: SSL is not enabled on the server, your DATABASE_URL is missing ?sslmode=disable (full fix in the Miniflux guide).
    2. Navidrome: put MP3/FLAC files in the music/ folder, then create your first account at :4533. It scans the library on first login. The official mobile app (Substreamer) pairs in one minute.
    3. DokuWiki: first visit runs a tiny config wizard (admin login, language). That is the whole setup.

    Measured resource usage

    Because we run this stack, here is what it actually costs, measured with docker stats after a day of normal use (RSS polling, a music session, some wiki edits):

    Container Idle RAM Idle CPU
    miniflux 17 MiB ~0%
    miniflux-db (Postgres) 38 MiB ~0%
    navidrome 26 MiB ~0%
    dokuwiki 25 MiB ~0%

    Total: about 106 MiB of RAM for a full starter stack. A Raspberry Pi 5 with 4 GB has 40× the headroom this needs. The real cost of this stack is the electricity of the machine it lives on — which you were already paying.

    What comes next

    Once these three are boring (the goal), the natural expansion path is:

    • Nextcloud or Immich for photos and files (see the NAS & Media section)
    • AdGuard Home for network-wide ad and tracker blocking (Security & Networking)
    • A reverse proxy (Caddy or Nginx Proxy Manager) plus Tailscale, so the stack is reachable from anywhere without opening ports on your router — this is the single highest-value upgrade after the starter stack, and we cover it in the security series
    • Monitoring: Uptime Kuma, so your server tells you it is down instead of you finding out

    Frequently asked questions

    Is self-hosting safe if I am not a security expert?

    Yes, with discipline: keep Docker updated, use a reverse proxy with TLS instead of exposing ports, and do not expose admin interfaces directly to the internet. The starter stack above is LAN-only, which is the safe default. Our Security & Networking guides cover hardening step by step.

    Docker or Kubernetes?

    Docker Compose. Kubernetes on a home server is solving a problem you do not have. You will use 90% of what Compose gives you for 10% of the complexity.

    Can I run this on a VPS instead of at home?

    The same compose file runs unchanged on a VPS — that is part of Docker’s point. A VPS is a fine starting point if your home connection is bad or your ISP blocks inbound connections.

    What about the initial hardware cost?

    If you already own a spare PC, the marginal cost is electricity (a few pounds a month). A Raspberry Pi 5 path is around £100–150 all-in. After that, most services are free software. That is the whole pitch: one small fixed cost instead of an open-ended subscription stack.