In the past years I’ve always wanted to have a home server for some reason, but my formation as a programmer who did leetcode style problems from the age of 10 made it really hard for me to bear the inefficiency of having a Linux machine running 24/7 in my home.

Having been semi-cured of that issue let’s jump in the actual config.

The config

Starting out with the computer I chose for a server: a Dell OptiPlex 7050 Micro that I bought refurbished from a local company for around $140, plus I had to get an SSD because the one that came with it had too many bad sectors; so I got an SSD for ~$35 and that was that. dell

Services

I started gathering all the needed software that was going to be useful around the internet.

Media

What I needed was a replacement for:

  • All the Movies/TV Series streaming services because most of the stuff I wanna watch is either not available in my country or not on the streaming platforms.
  • Music because I recently completed my listening setup and streaming services don’t satisfy anymore. I still keep my YouTube Music subscription and use it, but when it comes to listening to full albums FLACs are the way to go.
  • Some kind of iCloud replacement.
  • Password manager
  • Pocket replacement. I would’ve said bookmark manager, but it’s not even that, I’ve been using Pocket for a long while and it just got worse and worse over the years.

And some nice-to-haves

  • Something to monitor everything and get notified if they’re down.
  • A reverse proxy to be able to go to cloud.erdeiserver.us rather than 192.162.0.10:8080
  • A dashboard to access everything from

Small note: I have a Synology NAS already, that handles all my photo backup stuff with a 2x4TB NAS grade HDDs in raid.

PSA: A very important thing when you have a home server is protection in case of power loss. For this I have a very dumb UPS connected that holds my NAS and server up and running for at least 2 hours. In my use case, it’s the perfect setup because I have frequent power spikes and drops for 2-3 minutes and it comes back.

So the dashboard looks like this

homarr

This being said everything I have is here.

Now get ready for the whole docker-compose file.

services:
  nginxproxymanager:
    image: "jc21/nginx-proxy-manager:github-pr-3121"
    container_name: nginxproxymanager
    restart: unless-stopped
    ports:
      - "80:80"
      - "81:81"
      - "443:443"
    volumes:
      - ${HOME}/storage/nginx/data:/data
      - ${HOME}/storage/nginx/letsencrypt:/etc/letsencrypt
      - ${HOME}/storage/nginx/config.json:/app/config/production.json

  nextcloud:
    image: lscr.io/linuxserver/nextcloud:latest
    container_name: nextcloud
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Bucharest
    volumes:
      - ${HOME}/storage/nextcloud/appdata:/config
      - ${HOME}/storage/nextcloud/data:/data
    restart: unless-stopped

  homeassistant:
    image: lscr.io/linuxserver/homeassistant:latest
    container_name: homeassistant
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Bucharest
      - TRUSTED_PROXIES=172.18.0.12

    volumes:
      - ${HOME}/storage/hass/config:/config
    restart: unless-stopped

  plex:
    image: lscr.io/linuxserver/plex
    container_name: plex
    environment:
      - PUID=1000
      - PGID=1000
      - VERSION=docker
      - PLEX_CLAIM=claim-B7znMtSSnu-YWd-Rv9Jy
    volumes:
      - ${HOME}/storage/configs:/config
      - ${HOME}/storage/tv:/tv
      - ${HOME}/storage/movies:/movies
    restart: unless-stopped
    ports:
      - 32400:32400
      - 1900:1900/udp
      - 5353:5353/udp
      - 8324:8324
      - 32410:32410/udp
      - 32412:32412/udp
      - 32413:32413/udp
      - 32414:32414/udp
      - 32469:32469

  overseerr:
    image: lscr.io/linuxserver/overseerr:latest
    container_name: overseerr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
    volumes:
      - ${HOME}/storage/overseerr/config:/config
    ports:
      - 5055:5055
    restart: unless-stopped

  homarr:
    container_name: homarr
    image: ghcr.io/ajnart/homarr:latest
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${HOME}/storage/homarr/configs:/app/data/configs
      - ${HOME}/storage/homarr/icons:/app/public/icons
      - ${HOME}/storage/homarr/data:/data
    ports:
      - "7575:7575"

  radarr:
    image: linuxserver/radarr
    container_name: radarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Bucharest
      - UMASK_SET=022
    volumes:
      - ${HOME}/storage/configs/Radarr:/config
      - ${HOME}/storage/downloads:/downloads
      - ${HOME}/storage/movies:/movies
    ports:
      - 7878:7878
    restart: unless-stopped

  lidarr:
    image: lscr.io/linuxserver/lidarr:latest
    container_name: lidarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Eruope/Bucharest
    volumes:
      - ${HOME}/storage/configs/lidarr:/config
      - ${HOME}/storage/music:/music
      - ${HOME}/storage/downloads:/downloads
    ports:
      - 8686:8686
    restart: unless-stopped

  slskd:
    ports:
      - 5030:5030/tcp
      - 5031:5031/tcp
      - 50300:50300/tcp
    volumes:
      - ${HOME}/storage/configs/slskd:/app:rw
      - ${HOME}/storage/music:/music:rw
    user: 1000:1000
    image: slskd/slskd:latest

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Eruope/Bucharest
    volumes:
      - ${HOME}/storage/configs/prowlarr:/config
    ports:
      - 9696:9696
    restart: unless-stopped

  sonarr:
    image: linuxserver/sonarr
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Bucharest
      - UMASK_SET=022
    volumes:
      - ${HOME}/storage/configs/Sonarr:/config
      - ${HOME}/storage/downloads:/downloads
      - ${HOME}/storage/tv:/tv
    ports:
      - 8989:8989
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
      - WEBUI_PORT=8080
      - TORRENTING_PORT=6881
    volumes:
      - ${HOME}/storage/qbittorrent/appdata:/config
      - ${HOME}/storage/downloads:/downloads
      - ${HOME}/storage/tv:/tv
      - ${HOME}/storage/movies:/movies
    ports:
      - 8080:8080
      - 6881:6881
      - 6881:6881/udp
    restart: unless-stopped

  navidrome:
    image: deluan/navidrome:latest
    ports:
      - "4533:4533"
    environment:
      ND_SCANSCHEDULE: 1h
      ND_LOGLEVEL: info
    volumes:
      - ${HOME}/storage/configs/navidrome:/data
      - ${HOME}/storage/music:/music:ro

  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    volumes:
      - ${HOME}/storage/configs/vw-data/:/data/
    restart: unless-stopped

  wallabag:
    image: wallabag/wallabag
    environment:
      - MYSQL_ROOT_PASSWORD=wallaroot
      - SYMFONY__ENV__DATABASE_DRIVER=pdo_mysql
      - SYMFONY__ENV__DATABASE_HOST=db
      - SYMFONY__ENV__DATABASE_PORT=3306
      - SYMFONY__ENV__DATABASE_NAME=wallabag
      - SYMFONY__ENV__DATABASE_USER=wallabag
      - SYMFONY__ENV__DATABASE_PASSWORD=wallapass
      - SYMFONY__ENV__DATABASE_CHARSET=utf8mb4
      - SYMFONY__ENV__DATABASE_TABLE_PREFIX="wallabag_"
      - SYMFONY__ENV__MAILER_DSN=smtp://127.0.0.1
      - SYMFONY__ENV__DOMAIN_NAME=https://wall.erdeiserver.us
      - SYMFONY__ENV__SERVER_NAME="Wallabag"
    ports:
      - 9010:80
    volumes:
      - ${HOME}/storage/wallabag/images:/var/www/wallabag/web/assets/images
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "https://wall.erdeiserver.us",
        ]
      interval: 1m
      timeout: 3s
    depends_on:
      - db
      - redis
  db:
    image: mariadb
    environment:
      - MYSQL_ROOT_PASSWORD=wallaroot
    volumes:
      - ${HOME}/storage/wallabag/data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 20s
      timeout: 3s
  redis:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 20s
      timeout: 3s

  uptime-kuma:
    container_name: uptime-kuma
    image: louislam/uptime-kuma:latest
    volumes:
      - ${HOME}/storage/configs/kuma/data:/app/data
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - 3001:3001
      - 3307:3306

  ### And some stuff my wife asked me for and I didn't get to organize nicely
  proxy: # The proxy must not be removed. If needed, point your own proxy to this container, rather than removing this
    container_name: recipesage_proxy
    image: julianpoy/recipesage-selfhost-proxy:v4.0.0
    ports:
      - 7270:80
    depends_on:
      - static
      - api
      - pushpin
    restart: unless-stopped
  static: # Hosts frontend assets
    container_name: recipesage_static
    image: julianpoy/recipesage-selfhost:static-v2.14.6
    restart: unless-stopped
  api: # Hosts backend API
    container_name: recipesage_api
    image: julianpoy/recipesage-selfhost:api-v2.14.6
    depends_on:
      - postgres
      - typesense
      - pushpin
      - browserless
    command: sh -c "npx prisma migrate deploy; npx ts-node --swc --project packages/backend/tsconfig.json packages/backend/src/bin/www"
    environment:
      - STORAGE_TYPE=filesystem
      - FILESYSTEM_STORAGE_PATH=/rs-media
      - NODE_ENV=selfhost
      - VERBOSE=false
      - VERSION=selfhost
      - POSTGRES_DB=recipesage_selfhost # If changing this, make sure to update the postgres container and the DATABASE_URL below accordingly
      - POSTGRES_USER=recipesage_selfhost # If changing this, make sure to update the postgres container and the DATABASE_URL below accordingly
      - POSTGRES_PASSWORD=recipesage_selfhost # If changing this, make sure to update the postgres container and the DATABASE_URL below accordingly
      - POSTGRES_PORT=5432 # If changing this, make sure to update the postgres container and the DATABASE_URL below accordingly
      - POSTGRES_HOST=postgres # If changing this, make sure to update the postgres container and the DATABASE_URL below accordingly
      - POSTGRES_SSL=false
      - POSTGRES_LOGGING=false
      - DATABASE_URL=postgresql://recipesage_selfhost:recipesage_selfhost@postgres:5432/recipesage_selfhost
      - GCM_KEYPAIR
      - SENTRY_DSN
      - GRIP_URL=http://pushpin:5561/
      - GRIP_KEY=changeme
      - SEARCH_PROVIDER=typesense
      - 'TYPESENSE_NODES=[{"host": "typesense", "port": 8108, "protocol": "http"}]'
      - TYPESENSE_API_KEY=recipesage_selfhost
      - STRIPE_SK # Value should not be set.
      - STRIPE_WEBHOOK_SECRET # Value should not be set
      - BROWSERLESS_HOST=browserless
      - BROWSERLESS_PORT=3000
      - INGREDIENT_INSTRUCTION_CLASSIFIER_URL=http://ingredient-instruction-classifier:3000/
      - OPENAI_API_KEY # Please follow the instructions in the README if you decide to supply a value here
    volumes:
      - ${HOME}/storage/food/apimedia:/rs-media
    restart: unless-stopped
  typesense: # Provides the fuzzy search engine
    container_name: recipesage_typesense
    image: typesense/typesense:0.24.1
    volumes:
      - ${HOME}/storage/food/typesensedata:/data
    command: "--data-dir /data --api-key=recipesage_selfhost --enable-cors"
    restart: unless-stopped
  pushpin: # Provides websocket support
    container_name: recipesage_pushpin
    image: julianpoy/pushpin:2023-09-17
    entrypoint: /bin/sh -c
    command:
      [
        'sed -i "s/sig_key=changeme/sig_key=$$GRIP_KEY/" /etc/pushpin/pushpin.conf && echo "* $${TARGET},over_http" > /etc/pushpin/routes && pushpin --merge-output',
      ]
    environment:
      - GRIP_KEY=changeme
      - TARGET=api:3000
    restart: unless-stopped
  postgres: # Database
    container_name: recipesage_postgres
    image: postgres:16
    environment:
      - POSTGRES_DB=recipesage_selfhost # If you change this, make sure to change both POSTGRES_DB and DATABASE_URL on the API container
      - POSTGRES_USER=recipesage_selfhost # If you change this, make sure to change both POSTGRES_USER and DATABASE_URL on the API container
      - POSTGRES_PASSWORD=recipesage_selfhost # If you change this, make sure to change both POSTGRES_PASSWORD and DATABASE_URL on the API container
    volumes:
      - ${HOME}/storage/food/postgresdata:/var/lib/postgresql/data
    restart: unless-stopped
  browserless: # A headless browser for scraping websites with the auto import tool
    container_name: recipesage_browserless
    image: browserless/chrome:1.61.0-puppeteer-21.4.1
    environment:
      - MAX_CONCURRENT_SESSIONS=3
      - MAX_QUEUE_LENGTH=10
    restart: unless-stopped

  ingredient-instruction-classifier:
    container_name: recipesage_classifier
    image: julianpoy/ingredient-instruction-classifier:1.4.11
    environment:
      - SENTENCE_EMBEDDING_BATCH_SIZE=200
      - PREDICTION_CONCURRENCY=2
    restart: unless-stopped

volumes:
  apimedia:
    driver: local
  typesensedata:
    driver: local
  postgresdata:
    driver: local

I suggest you use Portainer to create this docker-compose stack because it’s a bit easier to manage than with the CLI, but Portainer itself will have to be started with the CLI.

That looks like this portainer

I left all passwords as default in this file so you can change them when you copy and paste everything, but it will run like this as well, it’s a matter of security only.

Now, how I suggest you go about these things is to check each container one by one and try to look up on Google what it does and how you have to set them up. For most of them, it is as easy as either looking up the default credentials or setting them up yourself.

The more complicated part is setting up nginx proxy manager, but here is the article that helped me set it up in less than 20 minutes.

The R suite (overseerr radarr lidarr prowlarr sonarr), is basically what will help you get all your media downloaded, needs a torrent client so there is qBitTorrent. Easy to set up and get working.

Some things to keep in mind to make your life easy:

  • Docker compose stacks have a network of their own, if you want to refer to a service it has an internal IP address that you see in Portainer. You don’t need to have the ports forwarded if you need to connect 2 containers. Just use their internal IP to refer them.
  • Anything can be deleted and things will still work, it’s not necessarily always true, but it’s true enough for you to play with things.
  • It will take a long time to start because you have to download lots of containers, be patient, or add each service after you read about it a bit.
  • You will need to SSH into your system often so it’s great to set up an SSH key for easier authentication.
  • Please change your passwords from the default ones.