Deploying Ring Web Applications using Docker
Chapter Author: Youssef Saeed
This tutorial guides you through containerizing a Ring application with Docker and setting up a reverse proxy for cloud deployment. We will explore three popular reverse proxy solutions: Nginx for a traditional, robust setup, Traefik for modern, dynamic routing, and Caddy for ultimate simplicity and automated HTTPS. You will learn how to create a production-ready setup using Docker Compose.
1. Introduction
When deploying Ring web applications to the cloud, containerization with Docker is the standard for ensuring consistency across environments. A reverse proxy is essential for managing incoming traffic, handling SSL/TLS termination, and routing requests to your application container.
This tutorial will demonstrate three common architectures:
Docker with Nginx: A classic, high-performance setup where Nginx acts as a reverse proxy. This is great for stable configurations and serving static files.
Docker with Traefik: A modern edge router that automatically discovers services and configures routing, making it ideal for dynamic, microservice-based environments.
Docker with Caddy: An incredibly simple, modern web server that provides automatic HTTPS by default, making secure deployments effortless.
We will use the ysdragon/ring:light
Docker image, which is optimized for web development.
2. Prerequisites
Before you begin, ensure you have the following installed on your system:
(Optional, for Path B: Traefik)
htpasswd
for generating passwords. It’s often included inapache2-utils
(Debian/Ubuntu) orhttpd-tools
(CentOS).A basic understanding of the Ring programming language.
A basic understanding of command-line interfaces.
3. Dockerizing Your Ring Application
First, we’ll create a simple Ring web application and package it into a Docker image.
Creating a Sample Ring Application
Create a new directory for your project, navigate into it, and then create a file named app.ring
with the following content:
load "httplib.ring"
# Main Execution Block
oServer = new Server {
# Route for the root path
route(:Get, "/", :mainRoute)
# Listen on all available network interfaces on port 8080
listen("0.0.0.0", 8080)
}
func mainRoute
# Set content type to HTML
oServer.setContent("<!DOCTYPE html>
<html>
<head><title>Ring HTTPLib App</title></head>
<body>
<h1>Hello from Ring HTTPLib!</h1>
<p>This is a simple Ring application running inside a Docker container.</p>
</body>
</html>", "text/html")
This application uses HTTPLib
to listen on port 8080
and serve a simple HTML page.
Creating the Dockerfile
In the same project directory, create a file named Dockerfile
(no extension):
# Use a lightweight Ring image as the base
FROM ysdragon/ring:light
# Set the working directory inside the container
WORKDIR /app
# Copy the application source code
COPY . .
# The ysdragon/ring:light image uses the RING_FILE environment variable
# to determine which script to run. We'll set this in docker compose.
# It also automatically exposes port 8080.
4. Local Development with Docker Compose
Now, choose one of the following paths for your local development setup.
—
Path A: Using Nginx as a Reverse Proxy
This approach uses Nginx to forward traffic from http://localhost
to your Ring application container.
1. Create the Nginx Configuration
Create a directory named nginx
, and inside it, create a file named nginx.conf
:
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://ring-app-dev:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
2. Create the Docker Compose File for Development
Create a docker-compose.dev.yml
file in your project root:
# docker-compose.dev.yml
services:
ring-app:
build: .
container_name: ring-app-dev
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
nginx:
image: nginx:latest
container_name: nginx-proxy-dev
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- ring-app
3. Run It
Open your terminal and run:
docker compose -f docker-compose.dev.yml up --build
You can now access your application at http://localhost
.
—
Path B: Using Traefik for Dynamic Routing & Local HTTPS
This approach uses Traefik to automatically detect the Ring application and provide routing, including generating a self-signed SSL certificate for a secure local development environment.
1. Create the Docker Compose File for Development
Create a docker-compose.dev.yml
in your project root. If you created one for Nginx, replace its contents with this.
# docker-compose.dev.yml
services:
traefik:
image: traefik:latest
container_name: traefik-dev
command:
- --api.insecure=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --serversTransport.insecureSkipVerify=true
ports:
- "80:80"
- "443:443"
- "8081:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ring-app:
build: .
container_name: ring-app-dev
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.ring-app-http.rule=Host(`ring.localhost`)"
- "traefik.http.routers.ring-app-http.entrypoints=web"
- "traefik.http.routers.ring-app-secure.rule=Host(`ring.localhost`)"
- "traefik.http.routers.ring-app-secure.entrypoints=websecure"
- "traefik.http.routers.ring-app-secure.tls=true"
- "traefik.http.services.ring-app-service.loadbalancer.server.port=8080"
2. Configure Your Hosts File
To make ring.localhost
work on your machine, edit your hosts
file to point it to your local machine.
Linux/macOS:
sudo nano /etc/hosts
Windows: Open Notepad as Administrator and open
C:\Windows\System32\drivers\etc\hosts
Add the following line:
127.0.0.1 ring.localhost
3. Run It
Open your terminal and run:
docker compose -f docker-compose.dev.yml up --build
You can now access:
Your App (HTTP):
http://ring.localhost
Your App (HTTPS):
https://ring.localhost
(Your browser will show a security warning. Proceed anyway.)Traefik Dashboard:
http://localhost:8081
—
Path C: Using Caddy for Simplicity & Auto-HTTPS
This approach uses Caddy to serve your application. Caddy automatically provisions a self-signed certificate for local development, providing HTTPS with zero effort.
1. Create the Caddyfile for Development
Create a file named Caddyfile.dev
in your project root:
# Caddyfile.dev
{
# For local development, allow Caddy to generate and trust self-signed certs
local_certs
}
ring.localhost {
# Reverse proxy requests to our Ring application container
reverse_proxy ring-app-dev:8080
}
2. Create the Docker Compose File for Development
Create a docker-compose.dev.yml
file. If you created one for another path, replace its contents with this.
# docker-compose.dev.yml
services:
ring-app:
build: .
container_name: ring-app-dev
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
caddy:
image: caddy:latest
container_name: caddy-proxy-dev
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile.dev:/etc/caddy/Caddyfile
- caddy_data:/data
volumes:
caddy_data:
3. Configure Your Hosts File
To make ring.localhost
work, edit your hosts
file to point it to your local machine.
Linux/macOS:
sudo nano /etc/hosts
Windows: Open Notepad as Administrator and open
C:\Windows\System32\drivers\etc\hosts
Add the following line:
127.0.0.1 ring.localhost
4. Run It
Open your terminal and run:
docker compose -f docker-compose.dev.yml up --build
You can now access:
Your App (HTTPS):
https://ring.localhost
(Your browser may show a one-time warning. Accept it to proceed.)
5. Deploying to Production
Path A: Nginx with Let’s Encrypt SSL
This setup uses Nginx alongside Certbot. To solve the initial startup puzzle (where Nginx needs a certificate to start, but Certbot needs a server to get a certificate), we will use an initialization script that leverages Certbot’s standalone
mode. This runs a temporary webserver on port 80 to get the certificate, cleanly separating the one-time setup from the long-running application stack.
Prerequisites for Production:
A cloud VM with Docker and Docker Compose installed.
A registered domain name (e.g.,
your-domain.com
).A DNS “A” record pointing your domain (e.g.,
ring.your-domain.com
) to your VM’s public IP address.Your server’s firewall must allow inbound traffic on port
80
(for the SSL challenge) and443
(for the final HTTPS traffic).
1. Create the Production Nginx Configuration
This will be the final configuration that Nginx uses once SSL is active. Create a directory named nginx-prod
, and inside it, create a file named default.conf
:
# nginx-prod/default.conf
server {
listen 80;
server_name ring.your-domain.com; # CHANGE THIS
# Certbot validation and redirect all other traffic to HTTPS
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name ring.your-domain.com; # CHANGE THIS
ssl_certificate /etc/letsencrypt/live/ring.your-domain.com/fullchain.pem; # CHANGE THIS
ssl_certificate_key /etc/letsencrypt/live/ring.your-domain.com/privkey.pem; # CHANGE THIS
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://ring-app-prod:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
2. Create the Docker Compose File for Production
This file defines the final, long-running state of your services. It will be used after you have obtained the certificates.
Create a docker-compose.prod.yml
file:
# docker-compose.prod.yml
services:
ring-app:
build: .
container_name: ring-app-prod
restart: unless-stopped
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
nginx:
image: nginx:latest
container_name: nginx-proxy-prod
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx-prod/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- ring-app
certbot:
image: certbot/certbot
container_name: certbot-prod
restart: unless-stopped
volumes:
- ./certbot/conf:/etc/letsencrypt:rw
- ./certbot/www:/var/www/certbot:rw
command: renew --quiet
3. Create the Automated Initialization Script
This self-contained script handles the one-time setup by running a temporary Certbot container. Create a file named init-letsencrypt.sh
in your project root.
#!/bin/bash
# =================================================================
# This script uses a standalone 'docker run' command to get the
# initial SSL certificate, making it independent of docker compose.
# =================================================================
# Stop immediately if any command fails
set -e
# --- Configuration ---
DOMAIN="ring.your-domain.com"
EMAIL="your-email@example.com"
# --- End of Configuration ---
# Function for colored output
color_echo() { echo -e "\e[$1m$2\e[0m"; }
# Check if certificates already exist
if [ -d "certbot/conf/live/$DOMAIN" ]; then
color_echo "33" "Certificates for $DOMAIN already exist. Exiting."
exit 0
fi
# Step 1: Create required directories and download SSL parameters
color_echo "34" "Creating directories and downloading recommended SSL parameters..."
mkdir -p ./certbot/conf ./certbot/www
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "./certbot/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "./certbot/conf/ssl-dhparams.pem"
# Step 2: Request the certificate using a temporary standalone Certbot container
color_echo "34" "Requesting Let's Encrypt certificate for $DOMAIN..."
# Temporarily stop any services running on port 80
color_echo "33" "Stopping any running services on port 80..."
docker stop nginx-proxy-prod >/dev/null 2>&1 || true
# Run the certbot container
docker run --rm \
-p 80:80 \
-v "./certbot/conf:/etc/letsencrypt" \
-v "./certbot/www:/var/www/certbot" \
certbot/certbot certonly \
--standalone \
--email $EMAIL \
--agree-tos \
--no-eff-email \
-d $DOMAIN
if [ $? -ne 0 ]; then
color_echo "31" "Certbot failed. Please check the logs."
exit 1
fi
color_echo "32" "\n================================================="
color_echo "32" " SSL setup complete!"
color_echo "32" " You can now start the full stack with:"
color_echo "32" " docker compose -f docker-compose.prod.yml up -d"
color_echo "32" "================================================="
4. The Automated Deployment Process
Your deployment is now a simple, reliable two-stage process.
First, perform the one-time initialization:
Edit the script: Open
init-letsencrypt.sh
and replace the placeholderDOMAIN
andEMAIL
with your actual information.Make the script executable:
chmod +x init-letsencrypt.sh
Run the script. It will stop any container using port 80, get the certificate, and then exit.
./init-letsencrypt.sh
Finally, launch your production stack:
Once the script succeeds, the certificates exist on your host machine. Now you can start your full application stack. Nginx will find the certificates and start correctly.
docker compose -f docker-compose.prod.yml up -d
Your application is now live, secure, and configured for automatic certificate renewals.
Path B: Traefik with Let’s Encrypt SSL
This setup uses Traefik to automatically provision and renew a real SSL certificate from Let’s Encrypt while routing traffic to your application.
Prerequisites for Production:
A cloud VM with Docker, Docker Compose, and
htpasswd
installed.A registered domain name (e.g.,
your-domain.com
).DNS “A” records pointing your domains (e.g.,
ring.your-domain.com
andtraefik.your-domain.com
) to your VM’s public IP address.
1. Prepare Production Files
On your cloud VM, prepare the environment for Traefik.
# 1. Create a directory for Let's Encrypt data
mkdir letsencrypt
# 2. Create the JSON file that will store certificate data
touch letsencrypt/acme.json
# 3. Set strict permissions on the file for security
chmod 600 letsencrypt/acme.json
# Generate a user:password for the dashboard. Replace 'admin' as desired.
htpasswd -c .htpasswd admin
2. Create the Docker Compose File for Production
Create a new docker-compose.prod.yml
file.
# docker-compose.prod.yml
services:
traefik:
image: traefik:latest
container_name: traefik-prod
restart: unless-stopped
command:
- --api=true # Enable the API
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.email=your-email@example.com # CHANGE THIS
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
- ./.htpasswd:/etc/traefik/.htpasswd:ro # Mount the password file
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.my-auth.basicauth.usersfile=/etc/traefik/.htpasswd"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.your-domain.com`)" # CHANGE THIS
- "traefik.http.routers.traefik-dashboard.service=api@internal"
- "traefik.http.routers.traefik-dashboard.middlewares=my-auth"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=myresolver"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
ring-app:
build: .
container_name: ring-app-prod
restart: unless-stopped
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.ring-app-secure.rule=Host(`ring.your-domain.com`)" # CHANGE THIS
- "traefik.http.routers.ring-app-secure.entrypoints=websecure"
- "traefik.http.routers.ring-app-secure.tls.certresolver=myresolver"
- "traefik.http.services.ring-app-service.loadbalancer.server.port=8080"
3. Deploy
Copy your project directory to your VM. Then, SSH into your VM and run Docker Compose:
docker compose -f docker-compose.prod.yml up -d --build
Your application is live at
https://ring.your-domain.com
.Your secure dashboard is at
https://traefik.your-domain.com
.
Path C: Caddy with Automatic Let’s Encrypt SSL
Caddy’s configuration for production is nearly identical to development. It will automatically detect that you are using a public domain and fetch a real SSL certificate from Let’s Encrypt.
Prerequisites for Production:
A cloud VM with Docker and Docker Compose installed.
A registered domain name (e.g.,
your-domain.com
).A DNS “A” record pointing your domain (e.g.,
ring.your-domain.com
) to your VM’s public IP address.
1. Create the Production Caddyfile
Create a Caddyfile.prod
file. This is the entire configuration needed.
# Caddyfile.prod
{
email your-email@example.com # CHANGE THIS
}
ring.your-domain.com { # CHANGE THIS
reverse_proxy ring-app-prod:8080
}
2. Create the Docker Compose File for Production
Create a new docker-compose.prod.yml
file.
# docker-compose.prod.yml
services:
ring-app:
build: .
container_name: ring-app-prod
restart: unless-stopped
environment:
- RING_FILE=app.ring
volumes:
- .:/app:ro
caddy:
image: caddy:latest
container_name: caddy-proxy-prod
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # For HTTP/3
volumes:
- ./Caddyfile.prod:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- ring-app
volumes:
caddy_data:
caddy_config:
3. Deploy
Copy your project directory to your VM. Then, SSH into your VM and run Docker Compose:
docker compose -f docker-compose.prod.yml up -d --build
That’s it! Caddy automatically handles SSL certificate acquisition and renewal.
6. Conclusion
This tutorial has shown you how to containerize a Ring application and deploy it with three powerful reverse proxy solutions.
Nginx is an excellent choice for its performance and stability, especially when your routing needs are simple and well-defined.
Traefik shines in dynamic environments, automating service discovery, routing, and SSL management, which drastically simplifies deployment and scaling.
Caddy is the champion of simplicity, providing an incredibly easy configuration experience with fully automated HTTPS, making it perfect for developers who want to get a secure site running in minutes.
By understanding these approaches, you can choose the right tool for your project and build a robust, scalable, and secure deployment pipeline for your Ring applications in the cloud.