Multi-Purpose Raspberry Pi 3 Home Server with Docker [Nextcloud, Gogs, Home Assistant etc.] Running from HDD, with Reverse Proxy [Traefik] and Auto DNS Updates [via ddclient/Cloudflare]
Here is the setup I'm using on my Raspberry Pi 3 server, compiled from different guides across the internet. Raspbian is running from an HDD for better performance, with most of the services running on Docker. This offers great maintainability, as all services start with a single docker-compose up. By having a reverse-proxy you don't need to expose various ports on your system, only 80 and 443. Plus Traefik auto-manages Let’s Encrypt SSL certificates, with auto renewal.
Raspbian Setup
If you don't have Raspbian installed, grab the official Stretch image and flash it with Etcher.
After logging in update your distro:
$ sudo apt-get update && sudo apt-get upgrade -y
$ sudo apt-get dist-upgrade
$ sudo apt-get autoremove
$ sudo reboot
Running Raspbian from HDD
Make sure you have rsync installed:
$ sudo apt-get install rsync
You can follow part 4 of this excellent guide. To summarize it:
First unmount your sda:
$ sudo umount /dev/sda1
Then prepare the disk. This example makes a 10GB ext4 partition and another ext4 with the rest of the disk space:
$ sudo parted /dev/sda
(parted) mktable msdos
Warning: The existing disk label on /dev/sda will be destroyed and all data on this disk will be lost. Do you want to continue?
Yes/No? Yes
(parted) mkpart primary ext4 0% 10000M
(parted) mkpart primary ext4 10000M 100%
Format boot and root file systems:
$ sudo mkfs.ext4 /dev/sda1
$ sudo mkfs.ext4 /dev/sda2
Mount your sda1 into mnt:
$ sudo mount /dev/sda1 /mnt
Copy the root to the mounted hdd:
$ sudo rsync -axv / /mnt
Now get the list os UUIDs and PARIDS of your disk:
$ sudo blkid | grep sda
/dev/sda1: UUID="uuid" TYPE="ext4" PARTUUID="partuuid-01"
/dev/sda2: UUID="uuid" TYPE="ext4" PARTUUID="partuuid-02"
You need to add your sda1's PARTUUID to root and add rootdelay=5 at the end. Change the boot file from default SD to HDD:
$ sudo nano /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=partuuid-01 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait rootdelay=5
Create the mount target to your sda2:
$ sudo mkdir /mnt/mnt/storage
Now will need to change fstab on the HD to auto mount everything. DO NOT CHANGE ON THE SD:
$ sudo nano /mnt/etc/fstab
proc /proc proc defaults 0 0
/dev/mmcblk0p1 /boot vfat defaults 0 2
/dev/mmcblk0p2 none ext4 ro,noauto 0 1
/dev/disk/by-uuid/uuid / ext4 defaults,noatime 0 1
/dev/disk/by-uuid/uuid /mnt/storage ext4 defaults,noatime 0 0
$ sudo cp /mnt/etc/fstab /boot/fstab_usb
$ sudo reboot
Setup ddclient for Auto DNS Updates
I'm using Cloudflare as my DNS. Other services work too, but they're not covered by this guide. Create the A records on Cloudflare for the subdomains you want to use.
You're going to need git, perl plus some perl packages:
$ sudo apt-get install git perl libdata-validate-ip-perl libjson-perl
First clone ddclient to your home dir:
$ cd ~
$ git clone https://github.com/ddclient/ddclient
$ cd ddclient
Then install it:
$ sudo cp ddclient /usr/sbin/
$ sudo mkdir /etc/ddclient
$ sudo mkdir /var/cache/ddclient
$ sudo cp sample-etc_ddclient.conf /etc/ddclient/ddclient.conf
$ sudo nano /etc/ddclient/ddclient.conf
ddclient.conf:
daemon=300 # check every 300 seconds
syslog=yes # log update msgs to syslog
pid=/var/run/ddclient.pid # record PID in file.
ssl=yes # use ssl-support.
use=web # via web
##
## CloudFlare (www.cloudflare.com)
##
protocol=cloudflare, \
zone=yourdomain.com, \
ttl=1, \
login=yourcloudflaremail@sample.com, \
password=your_cloudflare_api_key \
subdomain1.yourdomain.com,subdomain2.yourdomain.com
$ sudo cp sample-etc_rc.d_init.d_ddclient.ubuntu /etc/init.d/ddclient
$ sudo update-rc.d ddclient defaults
Start the first time by hand:
$ sudo service ddclient start
To debug it:
$ sudo ddclient -daemon=0 -debug -verbose -noquiet
Docker with Traefik Proxy Setup
Here are the services we'll be using:
- Traefik - The reverse proxy - with a Web UI
- Portainer - Monitoring UI for Docker
- Home Assistant - Home automation platform
- Nextcloud - File-hosting service
- Gogs - Self-hosted Git service
- MariaDB - Fork of the MySQL RDBMS
Install Docker & Docker Compose
Install Docker using the convenience script from Docker Documentation:
$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh
Install pip if you don't have it already:
$ sudo apt-get install python-pip
Then install docker-compose via pip:
$ sudo pip install docker-compose
Add your user to the docker group
$ sudo usermod -aG docker ${USER}
Logout and login again to apply changes.
Docker Folders and Permissions
$ mkdir ~/docker
$ sudo chmod -R 775 ~/docker
.htpasswd File
This will be used to password protect plain HTTP pages on your server (like the traefik dashboard), via an .htpasswd file. Use this HTPASSWD Generator to create a combination with this structure:
sample_username:sample_password
Make a shared folder to use for docker containers, with a .htpasswd file inside, then paste the generated password:
$ mkdir ~/docker/shared
$ nano ~/docker/shared/.htpasswd
Docker environment variables
Make an .env file in your docker folder to keep all of the environment variables you'll be using:
$ touch ~/docker/.env
Before writing the .env file, get the PUID(uid) and PGID(gid) of your user with the id command:
$ id
Edit your .env file similar to this:
$ cd ~/docker
$ nano .env
.env:
PUID=1004
PGID=1004
TZ=Europe/Athens
USERDIR=/home/pi
MYSQL_ROOT_PASSWORD=your_secure_password_here
HTTP_USERNAME=htpasswd_file_username
HTTP_PASSWORD=htpasswd_file_password
DOMAINNAME=cloud.sample.com
CLOUDFLARE_EMAIL=sample@sample.com
CLOUDFLARE_API_KEY=your_cloudflare_api_key
Prepare the Traefik configs
Create new folders for Traefik and ACME:
$ mkdir ~/docker/traefik
$ mkdir ~/docker/traefik/acme
Docker cannot create missing files (only directories). So we will need to create an empty file for Traefik docker container to use. You also need to set the proper permissions:
$ touch ~/docker/traefik/acme/acme.json
$ chmod 600 ~/docker/traefik/acme/acme.json
Next we move on to the traefik.toml file:
$ touch ~/docker/traefik/traefik.toml
$ nano ~/docker/traefik/traefik.toml
traefik.toml:
#debug = true
logLevel = "ERROR" #DEBUG, INFO, WARN, ERROR, FATAL, PANIC
InsecureSkipVerify = true
defaultEntryPoints = ["https", "http"]
# WEB interface of Traefik - it will show web page with overview of frontend and backend configurations
[web]
address = ":8080"
[web.auth.basic]
usersFile = "/shared/.htpasswd"
# Force HTTPS
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
# Let's encrypt configuration
[acme]
email = "your@email.com"
storage="/etc/traefik/acme/acme.json"
entryPoint = "https"
acmeLogging=true
onDemand = false #create certificate when container is created
[acme.dnsChallenge]
provider = "cloudflare"
delayBeforeCheck = 0
[[acme.domains]]
main = "CLOUD.YOURDOMAIN.COM"
[[acme.domains]]
main = "*.CLOUD.YOURDOMAIN.COM"
[[acme.domains]]
main = "OTHERSUBDOMAIN.YOURDOMAIN.COM"
# Connection to docker host system (docker.sock), pointing to your raspberry
[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "CLOUD.YOURDOMAIN.COM"
watch = true
# This will hide all docker containers that don't have explicitly
# set label to "enable"
exposedbydefault = false
Create the proxy network for docker
After this you'll have an internal docker network (default) and an external, used by Traefik (traefik_proxy):
$ docker network create traefik_proxy
The docker-compose file
This will start all services, along with the reverse-proxy.
Create the file:
$ touch ~/docker/docker-compose.yml
$ nano ~/docker/docker-compose.yml
docker-compose.yml:
version: "3.6"
services:
traefik:
hostname: traefik
image: traefik:latest
container_name: traefik
restart: always
domainname: ${DOMAINNAME}
networks:
- default
- traefik_proxy
ports:
- "80:80"
- "443:443"
- "8080:8080"
environment:
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
- CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY}
labels:
- "traefik.enable=true"
- "traefik.backend=traefik"
- "traefik.frontend.rule=Host:traefik.${DOMAINNAME}"
- "traefik.port=8080"
- "traefik.docker.network=traefik_proxy"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${USERDIR}/docker/traefik:/etc/traefik
- ${USERDIR}/docker/shared:/shared
portainer:
image: portainer/portainer:arm #use the arm image for this
container_name: portainer
restart: always
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${USERDIR}/docker/portainer/data:/data
- ${USERDIR}/docker/shared:/shared
environment:
- TZ=${TZ}
homeassistant:
container_name: homeassistant
restart: always
image: homeassistant/raspberrypi3-homeassistant:0.73.0
volumes:
- ${USERDIR}/docker/homeassistant:/config
- /etc/localtime:/etc/localtime:ro
- ${USERDIR}/docker/shared:/shared
privileged: true
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
labels:
- "traefik.enable=true"
- "traefik.backend=homeassistant"
- "traefik.frontend.rule=Host:homeassistant.${DOMAINNAME}"
- "traefik.port=8123"
- "traefik.docker.network=traefik_proxy"
nextcloud:
container_name: nextcloud
restart: always
image: nextcloud:latest
links:
- mariadb
volumes:
- /mnt/nextcloud:/var/www/html
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
networks:
- traefik_proxy
- default
labels:
- "traefik.enable=true"
- "traefik.backend=nextcloud"
- "traefik.frontend.rule=Host:nextcloud.${DOMAINNAME}"
- "traefik.docker.network=traefik_proxy"
- "traefik.port=80"
gogs:
container_name: gogs
restart: always
image: gogs/gogs-rpi
links:
- mariadb
ports:
- "10022:22"
volumes:
- /mnt/gogs:/data
labels:
- "traefik.enable=true"
- "traefik.backend=gogs"
- "traefik.frontend.rule=Host:git.${DOMAINNAME}"
- "traefik.port=3000"
- "traefik.docker.network=traefik_proxy"
mariadb:
image: hypriot/rpi-mysql:latest
container_name: "mariadb"
hostname: mariadb
volumes:
- ${USERDIR}/docker/mariadb:/var/lib/mysql
ports:
- target: 3306
published: 3306
protocol: tcp
mode: host
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
networks:
traefik_proxy:
external:
name: traefik_proxy
default:
driver: bridge
If you want to use Owncloud instead of Nextcloud, use the arm32v7 image:
image: arm32v7/owncloud:latest
Start the server
$ docker-compose -f ~/docker/docker-compose.yml up -d
Make sure you have forwarded ports 80 & 443 on your router! Docker will download the images you specified and start. Traefik will then generate the certificates from Let's Encrypt and store them to the acme.json file. The first time you run it it will take some minutes. You can monitor the process with this command:
$ docker stats
Maintenance
Stopping/restarting containers. For single containers use:
$ docker-compose stop CONTAINER-NAME
To stop all the containers spawned by docker-compose:
$ docker-compose -f ~/docker/docker-compose.yml down
Cleaning up docker from leftover images:
$ docker system prune
$ docker image prune
$ docker volume prune
Backups:
To take backups the only thing you need to do is copy the docker-mounted folders to another drive (~/docker, /mnt/nextcloud, /mnt/gogs).
Updates:
The update process is simple:
$ docker-compose -f ~/docker/docker-compose.yml down && docker-compose -f ~/docker/docker-compose.yml up -d
Docker will download images with the latest tag if newer versions are found. If you're not using latest tags, just edit your docker-compose.yml before restarting.
Sources: