My Hosting Setup

Since some time now, I am running my own private server with a couple of different services. The website which you are currently reading is also hosted on this server. I want to give a short overview about the technical background and summarize some of my experience from the last years.

Hosting Provider

When I started, I was using Netcup but switched to Hetzner since a couple of weeks. In general, I was looking for a hosting provider with Linux root servers located in Germany. Ideally, the company would run their servers on renewable electricity and have a fair price.

Why did I switch from Netcup to Hetzner? Hetzner has from my point of view a couple of advantages:

The prices are slightly higher, however I am much more flexible and can down- and upgrade the machine whenever I need.

OS and Provisioning

As an operating system, I chose the latest long-term stable release of Ubuntu, mainly because I used it as my private OS on my laptop before. It is stable and well suited for my use-case.

Since I good introduced to Ansible through my work, it became my preferred way of administrating remote servers. The advantages are that everything is scripted and visible in code which allows me to quickly bring up a new server. Furthermore, I can use pre-made roles for security settings (like this one) so I can easily follow best-practices.

Reverse Proxy, Certs And Services

Currently I am running the following services on the server:

All services run in Docker containers which makes dependency management and upgrading easy. As a reverse-proxy, I use Traefik which goes very well together with Docker containers. It provides automatic service discovery via labels given in docker-compose files. I maintain one code repository where each service resides in a folder with its own docker-compose file. It is structured like this:

.
├── diyary
│   └── docker-compose.yml
├── docker-compose.yaml
├── files
│   └── docker-compose.yaml
├── funkwhale
│   ├── docker-compose.yaml
│   ├── funkwhale_proxy.conf
│   └── funkwhale.template
├── hedgedoc
│   └── docker-compose.yml
├── nginx
│   └── docker-compose.yaml
├── nginx_marika
│   └── docker-compose.yaml
├── peertube
│   └── docker-compose.yml
├── README.md
├── rocketchat
│   └── docker-compose.yml
├── traefik_dyn.yaml
└── traefik.yaml

In the root of the repo is a docker-compose which starts Traefik and Portainer as infrastructure services:

version: '3.8'

services:
  traefik:
    image: traefik:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yaml:/traefik.yaml:ro
      - ./traefik_dyn.yaml:/dynamic/traefik_dyn.yaml:ro
    networks:
      - web

  portainer_server:
    image: portainer/portainer-ce
    command: -H unix:///var/run/docker.sock
    volumes:
      - portainer_data:/data
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    ports:
      - "127.0.0.1:9000:9000"
    restart: always

volumes:
  portainer_data:

networks:
  web:
    external: true

Note that I use an external network called “web” for Traefik. All services residing in this network will be automatically picked up by Traefik because I configured Traefik with the options

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: web

The network is created via Ansible before any service is started. One note on the port bindings of Docker: it is very important to bind things like portainer with the interface explicitly specified (here 127.0.0.1), otherwise you might expose the service to the open internet. Docker is not restricted by your firewall settings!

Now let’s take a look at a single service, for example my website running with nginx:

version: "3"

services:
  website:
    image: nginx:latest
    volumes:
      - /home/services/website:/usr/share/nginx/html:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.website.rule=Host(`www.jenspetit.de`, `jenspetit.de`)"
      - "traefik.http.routers.website.entrypoints=websecure"
      - "traefik.http.routers.website.tls=true"
      - "traefik.http.routers.website.tls.certresolver=netcup"
      - "traefik.http.services.website.loadbalancer.server.port=80"
    networks:
      - web
    restart: unless-stopped

networks:
  web:
    external: true

Through the labels given in the file, Traefik will automatically create the correct routes and generate a valid certificate via Let’s Encrypt and a http challenge. Therefore, I don’t have to manually edit any configuration when deploying a new service, I simply start the corresponding docker-compose which I do via executing an Ansible playbook.

Conclusion

Right now, I am happy with the setup I have. It allows me to quickly add new services via Docker and Traefik withouth being too complex. At some point, I might want to add a VPN connection from my private laptop, so I don’t need to do any port forwarding to administrative interfaces or internal services.