Like many, I relied on Plex for years to manage and stream my personal media library – hundreds of movies and countless TV show episodes. It served me well. However, recent shifts towards monetizing core features, like remote streaming, prompted me to look for alternatives. I wanted a solution that was open-source, powerful, and gave me complete control over my media and its access, without unexpected subscription requirements for basic functionality.

Enter Jellyfin. It's a fantastic, free software media system that puts you in control. But simply installing it isn't enough; I needed secure, easy remote access for myself and my family, just like I had before.

This post details my journey setting up Jellyfin locally using Docker, but making it securely accessible from anywhere using Traefik as a reverse proxy, Let's Encrypt for free SSL certificates, and leveraging AWS Route 53 for seamless and reliable DNS hosting. If you're comfortable with Docker and use AWS for your DNS, this guide is for you!

Why This Stack?

  • Jellyfin: Awesome open-source media server.
  • Docker & Docker Compose: Easy deployment, management, and reproducibility of services.
  • Traefik: A modern, cloud-native reverse proxy that integrates beautifully with Docker. It automatically discovers services and handles SSL certificate management.
  • Let's Encrypt: Free, automated SSL certificates. Essential for secure HTTPS connections.
  • AWS Route 53: Reliable DNS hosting. Crucially, its API allows Traefik to perform the Let's Encrypt DNS-01 challenge automatically – perfect if your ISP blocks port 80 (a common issue!).

Prerequisites

  • A server or machine (like a NAS, Raspberry Pi, or home server) running Docker and Docker Compose.
  • A registered domain name (we'll use yourdomain.com as an example).
  • Your domain's DNS hosted on AWS Route 53.
  • An AWS account with IAM permissions to modify Route 53 records for yourdomain.com.
  • Basic familiarity with the command line.

The Challenge: Secure Remote Access & The Port 80 Problem

Running Jellyfin locally is easy, but accessing it securely from outside your network requires HTTPS. Let's Encrypt provides free SSL certificates via the ACME protocol. The most common validation method (HTTP-01 challenge) requires your server to be reachable from the internet on port 80. Unfortunately, many residential ISPs block incoming traffic on port 80.

This is where the DNS-01 challenge shines. Instead of checking port 80, Let's Encrypt verifies domain ownership by checking for a specific TXT record in your DNS zone. Since we're using Route 53, Traefik can automatically create and remove these temporary records using the AWS API.

Step 1: AWS Setup - IAM User for Traefik

Traefik needs permission to modify your Route 53 DNS records for yourdomain.com. We'll create a dedicated IAM user with only the necessary permissions (Principle of Least Privilege).

  1. Go to the IAM console in AWS.
  2. Create a new User. Give it a descriptive name (e.g., traefik-route53-manager).
  3. Select "Attach policies directly".
  4. Create a new policy. Use the JSON editor and paste the following policy, replacing YOUR_HOSTED_ZONE_ID with the actual Route 53 Hosted Zone ID for yourdomain.com (you can find this in the Route 53 console):

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "route53:GetChange",
                    "route53:ListHostedZonesByName"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "route53:ChangeResourceRecordSets",
                    "route53:ListResourceRecordSets"
                ],
                "Resource": "arn:aws:route53:::hostedzone/YOUR_HOSTED_ZONE_ID"
            }
        ]
    }
    

    > Note: Using ListHostedZonesByName and filtering by resource ARN for ChangeResourceRecordSets scopes down permissions significantly.

  5. Finish creating the policy (give it a name like TraefikRoute53AccessPolicy).

  6. Attach this new policy to the user you created.

  7. Proceed to the "Security credentials" tab for the new user.

  8. Create an "Access key" - select "Application running outside AWS" as the use case.

  9. Carefully copy the Access Key ID and Secret Access Key. You won't see the secret key again! Store these securely for the next steps.

Step 2: Docker Compose Configuration

Now, let's define our services using docker-compose.yml. This file defines the Traefik reverse proxy and the Jellyfin application itself. Remember to replace the example hostnames (jellyfin.yourdomain.com, traefik.yourdomain.com) with your actual desired subdomains.

# docker-compose.yml
services:
  # Traefik Service (Reverse Proxy & SSL)
  traefik:
    image: "traefik:v2.11" # Pin to a specific stable version
    container_name: "traefik-proxy" # Descriptive container name
    command:
      # Enable Docker provider & disable exposing by default
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      # Define HTTP (80) and HTTPS (443) entry points
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      # Enable Traefik API/Dashboard (for monitoring)
      - "--api.dashboard=true"
      # Configure Let's Encrypt Resolver (named 'myresolver')
      - "--certificatesresolvers.myresolver.acme.email=your-email@example.com" # !! REPLACE THIS with your email !!
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      # Use DNS-01 challenge with Route53 provider
      - "--certificatesresolvers.myresolver.acme.dnschallenge=true"
      - "--certificatesresolvers.myresolver.acme.dnschallenge.provider=route53"
      # Global Redirect: HTTP -> HTTPS
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
    ports:
      # Expose port 80 for HTTP->HTTPS redirect (optional if ISP blocks)
      - "80:80"
      # Expose port 443 for HTTPS access (ESSENTIAL)
      - "443:443"
    volumes:
      # Mount Docker socket to detect container events
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # Persist Let's Encrypt certificates
      - "./letsencrypt:/letsencrypt"
    networks:
      - proxy # Connect to our custom network
    # Environment variables for AWS credentials (will use .env file)
    environment:
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      # - AWS_REGION=us-east-1 # Optional: Specify if needed
      # - AWS_HOSTED_ZONE_ID=YOUR_HOSTED_ZONE_ID # Optional
    labels:
      # Enable Traefik for its own dashboard
      - "traefik.enable=true"
      # Dashboard Router Rule (HTTPS)
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.yourdomain.com`)" # !! REPLACE with your dashboard hostname !!
      - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard.service=api@internal"
      # Use Let's Encrypt for the dashboard domain
      - "traefik.http.routers.traefik-dashboard.tls.certresolver=myresolver"
      # Secure the dashboard with Basic Auth
      - "traefik.http.routers.traefik-dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=user:$ class="math-inline">apr1abcdefg$yourhashedpassword" # !! REPLACE THIS HASH !!
    restart: unless-stopped

  # Jellyfin Service
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000 # Your user ID
      - PGID=1000 # Your group ID
      - TZ=Etc/UTC # !! REPLACE with Your timezone e.g. Pacific/Auckland !!
    volumes:
      # Map Jellyfin config directory
      - /path/to/jellyfin/config:/config/ # !! REPLACE host path to your config location !!
      # Map Media Libraries
      - /path/to/tvshows:/data/tvshows   # !! REPLACE host path to your TV shows !!
      - /path/to/movies:/data/movies     # !! REPLACE host path to your Movies !!
    networks:
      - proxy # Connect to the same network as Traefik
    restart: unless-stopped
    labels:
      # Enable Traefik management for this container
      - "traefik.enable=true"
      # Jellyfin Router Rule (HTTPS)
      - "traefik.http.routers.jellyfin.rule=Host(`jellyfin.yourdomain.com`)" # !! REPLACE with your Jellyfin hostname !!
      - "traefik.http.routers.jellyfin.entrypoints=websecure" # Use HTTPS entrypoint
      # Use Let's Encrypt resolver defined above
      - "traefik.http.routers.jellyfin.tls.certresolver=myresolver"
      # Tell Traefik which internal port Jellyfin uses
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"

# Define the shared network
networks:
  proxy:
    name: proxy

# Define the volume for Let's Encrypt certificates
volumes:
  letsencrypt:

Step 3: Handling Secrets - The .env File

Never commit secrets directly into your docker-compose.yml! We'll use a .env file to store the AWS credentials.

  1. In the same directory as your docker-compose.yml, create a file named .env.

  2. Add your AWS keys:

    # .env
    AWS_ACCESS_KEY_ID=YOUR_ACTUAL_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY=YOUR_ACTUAL_SECRET_ACCESS_KEY
    
  3. Crucially: Add .env to your .gitignore file to prevent accidentally committing it.

    # .gitignore
    .env
    

Docker Compose will automatically read this file and inject these variables into the Traefik container's environment, as specified in the environment: section of the docker-compose.yml.

Step 4: Securing the Traefik Dashboard

We added basic authentication to the dashboard using Traefik labels. You need to generate a password hash.

  1. Install htpasswd (usually via apache2-utils or httpd-tools).
  2. Run the following, replacing admin and 'YourSecurePassword!' with your desired credentials:

    htpasswd -nb admin 'YourSecurePassword!'
    
  3. Copy the output (e.g., admin:$apr1$somehash$morehash).

  4. Paste it into the traefik.http.middlewares.auth.basicauth.users label in docker-compose.yml.
    Remember to escape the dollar signs ($) by doubling them ($$):

    # Example label in docker-compose.yml
    _ "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$somehash$$morehash" # !! REPLACE user:hash !!
    

Step 5: DNS Configuration (Route 53)

Before starting, ensure you have A records in Route 53 pointing your chosen hostnames (e.g., jellyfin.yourdomain.com and traefik.yourdomain.com) to the public IP address of the server running Docker.

jellyfin.yourdomain.com -> YOUR_SERVER_PUBLIC_IP
traefik.yourdomain.com -> YOUR_SERVER_PUBLIC_IP

Step 6: Final Checks & Deployment

  1. Placeholders: Ensure you've replaced all placeholders marked with !! REPLACE !! or YOUR_... in the docker-compose.yml and .env files with your actual values (hostnames, email, paths, user IDs, timezone, password hash, AWS keys).
  2. Paths: Double-check the host paths mapped in the volumes section for Jellyfin – point them to your actual config and media library locations.
  3. User/Group ID: Update PUID and PGID in the Jellyfin environment section to match the user/group that owns your media files (use id $USER on Linux to find yours).
  4. Directory: Create the Let's Encrypt volume directory: mkdir ./letsencrypt.
  5. Firewall/Router: Ensure port 443 is forwarded from your router/firewall to the internal IP address of your Docker host machine. Port 80 forwarding is optional now but recommended for the HTTP->HTTPS redirect to work smoothly.
  6. Launch: Run docker compose up -d.
  7. Monitor: Check the Traefik logs, especially on the first run, to ensure successful AWS authentication and certificate acquisition: docker compose logs -f traefik-proxy.

Conclusion

You should now have a fully functional Jellyfin instance accessible securely via https://jellyfin.yourdomain.com (using your actual domain), with automatic SSL certificate management handled by Traefik and Let's Encrypt, leveraging the power and reliability of AWS Route 53 for DNS validation. The Traefik dashboard is also available (and password-protected) at https://traefik.yourdomain.com (again, using your actual domain).

This setup gives me the control and freedom I was seeking after moving from Plex. By combining powerful open-source tools like Jellyfin and Traefik with robust cloud services like AWS Route 53 and IAM, we can build secure, flexible, and non-subscription-based solutions tailored perfectly to our needs.

Happy self-hosting! Let me know about your experiences in the comments below.