Author: wallace

  • Self-Hosting your services at home

    In this post, I explain how I made my self-hosted services accessible from anywhere in the world using:

    • Dynu for dynamic DNS updates
    • Caddy for automatic HTTPS and reverse proxying
    • Home router port forwarding to route public traffic into my server

    🌍 Problem

    My home internet connection uses a dynamic IP address provided by Vodafone, which changes periodically. Without a static IP, it’s difficult to access self-hosted services like Jellyfin, Immich, or WordPress remotely. Also, I wanted HTTPS support without having to manually configure certificates.

    πŸ› οΈ Solution Overview

    1. Register a domain and manage DNS via Dynu
    2. Install the Dynu IP Update Client on my server to keep the A record updated with my current public IP
    3. Use Caddy in Docker as a reverse proxy to handle subdomains and HTTPS automatically via Let’s Encrypt
    4. Configure my router to forward ports 80 and 443 to the server’s internal IP

    πŸ”— Dynu Setup

    1. Create an account at dynu.com
    2. Add a domain (e.g., wallaceat.me) and enable Dynamic DNS
    3. Install the Dynu client on your server:
    sudo wget https://github.com/shibani/dynu-ip-update-client/releases/latest/download/dynu-ip-update-client-linux-amd64.deb
    sudo dpkg -i dynu-ip-update-client-linux-amd64.deb
    
    1. Configure it with your Dynu API token or login credentials
    2. Ensure the service is running and enabled:
    sudo systemctl enable --now dynu-ip-update-client.service

    The client checks for public IP changes and updates your Dynu DNS record automatically.

    ⚑ Router Configuration

    Log in to your home router and forward the following ports to your server’s internal IP (e.g., 192.168.1.100):

    External PortInternal IPInternal PortProtocol
    80192.168.1.10080TCP
    443192.168.1.100443TCP

    Now any request to http(s)://yourdomain.com is routed to the Caddy server inside your LAN.

    🧠 Caddy Configuration

    The Caddyfile defines reverse proxy rules. Here’s a simplified example:

    wallaceat.me {
      reverse_proxy wordpress:8080
    }
    
    photos.wallaceat.me {
      reverse_proxy immich_server:2283
    }
    
    tv.wallaceat.me {
      reverse_proxy jellyfin:8096
    }

    Caddy automatically issues and renews TLS certificates via Let’s Encrypt, making HTTPS seamless.

    πŸ“‘ Testing Everything

    • Visit https://wallaceat.me and confirm the WordPress page loads
    • Check https://tv.wallaceat.me for Jellyfin
    • Verify Dynu logs to see if IP updates are working
    • Confirm port 80/443 are open using https://www.yougetsignal.com/tools/open-ports/

    βœ… Outcome

    All self-hosted services are now available from anywhere, secured with HTTPS, using a reliable and automatic DNS + reverse proxy setup. The stack is stable, scalable, and doesn’t require paying for a static IP or managed DNS.

    🧩 Technologies Used

    • Ubuntu Linux
    • Docker & Docker Compose
    • Caddy Server (v2)
    • Dynu Dynamic DNS
    • Vodafone Broadband (dynamic IP)

    πŸ’¬ Final Thoughts

    This setup is perfect for enthusiasts looking to self-host services and access them securely over the internet. With minimal cost and maintenance, you get a robust system that’s entirely under your control.

  • Setting Up Jellyfin as a Self-Hosted Media Server

    In this post, I share how I set up Jellyfin, a free and open-source media server, on my home server using Docker, integrated with Caddy for HTTPS and a custom subdomain.

    🎯 Goals

    • Host Jellyfin locally using Docker
    • Access the media server via https://tv.wallaceat.me
    • Enable HTTPS with automatic certificate management using Caddy

    πŸ“ Project Structure

    /home/wallace/server/jellyfin
    β”œβ”€β”€ docker-compose.yml
    β”œβ”€β”€ media/
    β”œβ”€β”€ media2/
    └── fonts/

    🧩 Docker Compose File

    This is the docker-compose.yml I used to spin up Jellyfin:

    version: "3.8"
    
    services:
      jellyfin:
        image: jellyfin/jellyfin
        container_name: jellyfin
        user: 1000:1000
        volumes:
          - /home/wallace/server/jellyfin/config:/config
          - /home/wallace/server/jellyfin/cache:/cache
          - type: bind
            source: /server/jellyfin/media
            target: /media
          - type: bind
            source: /server/jellyfin/media2
            target: /media2
          - type: bind
            source: /mnt/torrents
            target: /torrent
            read_only: true
          - type: bind
            source: /path/to/fonts
            target: /usr/local/share/fonts/custom
            read_only: true
        restart: unless-stopped
        environment:
          - JELLYFIN_PublishedServerUrl=https://tv.wallaceat.me
        extra_hosts:
          - 'host.docker.internal:host-gateway'
        networks:
          - local
    
    networks:
      local:
        external: true
    

    πŸ”’ Caddy Reverse Proxy

    I configured the reverse proxy in my existing Caddy container. Here’s the snippet added to the Caddyfile:

    tv.wallaceat.me {
      reverse_proxy jellyfin:8096
    }

    Ensure Jellyfin is in the same Docker network that Caddy is part of, or adjust to host.docker.internal:8096 if needed.

    🌐 Domain and DNS

    • A subdomain tv.wallaceat.me was created and pointed to the public IP of the server using Dynu DNS
    • Dynu IP Update Client keeps the IP up to date in case it changes

    ⚠️ Common Issues I Faced

    • Port conflicts: Jellyfin initially used host network, which interfered with other services; switching to bridged network fixed it
    • Reverse proxy errors: Caddy logged connection timeouts when the Jellyfin container was misconfigured or not reachable
    • Permissions: Mounted volumes needed correct read/write permissions for Jellyfin to access media files

    πŸŽ‰ Outcome

    Jellyfin is now fully accessible at https://tv.wallaceat.me with SSL and remote access. It’s integrated into my self-hosted stack alongside Immich and WordPress using Caddy as a central reverse proxy.

    πŸ’‘ Lessons Learned

    • Using Caddy simplifies HTTPS and domain routing significantly
    • Proper Docker network configuration is critical for reverse proxying
    • Persistent volumes are essential to keep configuration and media libraries intact

    Jellyfin has been a solid and customizable solution for streaming my personal media library across devices, all from a home server setup.

  • Hosting Immich Locally with Docker and Caddy

    In this post, I document the steps I followed to set up Immich – a self-hosted photo and video backup solution – on my local server using Docker and Caddy. I also configured it to be accessible over HTTPS with a custom subdomain.

    πŸ”§ Requirements

    • Docker and Docker Compose installed
    • A registered domain (e.g. wallaceat.me)
    • Caddy running in a container for reverse proxy and HTTPS

    πŸ“ Directory Structure

    /home/wallace/server/immich
    β”œβ”€β”€ docker-compose.yml
    β”œβ”€β”€ stack.env
    └── ...

    πŸ“ docker-compose.yml

    Here’s the Docker Compose file I used:

    version: "3.8"
    services:
      immich-server:
        container_name: immich_server
        image: ghcr.io/immich-app/immich-server:release
        volumes:
          - /mnt/immich:/usr/src/app/upload:rw
          - /etc/localtime:/etc/localtime:ro
        env_file:
          - stack.env
        ports:
          - '2283:2283'
        depends_on:
          - redis
          - database
        restart: always
        networks:
          - immich-network
    
      immich-machine-learning:
        image: ghcr.io/immich-app/immich-machine-learning:release
        volumes:
          - model-cache:/cache
        env_file:
          - stack.env
        restart: always
        networks:
          - immich-network
    
      redis:
        image: redis:6.2-alpine
        restart: always
        networks:
          - local
    
      database:
        image: tensorchord/pgvecto-rs:pg14-v0.2.0
        environment:
          POSTGRES_PASSWORD: your_password
          POSTGRES_USER: your_user
          POSTGRES_DB: your_db
        volumes:
          - /mnt/postgres:/var/lib/postgresql/data
        restart: always
        networks:
          - local
    
    volumes:
      model-cache:
    
    networks:
      local

    🌐 Caddy Configuration

    My Caddy server is already running in a separate container and serves multiple services. Here’s the config snippet I added to the Caddyfile to expose Immich:

    photos.wallaceat.me {
      reverse_proxy immich_server:2283
    }

    If using an external Caddy container, ensure it’s in the same Docker network or resolve to the host’s IP and port.

    🌍 Domain and HTTPS

    I pointed the subdomain subdomain.wallaceat.me to my server’s IP using an A record on my DNS provider (Dynu). Caddy automatically issued a Let’s Encrypt certificate.

    πŸ›  Common Issues

    • Port conflicts: Ensure no other services are using port 80 or 443
    • Redis timeout: Check if the Redis container is running and available on the Docker network
    • Permissions: Mount points like /mnt/immich must be accessible by Docker

    πŸ“Œ Tips

    • Automate DNS updates using Dynu’s dynamic IP update client if your IP is dynamic
    • Consider automating deployment via GitHub Actions or Drone CI in the future