prioritizes security hardening, automated TLS, and idempotent configuration.
Phase 1: Infrastructure Bootstrap
Provision a DigitalOcean Droplet running Ubuntu 22.04 LTS. For standard web workloads, a 1GB RAM / 1 vCPU instance is sufficient. Note the public IPv4 address.
Security Hardening:
Never operate as the root user for routine tasks. Create a dedicated deployment user with sudo privileges.
# Connect to the Droplet
ssh root@198.51.100.42
# Create deployment user
adduser deploy
usermod -aG sudo deploy
# Configure SSH key authentication for the deploy user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
Phase 2: DNS Resolution Strategy
Configure DNS records in the Namecheap dashboard to point the domain to the Droplet. This example uses acme-corp.io.
Record Configuration:
-
Type: A Record
-
Host: @
-
Value: 198.51.100.42
-
TTL: Automatic
-
Type: A Record
-
Host: www
-
Value: 198.51.100.42
-
TTL: Automatic
This configuration ensures both the apex domain and the www subdomain resolve to the same infrastructure endpoint.
Phase 3: Web Server Orchestration
Install Nginx and configure the firewall to restrict traffic to essential ports.
# Switch to deployment user
ssh deploy@198.51.100.42
# Update package index and install Nginx
sudo apt update
sudo apt install nginx -y
# Configure UFW firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Document Root and Configuration:
Establish a structured directory layout and create the Nginx server block.
# Create document root
sudo mkdir -p /srv/web/acme-corp/public
# Create index file
echo "<h1>Acme Corp Infrastructure Active</h1>" | sudo tee /srv/web/acme-corp/public/index.html
# Set ownership
sudo chown -R deploy:www-data /srv/web/acme-corp
Create the Nginx server block at /etc/nginx/sites-available/acme-corp.conf:
server {
listen 80;
listen [::]:80;
server_name acme-corp.io www.acme-corp.io;
root /srv/web/acme-corp/public;
index index.html;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
try_files $uri $uri/ =404;
}
}
Enable and Validate:
Symlink the configuration to sites-enabled and validate syntax before reloading.
sudo ln -s /etc/nginx/sites-available/acme-corp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Phase 4: TLS Automation
Automate SSL certificate provisioning using Certbot with Let's Encrypt.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d acme-corp.io -d www.acme-corp.io
Certbot modifies the Nginx configuration to listen on port 443, installs the certificate, and configures automatic renewal via a systemd timer. Verify the renewal process:
sudo certbot renew --dry-run
Pitfall Guide
| Pitfall | Explanation | Remediation |
|---|
| Root Privilege Escalation | Operating as root increases the attack surface and risk of accidental system damage. | Create a non-root user with sudo access; disable root SSH login in sshd_config. |
| DNS Propagation Mirage | Engineers assume DNS failure when changes haven't propagated globally due to TTL caching. | Use dig acme-corp.io +short to verify local resolution; check global status via DNS checker tools. |
| Firewall Lockout | Enabling UFW before allowing SSH rules results in immediate loss of remote access. | Always run ufw allow OpenSSH before ufw enable. |
| Nginx Syntax Crash | Reloading Nginx with invalid configuration syntax causes the web server to stop serving traffic. | Always run nginx -t before systemctl reload nginx. |
| Certbot Renewal Failure | Manual Nginx config changes can break Certbot's renewal hooks, leading to expired certificates. | Test renewal with certbot renew --dry-run; avoid manual edits to Certbot-managed blocks. |
| Mixed Content Leakage | Serving HTTP assets over an HTTPS connection triggers browser security warnings. | Use protocol-relative URLs or enforce upgrade-insecure-requests in Nginx headers. |
| Server Name Mismatch | Nginx returns the default server block if server_name does not match the request header. | Ensure server_name includes all required domains and subdomains; verify with curl -I. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Rationale | Cost Impact |
|---|
| Single Web Application | A-Record Delegation | Minimal configuration overhead; sufficient for basic routing. | No additional cost. |
| Multi-Service Architecture | Nameserver Delegation | Centralized DNS management; enables API-driven subdomain provisioning. | No additional cost; higher operational complexity. |
| High-Traffic Production | Managed DNS (e.g., Cloudflare) | DDoS protection, caching, and advanced routing features. | Potential subscription cost; improved performance. |
| Development/Staging | A-Record Delegation | Fast setup; easy to tear down; no provider lock-in. | No additional cost. |
Configuration Template
Production-grade Nginx configuration with security headers, gzip compression, and static asset caching.
server {
listen 80;
listen [::]:80;
server_name acme-corp.io www.acme-corp.io;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name acme-corp.io www.acme-corp.io;
ssl_certificate /etc/letsencrypt/live/acme-corp.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/acme-corp.io/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /srv/web/acme-corp/public;
index index.html;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Static Asset Caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ =404;
}
}
Quick Start Guide
- Provision Droplet: Create an Ubuntu 22.04 Droplet on DigitalOcean; note the IPv4 address.
- Configure DNS: Add A records for
@ and www in Namecheap pointing to the Droplet IP.
- Initialize Server: SSH into the Droplet, create a
deploy user, install Nginx, and configure UFW.
- Deploy Config: Create the document root, write the Nginx server block, enable the site, and reload Nginx.
- Enable HTTPS: Run Certbot to obtain and install an SSL certificate; verify automatic renewal.