This project is a complete (and maybe overkill) web server environment for static websites. You can try to fully use it or to pick only a few elements.
This project is not production grade. My main goal is to use it as a playground to improve my own skills and to test its integration with other web mini-projects. Any improvement suggestion will be greatly appreciated. I'm especially eager to learn on the securization and optimization aspects.
The dockerized ecosystem is composed of :
I won't go into details on the reasons that made me choose one tool over another but I guess I could explain it with a few words. It's a mix between Open-Source, user personal data privacy, self-hosted solutions and ease of implementation and documentation.
You are going to need a couple of things to make this work:
docker network create frontend
docker network create backend
Since we will be using Traefik to automatically discover our backend services, it will require access to the docker socket. This could be a security concern as explained in the Docker Daemon Attack Surface documentation. Traefik provides a few solutions in its Docker setup documentation and I chose to use the Docker socket proxy provided by Tecnativa.
Let's create the docker-compose.yaml
file at the root of our project.
version: "3.8"
services:
dockerproxy:
container_name: docker-proxy
environment:
CONTAINERS: 1
image: tecnativa/docker-socket-proxy
networks:
- backend
ports:
- 2375
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
backend:
external: true
That's it. With this configuration we have added the Docker proxy and it only allows the listing of the containers through the `CONTAINERS: 1` environment variable. Next step will be to configure Traefik to use the Docker proxy as a provider instead of the Docker socket file.
Create a traefik
folder at the root of our project.
Create Traefik configuration file traefik.yaml
in this folder.
api:
dashboard: true
debug: false
entryPoints:
http:
address: ":80"
https:
address: ":443"
providers:
docker:
endpoint: "tcp://docker-proxy:2375"
watch: true
exposedbydefault: false
network: backend
log:
filePath: "/data/traefik.log"
format: json
level: WARN
accessLog:
filePath: "/data/traefik_access.log"
format: json
certificatesResolvers:
http:
acme:
email: [email protected]
storage: acme.json
httpChallenge:
entryPoint: http
We have :
backend
network per default. Prevented Traefik from exposing all containers per default (only those with the label traefik.enable=true
will be exposed)http
certificate resolver so Traefik can use an ACME provider like Let's Encrypt for automatic SSL certificate generation.Now we must create the empty acme.json
file in the traefik folder. It will be used to keep the automatically generated SSL certificates.
Caution! You MUST set the permissions for this file to 600. If you don't you will get a self-explanatory error in your Traefik logs. If you are on a Windows environment you may have to use wsl or your chmod 600 acme.json
command won't work.
Next, we must add the Traefik service to the docker-compose.yaml
file.
services:
traefik:
depends_on:
- dockerproxy
image: traefik:v2.9.6
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- frontend
- backend
ports:
- 80:80
- 443:443
volumes:
- /etc/localtime:/etc/localtime:ro
- ./traefik/traefik.yaml:/traefik.yaml:ro
- ./traefik/acme.json:/acme.json
- ./traefik/traefik_access.log:/data/traefik_access.log
- ./traefik/traefik.log:/data/traefik.log
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`$TRAEFIK_DOMAIN`)"
- "traefik.http.middlewares.traefik-auth.basicauth.users=(`$TRAEFIK_BASIC_AUTH`)"
- "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
- "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`$TRAEFIK_DOMAIN`)"
- "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=http"
- "traefik.http.routers.traefik-secure.service=api@internal"
I'm assuming you're already familiar with Docker and Docker Compose, that's why I will only explain the labels
part.
Let's see all of them :
traefik.enable=true
: We are enabling this container.
traefik.http.routers.traefik.entrypoints=http
: We are allowing requests from the http entrypoint defined in the config file.
traefik.http.routers.traefik.rule=Host(
$TRAEFIK_DOMAIN)
: We are telling Traefik on which domain this service will respond. Here the value is defined in a separate .env
environment file. I will come back to this later.
traefik.http.middlewares.traefik-auth.basicauth.users=(
$TRAEFIK_BASIC_AUTH)
: We have enabled the dashboard so we are adding the Traefik's Basic Auth middleware for authentication. The value is provided by the environment file.
traefik.http.routers.traefik.middlewares=traefik-https-redirect
: We are adding a middleware to the http router.
traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https
: We are asking to the previous middleware to redirect all HTTP incoming traffic to the HTTPS entrypoint.
traefik.http.routers.traefik-secure.entrypoints=https
: We are allowing requests from the https entrypoint defined in the config file.
traefik.http.routers.traefik-secure.rule=Host(
$TRAEFIK_DOMAIN)
: We are telling Traefik on which domain the secure routing will respond.
traefik.http.routers.traefik-secure.middlewares=traefik-auth
: We are enabling the previously created traefik-auth
middleware on the secure router. This way we will be able to authenticate ourselves on the Traefik's dashboard with the appropriate credentials.
traefik.http.routers.traefik-secure.tls=true
: We are enabling automatic TLS certificate generation.
traefik.http.routers.traefik-secure.tls.certresolver=http
: We are telling Traefik to use the SSL certificate resolver specified in the configuration file.
traefik.http.routers.traefik-secure.service=api@internal
: We are referencing a special service which is created automatically when the API is enabled (which is our case since we enabled the dashboard).
Finally we need to create the .env
file I mentioned earlier at the root of the project to hold all the Compose environment variables.
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
The username:hashedpassword value can be generated with the htpasswd
tool. You will need to install the apache2-utils
package on your OS to use it.
htpasswd -nb user password
user:$apr1$sYbLhg33$u3k7DomrUjAvUYJ5inffF/
Do NOT use such obvious credentials, this is an example...
IMPORTANT : If the hashed string has any
$
you will need to modify them to be$$
or Docker Compose will think it's a variable. In the given example, the final credentials value will beuser:$$apr1$$sYbLhg33$$u3k7DomrUjAvUYJ5inffF/
and you will be able to authenticate yourself with the usernameuser
and the passwordpassword
.
Okay, at this point you can try to run :
docker-compose up -d
Traefik service should start with its dashboard available at https://your.traefik.domain.com. HTTPS should be enabled, automatically generating a TLS certificate in your acme.json
file. All HTTP traffic should be redirected to HTTPS.
Note : You may encounter some weird behaviour with Traefik log and access log files. If that's the case create them manually and restart your Docker environment.
I will quickly go through the process of creating a new static website with Jekyll before showing how to integrate it to our docker-compose.yaml
file. Feel free to skip the next part if you're not interested. At the end all you need is your website files into a website/_site
directory (create both directories if you didn't use Jekyll).
Install Jekyll
gem install bundler jekyll
Create a new Jekyll site with default theme
In project directory execute
jekyll new website
Generate site HTML code executing the command
jekyll build
HTML generated code is under _site
directory.
Feel free to read Jekyll documentation for more informations.
We will use a basic Apache image to mount our static website in the Docker container as a bind mount of /usr/local/apache2/htdocs
.
website:
depends_on:
- traefik
image: httpd:2.4-alpine
container_name: "website"
hostname: "website"
restart: unless-stopped
networks:
- backend
volumes:
- ./website/_site:/usr/local/apache2/htdocs/
ports:
- target: 80
protocol: tcp
labels:
- "traefik.enable=true"
- "traefik.http.routers.website.entrypoints=http"
- "traefik.http.routers.website.rule=Host(`$WEBSITE_DOMAIN`)"
- "traefik.http.middlewares.website-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.website.middlewares=website-https-redirect"
- "traefik.http.routers.website-secure.entrypoints=https"
- "traefik.http.routers.website-secure.rule=Host(`$WEBSITE_DOMAIN`)"
- "traefik.http.routers.website-secure.tls=true"
- "traefik.http.routers.website-secure.tls.certresolver=http"
- "traefik.http.routers.website-secure.service=website"
- "traefik.http.services.website.loadbalancer.server.port=80"
Do not forget to add the WEBSITE_DOMAIN
value to your .env
file.
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
WEBSITE_DOMAIN=your.website.domain.com
Now you can try to stop our dockerized environment and restart it.
docker-compose down -v
docker-compose up -d
Your static website should be available at https://your.website.domain.com
Now that we have our static website up and running behind Traefik reverse proxy, let's use Prometheus to collect metrics.
We need to update our traefik.yaml
file to enable Prometheus and set it up. The official Traefik documentation on the matter is there.
api:
dashboard: true
debug: false
metrics:
prometheus:
addRoutersLabels: true
addEntryPointsLabels: true
addServicesLabels: true
entryPoint: metrics
entryPoints:
http:
address: ":80"
https:
address: ":443"
metrics:
address: ":8080"
providers:
docker:
endpoint: "tcp://docker-proxy:2375"
watch: true
exposedbydefault: false
network: backend
log:
filePath: "/data/traefik.log"
format: json
level: WARN
accessLog:
filePath: "/data/traefik_access.log"
format: json
certificatesResolvers:
http:
acme:
email: [email protected]
storage: acme.json
httpChallenge:
entryPoint: http
We have added the metrics entry point and configured Traefik to :
Now let's create a prometheus
directory at the root of our project and a prometheus.yaml
file inside.
global:
scrape_interval: 30s
scrape_timeout: 10s
evaluation_interval: 5s
scrape_configs:
- job_name: prometheus
scheme: http
static_configs:
- targets:
- prometheus:9090
- job_name: traefik
scheme: http
static_configs:
- targets:
- traefik:8080
With this configuration we are asking Prometheus to collect both its own metrics and Traefik metrics. This is a very basic setup, I highly recommend to read the official documentation, starting there.
We are going to add prometheus to our services in the docker-compose.yaml
file.
prometheus:
image: prom/prometheus:v2.41.0
container_name: prometheus
restart: unless-stopped
networks:
- backend
volumes:
- ./prometheus/:/etc/prometheus/
- /etc/localtime:/etc/localtime:ro
- ./prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
command:
- "--config.file=/etc/prometheus/prometheus.yaml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
- "--web.console.templates=/usr/share/prometheus/consoles"
ports:
- target: 9090
protocol: tcp
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.entrypoints=http"
- "traefik.http.routers.prometheus.rule=Host(`$PROMETHEUS_DOMAIN`)"
- "traefik.http.middlewares.prometheus-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.prometheus.middlewares=prometheus-https-redirect"
- "traefik.http.routers.prometheus-secure.entrypoints=https"
- "traefik.http.routers.prometheus-secure.rule=Host(`$PROMETHEUS_DOMAIN`)"
- "traefik.http.routers.prometheus-secure.tls=true"
- "traefik.http.routers.prometheus-secure.tls.certresolver=http"
- "traefik.http.routers.prometheus-secure.service=prometheus"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
Also add the 8080 port to traefik service configuration.
traefik:
...
ports:
- 80:80
- 443:443
- 8080:8080
...
And don't forget to add the PROMETHEUS_DOMAIN
value to your .env
file!
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
WEBSITE_DOMAIN=your.website.domain.com
PROMETHEUS_DOMAIN=your.prometheus.domain.com
Now you can try to restart everything.
docker-compose down -v
docker-compose up -d
Prometheus should be available at https://your.prometheus.domain.com and you should see its metrics at https://your.prometheus.domain.com/metrics. There are also a lot of other possibilities available with Prometheus such as alerting etc. Check the doc!
Ok we got the metrics. But maybe they would be easier to exploit with some dashboards don't you think? Let's add Grafana to our stack.
First create a grafana
directory.
I'm providing an example of dasboard in this project. All you need to do is to copy the content of the provisioning
folder from this repository into the grafana
directory.
The provided dashboard will use prometheus metrics as a datasource.
You should read the Grafana official documentation on the matter.
You will need to create a grafana.env
file into the grafana
directory.
GF_AUTH_ANONYMOUS_ENABLED=true
GF_AUTH_BASIC_ENABLED=false
GF_AUTH_PROXY_ENABLED=false
GF_USERS_ALLOW_SIGN_UP=false
GF_INSTALL_PLUGINS=grafana-piechart-panel
We are overriding Grafana configuration with environment variables as explained there. This configuration is designed for a development/testing environment because it allows anonymous authentication. No login will be required to access to our dashboards.
Now let's add grafana service to our docker-compose.yaml
grafana:
image: grafana/grafana:9.3.2
restart: unless-stopped
container_name: grafana
volumes:
- ./grafana:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
- /etc/localtime:/etc/localtime:ro
env_file:
- grafana/grafana.env
depends_on:
- prometheus
networks:
- backend
ports:
- target: 3000
protocol: tcp
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.entrypoints=http"
- "traefik.http.routers.grafana.rule=Host(`$GRAFANA_DOMAIN`)"
- "traefik.http.middlewares.grafana-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.grafana.middlewares=grafana-https-redirect"
- "traefik.http.routers.grafana-secure.entrypoints=https"
- "traefik.http.routers.grafana-secure.rule=Host(`$GRAFANA_DOMAIN`)"
- "traefik.http.routers.grafana-secure.tls=true"
- "traefik.http.routers.grafana-secure.tls.certresolver=http"
- "traefik.http.routers.grafana-secure.service=grafana"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
Finally add the GRAFANA_DOMAIN
value to your .env
file!
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
WEBSITE_DOMAIN=your.website.domain.com
PROMETHEUS_DOMAIN=your.prometheus.domain.com
GRAFANA_DOMAIN=your.grafana.domain.com
And restart everything.
docker-compose down -v
docker-compose up -d
Grafana should be up at https://your.grafana.domain.com and if you go to the Dashboards panel you should see a traefik dashboard available. Start playing!
What about adding some web analytics solution to our website? Matomo is a free and open-source alternative to Google Analytics and this is the tool I have chosen. We will add 2 containers to our Docker environment. One for Matomo database and one for its frontend administration website.
First, let's create the directory structure we need in our project :
matomo
directory at the root of the project.matomo
directory create a db
directory and a www-data
directory.
We will use those as bind mounts for Matomo persistent data (database and website).Let's create Matomo's environment file db.env
in the matomo
directory. It will be used by both containers.
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=matomo
MYSQL_USER=matomo
MYSQL_PASSWORD=password
MATOMO_DATABASE_ADAPTER=mysql
MATOMO_DATABASE_TABLES_PREFIX=matomo_
MATOMO_DATABASE_USERNAME=matomo
MATOMO_DATABASE_PASSWORD=password
MATOMO_DATABASE_DBNAME=matomo
NOTE : Do not keep those default password values !
Now we are going to add Matomo MariaDB database service to the docker-compose.yaml
file.
db:
image: mariadb
container_name: mariadb
networks:
- backend
command: --max-allowed-packet=64MB
restart: always
volumes:
- ./matomo/db:/var/lib/mysql
env_file:
- ./matomo/db.env
NOTE: I didn't invent anything here. This is taken from Matomo official Docker project
And now let's add the Matomo frontend container.
matomo:
depends_on:
- db
image: matomo
container_name: matomo
restart: always
networks:
- backend
volumes:
- ./matomo/www-data:/var/www/html
environment:
- MATOMO_DATABASE_HOST=db
env_file:
- ./matomo/db.env
ports:
- target: 80
protocol: tcp
labels:
- "traefik.enable=true"
- "traefik.http.routers.matomo.entrypoints=http"
- "traefik.http.routers.matomo.rule=Host(`$MATOMO_DOMAIN`)"
- "traefik.http.middlewares.matomo-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.matomo.middlewares=matomo-https-redirect"
- "traefik.http.routers.matomo-secure.entrypoints=https"
- "traefik.http.routers.matomo-secure.rule=Host(`$MATOMO_DOMAIN`)"
- "traefik.http.routers.matomo-secure.tls=true"
- "traefik.http.routers.matomo-secure.tls.certresolver=http"
- "traefik.http.routers.matomo-secure.service=matomo"
- "traefik.http.services.matomo.loadbalancer.server.port=80"
Now, don't forget to add the value for your Matomo domain in the main .env
file.
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
WEBSITE_DOMAIN=your.website.domain.com
PROMETHEUS_DOMAIN=your.prometheus.domain.com
GRAFANA_DOMAIN=your.grafana.domain.com
MATOMO_DOMAIN=your.matomo.domain.com
For the last part of the installation you will need to access the administration website which should be running after you restart you docker-environment.
docker-compose down -v
docker-compose up -d
If everything went well you should now be able to access Matomo at https://your.matomo.domain.com. You can now finish the installation by following the official documentation.
To start tracking your website you will also have to include the Matomo javascript code snippet into your website pages. I won't go into the details here as it is a trivial step and well documented.
Finally, the last part of this project will be to add Remark42 which is a simple commenting engine so your website visitors can leave comments (usefull for a blog).
Create the following directories :
remark42
directory at the root of the project.var
directory into remark42
directory. It will be used as a docker bind mount.Create the environment file .env
into the remark42
directory.
REMARK_URL=https://your.remark42.domain.com
SECRET=<remark42_secret>
STORE_BOLT_PATH=/srv/var/db
BACKUP_PATH=/srv/var/backup
SITE=<site_id>
AUTH_ANON=true
site_id
: It should be the same id than the one you will add to the Remark42 javascript code snippet which you will add to your website.AUTH_ANON=true
: We are allowing anonymous comment for testing purpose. You can remove this part later and add social or email logins. Read the official documentation thereAdd remark42 service to the docker-compose.yaml
file
remark42:
image: umputun/remark42:v1.11.2
container_name: "remark42"
hostname: "remark42"
restart: unless-stopped
networks:
- backend
volumes:
- ./remark42/var:/srv/var
ports:
- target: 8080
protocol: tcp
env_file:
- ./remark42/remark42.env
environment:
- TIME_ZONE=Europe/Paris
labels:
- "traefik.enable=true"
- "traefik.http.routers.remark42.entrypoints=http"
- "traefik.http.routers.remark42.rule=Host(`$REMARK42_DOMAIN`)"
- "traefik.http.middlewares.remark42-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.remark42.middlewares=remark42-https-redirect"
- "traefik.http.routers.remark42-secure.entrypoints=https"
- "traefik.http.routers.remark42-secure.rule=Host(`$REMARK42_DOMAIN`)"
- "traefik.http.routers.remark42-secure.tls=true"
- "traefik.http.routers.remark42-secure.tls.certresolver=http"
- "traefik.http.routers.remark42-secure.service=remark42"
- "traefik.http.services.remark42.loadbalancer.server.port=8080"
- "traefik.http.middlewares.remark42.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.remark42.headers.accesscontrolalloworiginlist=*"
- "traefik.http.middlewares.remark42.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.remark42.headers.addvaryheader=true"
We have added a few extra Traefik labels here to resolve some CORS issues as explained here.
Add your Remark42 domain to the main .env
file
TRAEFIK_BASIC_AUTH=username:hashedpassword
TRAEFIK_DOMAIN=your.traefik.domain.com
WEBSITE_DOMAIN=your.website.domain.com
PROMETHEUS_DOMAIN=your.prometheus.domain.com
GRAFANA_DOMAIN=your.grafana.domain.com
MATOMO_DOMAIN=your.matomo.domain.com
REMARK42_DOMAIN=your.remark42.domain.com
Restart everything.
docker-compose down -v
docker-compose up -d
Congratulations we are done!
You should have your complete web environment up and running and you can start playing with it.
In my next project I plan to add automatized backups, a security layer for all those non-encrypted password, integrate this environment into a Kubernetes cluster and ease its deployment with Ansible. But this is another story.