nginx-proxy and Portainer: Multiple applications in a single server
Working in a web agency there was always the need for testing applications online and showing them to clients. As each project is developed in a particular environment (language, database, server, version), one question arise: How to serve all those applications in a single domain?
One possibility is to use docker. This way the environments are separated in containers and we can expose each in distinct ports of the host. However the routing through ports is not very practical. A better approach is to use the DNS to map each application to a particular subdomain. To this end we can use a reverse proxy.
Reverse Proxy
A reverse proxy is a server that typically sits in front of web servers and forwards clients requests to those web servers also providing functionalities like SSL, load balancer and cache.
NGINX can be configured as a reverse proxy forwarding the request to docker containers. This configuration can become a bit complex especially when using SSL. Also, when the container is updated it is necessary to also update the NGINX configuration which increases the chance of an error and consumes more time. In this article there is a step-by-step example for this configuration.
nginx-proxy
One commonly used package that abstracts and helps with the configuration and maintenance of this scenario is nginx-proxy. With only a few parameters it creates a NGINX reverse proxy container that is reloaded when the target containers configurations are updated.
To use nginx-proxy you must have docker installed in your system and execute the following command:
docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
Then each target container must have an exposed port to the host and the application address stored in a environment variable VIRTUAL_HOST.
docker run -e VIRTUAL_HOST=app1.mysite.com ...
I prefer to use docker-compose because with it you don’t need to execute long commands as the definitions are defined in a file. In the example bellow I use a reverse proxy with 3 target applications:
- Apache + PHP
- PM2 + Node.js
- Tomcat + Java
version: '2'
services:
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
app1:
image: php:7.4-apache
environment:
- VIRTUAL_HOST=app1.mysite.com
app2:
image: keymetrics/pm2
environment:
- VIRTUAL_HOST=app2.mysite.com
app3:
image: tomcat
environment:
- VIRTUAL_HOST=app3.mysite.com
docker-letsencrypt-nginx-proxy-companion
It is possible to use the package docker-letsencrypt-nginx-proxy-companion alongside with nginx-proxy to create, renew and use SSL certificates from Let’s Encrypt on the target containers. To use it you need to create a fex volumes on the nginx-proxy container, add the docker-letsencrypt-nginx-proxy-companion container and set the LETSENCRYPT_HOST environment variable for each target container.
version: '2'
services:
nginx-proxy:
container_name: nginx-proxy
image: jwilder/nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- "/etc/nginx/vhost.d"
- "/usr/share/nginx/html"
- "/var/run/docker.sock:/tmp/docker.sock:ro"
- "/etc/nginx/certs"
letsencrypt-nginx-proxy-companion:
container_name: letsencrypt-nginx-proxy-companion
image: jrcs/letsencrypt-nginx-proxy-companion
restart: always
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
volumes_from:
- "nginx-proxy"
app1:
image: php:7.4-apache
environment:
- VIRTUAL_HOST=app1.mysite.cocm
- LETSENCRYPT_HOST=app1.mysite.com
app2:
image: keymetrics/pm2
environment:
- VIRTUAL_HOST=app2.mysite.com
- LETSENCRYPT_HOST=app2.mysite.com
app3:
image: tomcat
environment:
- VIRTUAL_HOST=app3.mysite.com
- LETSENCRYPT_HOST=app3.mysite.com
Portainer
To facilitate the applications management, I recommend Portainer. It provides an well organized and practical graphic interface to manage containers, images, volumes, networks, stacks and docker configurations. You can also access the container through the browser and control users permissions which is interesting as not all users access the server, know how to use docker or should have control over the applications.
To install Portainer via docker-compose follow the example bellow and then access the Portainer GUI at port 9000 of the host via browser. In the first login you should define a password but it can be predefined. Check the documentation.
version: '2'
services:
portainer:
image: portainer/portainer
command: -H unix:///var/run/docker.sock
restart: always
ports:
- 9000:9000
- 8000:8000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
volumes:
portainer_data:
With this configuration Portainer is accessed via HTTP. To enable HTTPS you must add a certificate. Here is an example on how to generate a certificate with OpenSSL.
$ openssl genrsa -out portainer.key 2048
$ openssl ecparam -genkey -name secp384r1 -out portainer.key
$ openssl req -new -x509 -sha256 -key portainer.key -out portainer.crt -days 3650
Use the example bellow to attach the certificate to the Portainer container where ~/local-certs is the path to the certificate (portainer.crt) and key (portainer.key) in the host. You can also use Certbot to generate certificates. Check the documentation.
version: '2'
services:
portainer:
image: portainer/portainer
restart: always
ports:
- 9000:9000
- 8000:8000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/local-certs:/certs
- portainer_data:/data
command:
-H unix:///var/run/docker.sock
--ssl
--sslcert /certs/portainer.crt
--sslkey /certs/portainer.key
volumes:
portainer_data:
Conclusion
Now you have distinct containerized applications in a single server, accessed by subdomains via HTTPS and a web GUI tool to manage it.