English
Complete deployment solution for Golang projects in production environment
Use cases
Pre-release environment/Canary validation environment/Internal test environment
Core needs: Need to hide it from the external network and allow access only to team members or specific IP ranges, but do not want to implement login logic in the application code.
Architecture reminder
If your frontend is SSR frameworks such as Nuxt.js / Next.js, and use Dedicated API subdomain(such as api.yourdomain.com), please be sure to read.Section 5.4 Explanation of the conflict.Strongly recommend using Cloudflare Access instead of Nginx basic authentication.
This guide follows the Linux Filesystem Hierarchy Standard (FHS), combines Systemd, Nginx, and Cloudflare, and provides a standardized, reusable deployment process from code compilation to going live.
1. Project Build (Local)
Assume your Golang project is named myapp, the main program entry output is myapp-apiserver.
bash
# Enable Go module proxy
go env -w GOPROXY=https://goproxy.cn,direct
# Cross-compile for the Linux environment
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o myapp-apiserver .CGO_ENABLED=0Generate an executable with statically linked dependencies to avoid missing dynamic libraries at runtime.-ldflags="-s -w": Strip debugging information to significantly reduce binary file sizeBuild artifacts:
myapp-apiserver
2. Server Environment Preparation (Using Ubuntu/Debian as an Example)
bash
# Update the system and install the necessary tools.
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx apache2-utils
# Create a dedicated service user (security best practice, optional)
# sudo groupadd -r myapp
# sudo useradd -r -g myapp -s /sbin/nologin -M myapp3. Standardized file deployment (following FHS)
According to FHS specification, place the file in the standard directory:
bash
# 1. Executable file -> /usr/local/bin
sudo cp myapp-apiserver /usr/local/bin/
sudo chmod 755 /usr/local/bin/myapp-apiserver
# If using a dedicated user: sudo chown myapp:myapp /usr/local/bin/myapp-apiserver
# 2. Configuration file -> /etc/myapp/
sudo mkdir -p /etc/myapp
sudo cp config.yaml /etc/myapp/config.yaml
sudo chown -R root:root /etc/myapp
sudo chmod 755 /etc/myapp
sudo chmod 644 /etc/myapp/config.yaml
# 3. Working directory and data persistence -> /var/lib/myapp/
sudo mkdir -p /var/lib/myapp
sudo chown root:root /var/lib/myapp # If using a dedicated user, then chown myapp:myapp
# 4. Log directory -> /var/log/myapp/
sudo mkdir -p /var/log/myapp
sudo touch /var/log/myapp/apiserver.log
sudo chown root:root /var/log/myapp/apiserver.log4. Create the Systemd service file
Create /usr/lib/systemd/system/myapp-apiserver.service:
ini
[Unit]
Description="MyApp API Server"
Requires=network-online.target
After=network-online.target
[Service]
WorkingDirectory=/var/lib/myapp
StandardOutput=append:/var/log/myapp/apiserver.log
StandardError=append:/var/log/myapp/apiserver.log
User=root
Group=root
ExecStart=/usr/local/bin/myapp-apiserver --config-dir /etc/myapp --config-name config.yaml
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGTERM
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Alias=myapp.serviceOperation command:
bash
# Reload systemd and start the service
sudo systemctl daemon-reload
sudo systemctl enable myapp-apiserver.service # Start on boot
sudo systemctl start myapp-apiserver.service
# Use alias management (optional)
sudo systemctl enable myapp.service --now
# View status
sudo systemctl status myapp-apiserver.service
# Gracefully reload configuration (send HUP signal)
sudo systemctl reload myapp-apiserver.service
# View logs (view the files directly, because StandardOutput/Error is configured)
sudo tail -f /var/log/myapp/apiserver.log
# View via journal (if also needed)
sudo journalctl -u myapp-apiserver -f5. Basic Auth Canary Access Control
5.1 Why is Basic Auth needed? (Canary release scenario)
Before the official launch, the pre-release environment (Staging/Preview) usually needs to be hidden from the external network and accessible only to team members. Enable Nginx auth_basic Has the following advantages:
Zero code intrusion: No need to implement login logic in the Go application; the Nginx layer handles interception uniformly.
Take effect quickly: Modify
.htpasswdMembers can be added or removed in the file without restarting the service.In conjunction with a grayscale strategy: Stackable IP whitelists for flexible access control
5.2 Generate Password File
bash
# Create password file (requires -c the first time)
sudo htpasswd -c /etc/nginx/.htpasswd your_username
# Remove -c when adding more users
sudo htpasswd /etc/nginx/.htpasswd another_user
# Verify password file
sudo cat /etc/nginx/.htpasswd5.3 Advanced Configuration: Basic Auth + IP Whitelist (Recommended)
If you want to allow password-free access from internal company IPs while requiring a password for external access, you can use the following configuration:
nginx
location / {
# Meeting any one condition is sufficient.
satisfy any;
# Allow password-free access from the company's egress IP
allow 203.0.113.0/24; # Company office network segment
allow 10.0.0.0/8; # Intranet VPN subnet
deny all; # Reject all other IPs
# IPs not in the whitelist need to enter a password
auth_basic "Restricted Access - Internal Preview";
auth_basic_user_file /etc/nginx/.htpasswd;
# ... other proxy_pass configurations
}5.4 ⚠️ Important reminder: Conflict issue between Nginx basic authentication and independent API domain names
If your frontend project is SSR frameworks such as Nuxt.js / Next.js, and use Dedicated API subdomain(such as api.yourdomain.com), then Nginx basic authentication will cause serious architectural problems.
Problem Scenario
Assume your architecture is as follows:
| subdomain | Use | Access method |
|---|---|---|
dash.yourdomain.com | Nuxt console frontend | User browser access |
api.yourdomain.com | Golang API service | Nuxt server-side calls + direct browser calls |
When Nginx to api.yourdomain.com Enable auth_basic After:
Nuxt server-side API calls: carry
Authorization: BasicRequest headers → and the API's ownBearer TokenAuthentication conflictSwagger / OpenAPI documentation: Completely inaccessible (a password must be entered in the browser, but the API documentation page cannot pop up an authentication dialog)
External partner debugging: Unable to provide a stable API testing entry point
Solution Comparison
| Plan | Implementation method | advantages | shortcoming |
|---|---|---|---|
| Option 1: Nginx path proxy | Nuxt through /api Proxy path to local http://127.0.0.1:2759, API domain is not exposed externally | Simple, no additional configuration required | The Swagger documentation is completely unavailable; the API cannot be called by external services. |
| Option 2: Cloudflare Access(Recommended) | Use Cloudflare Access zero-trust access control instead of Nginx basic authentication | Not occupied Authorization Header; supports Swagger documentation access; supports inter-service calls (Service Token); team email domain authorization; one-click disable on launch | The domain needs to be hosted on Cloudflare (the free plan is sufficient). |
Option 1: Nginx path proxy (not recommended)
nginx
# Add a path proxy to the dash.yourdomain.com configuration.
location /api/ {
proxy_pass http://127.0.0.1:2759/; # Proxy to local API
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Note: there is no auth_basic here
}Question:
https://dash.yourdomain.com/api/swagger/index.htmlUnable to accessThe API cannot be called by independent API clients (such as Postman or external services).
Option 2: Cloudflare Access (Strongly Recommended)
Use Cloudflare Access to replace Nginx basic authentication, completely resolving the conflict issue.
Key configuration points:
| subdomain | Access strategy | Action | Rules |
|---|---|---|---|
dash.yourdomain.com | Team visit | Allow | Email with @company.com ending |
api.yourdomain.com | Service invocation | Service Auth | Service Token |
api.yourdomain.com | Team visit | Allow | Email with @company.com Ending |
Core strengths:
Does not occupy
Authorizationhead: API'sBearer TokenAuthentication is completely unaffected.Swagger documentation accessible: Team members can view the API documentation normally after logging in
Supports inter-service calls: Implement seamless invocation via Service Token
One-click shutdown on launch: No need to modify any code or Nginx configuration.
📖Complete Configuration Guide: Refer to 《Zero Trust access control based on Cloudflare Access: a perfect solution from canary release to official launch》 article, which includes detailed steps for activating the Zero Trust Free plan, configuring Service Tokens, and setting up policies.
Summary and recommendations
| Your requirements | Recommended Solution |
|---|---|
Only protect console (dash.), the API is fully open to the public | Nginx Basic Authentication |
Protect API (api.) but the Swagger documentation needs to be accessible | Cloudflare Access |
| Protect the API and require seamless service-to-service invocation. | Cloudflare Access |
| Aim for zero-configuration switchover at launch. | Cloudflare Access |
If you are using an architecture with Nuxt + a separate API domain, it is strongly recommended to adopt the Cloudflare Access solution directly to avoid future refactoring costs.
6. Configure Nginx reverse proxy + basic authentication
6.0 SSL Certificate Solution Selection
Before configuring Nginx HTTPS, you need to choose an SSL certificate solution. The following is a comparison of two mainstream free solutions:
| Characteristics | Cloudflare Origin CA | Let's Encrypt |
|---|---|---|
| Applicable scenarios | The domain is hosted on Cloudflare. | Any domain (general) |
| Configuration complexity | Simple | Medium |
| Certificate renewal | 15-year validity period, no renewal required | 90 days, automatic renewal |
| CDN acceleration | ✅ Cloudflare proxy needs to be enabled | ❌ |
| DDoS protection | ✅ | ❌ |
| Independent deployment | ❌ Depends on Cloudflare | ✅ Completely independent |
Selection recommendations:
If you have already hosted your domain on Cloudflare, it is recommended to use Cloudflare Origin CA(The schemes in sections 6.1–6.3 below)
If your domain is not on Cloudflare, or you want to deploy independently, it is recommended to use Let's Encrypt(Reference From Scratch: Deploying a Nuxt Application to an Ubuntu 22.04 Server SSL configuration section in )
The following uses Cloudflare Origin CA Configure using this as an example; if you use Let's Encrypt, refer to the blog post linked above.
6.1 Prepare a Cloudflare Origin CA Certificate
From Cloudflare Dashboard → SSL/TLS → Origin Server, download the certificate:
Note
Cloudflare Origin CA certificates are based on Zone (Zone)issued, a single certificate can cover the primary domain and all subdomains (such as *.yourdomain.com). Therefore, the certificate file name should use Primary domain.
bash
# Place the certificate (named using the main domain)
sudo mkdir -p /etc/nginx/ssl
sudo cp yourdomain.com.pem /etc/nginx/ssl/
sudo cp yourdomain.com.key /etc/nginx/ssl/
sudo chmod 600 /etc/nginx/ssl/yourdomain.com.key6.2 Create Nginx Site Configuration
Create /etc/nginx/sites-available/api.yourdomain.com.conf:
nginx
# Redirect HTTP to HTTPS
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$server_name$request_uri;
}
# HTTPS configuration
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
# Security response headers
add_header X-Frame-Options "DENY" 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;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Global Basic Authentication (Canary Release Control)
auth_basic "Restricted Access - Internal Preview";
auth_basic_user_file /etc/nginx/.htpasswd;
# Cloudflare Origin CA certificate (using the primary domain certificate)
ssl_certificate /etc/nginx/ssl/yourdomain.com.pem;
ssl_certificate_key /etc/nginx/ssl/yourdomain.com.key;
# SSL optimization configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
# Log
access_log /var/log/nginx/api.yourdomain.com-access.log;
error_log /var/log/nginx/api.yourdomain.com-error.log;
# Cloudflare IP address ranges (get the real client IP)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
# Primary endpoint agent
location / {
proxy_pass http://127.0.0.1:2759; # Assume the Go service listens on this port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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;
}
# Static asset caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
proxy_pass http://127.0.0.1:2759;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check authentication exemption (for load balancer probing)
location = /health {
auth_basic off;
proxy_pass http://127.0.0.1:2759;
proxy_set_header Host $host;
}
}6.3 Enable the site and reload Nginx
bash
# Enable site configuration
sudo ln -s /etc/nginx/sites-available/api.yourdomain.com.conf /etc/nginx/sites-enabled/
# Check syntax and reload
sudo nginx -t && sudo systemctl reload nginx7. Cloudflare DNS and SSL Mode Configuration
7.1 Add DNS A Record
| Type | Name | Content | Proxy Status |
|---|---|---|---|
| A | api | Your server's public IP | ✅ On (orange cloud) |
7.2 SSL/TLS Settings
In the Cloudflare SSL/TLS panel, set the mode to Full (strict)
This requires the origin server to have a valid certificate; we have configured an Origin CA certificate, which meets this requirement.
8. Post-deployment verification
bash
# 1. Check Go service status
sudo systemctl status myapp-apiserver
# 2. Check Nginx proxy connectivity (local)
curl -H "Host: api.yourdomain.com" http://127.0.0.1/health
# 3. Passed the Cloudflare external network test (authentication available upon request)
curl -I https://api.yourdomain.com
# 4. Authenticated access
curl -I -u your_username:your_password https://api.yourdomain.com
# 5. Verify HSTS and security headers
curl -I https://api.yourdomain.com | grep -i "strict-transport-security"
# 6. Verify that health checks are exempt from authentication
curl -I https://api.yourdomain.com/health9. Common Troubleshooting
| phenomenon | Possible reasons | Solution |
|---|---|---|
| Accessing it displays the default Nginx page. | sites-enabled there are others in default_server Configuration conflict | Ensure that unnecessary items are deleted or removed.default Configuration link |
| 502 Bad Gateway | The Go service is not started or the port does not match. | sudo systemctl status myapp-apiserver; Check Nginx proxy_pass port |
| No authentication prompt (direct 200) | auth_basic Not correctly inherited or overridden | Check location whether there is in the block auth_basic off; or syntax error |
| The logs show that the client IP is a Cloudflare node. | real_ip_header Configuration missing or order incorrect | Confirm set_real_ip_from Includes all Cloudflare ranges, and real_ip_header CF-Connecting-IP; Correct |
Certificate error SSL_ERROR_BAD_CERT_DOMAIN | The certificate domain name does not match the actual access domain name. | Confirm that the certificate is issued for the primary domain (e.g.yourdomain.com), can cover *.yourdomain.com |
10. Quick Maintenance Command Reference
bash
# Recompile and replace the binary file
go build -o myapp-apiserver .
sudo systemctl stop myapp-apiserver
sudo cp myapp-apiserver /usr/local/bin/
sudo systemctl start myapp-apiserver
# Reload after modifying the configuration file (send HUP signal)
sudo vi /etc/myapp/config.yaml
sudo systemctl reload myapp-apiserver
# View real-time logs
sudo tail -f /var/log/myapp/apiserver.log
sudo tail -f /var/log/nginx/api.yourdomain.com-access.log
# Reload after updating the Nginx configuration
sudo nginx -t && sudo systemctl reload nginx
# Manage Basic Auth users
sudo htpasswd /etc/nginx/.htpasswd new_user # Add user
sudo htpasswd -D /etc/nginx/.htpasswd old_user # Delete user11. Summary of Following FHS Principles
| File type | Storage path | Description |
|---|---|---|
| executable file | /usr/local/bin/myapp-apiserver | Locally compiled programs are not part of the distribution repository. |
| configuration file | /etc/myapp/config.yaml | Global configuration directory and files |
| log file | /var/log/myapp/apiserver.log | Application log (append mode) |
| Work/Persistent Directory | /var/lib/myapp/ | Application status data, working directory |
| System services | /usr/lib/systemd/system/myapp-apiserver.service | System unit |
| Web proxy configuration | /etc/nginx/sites-available/ and /etc/nginx/sites-enabled/ | Nginx site configuration |
Quick memorization mnemonic
Binary search bin, configuration find etc,
Search logs var, static search share,
Compile it yourself local, big tech software release opt.
This layout ensures the system's cleanliness and maintainability, and keeps it consistent with the behavior of other Linux software packages.
Appendix: Placeholder Replacement Checklist
During deployment, please replace the following placeholders with actual values:
| placeholder | Example value | Explanation |
|---|---|---|
myapp | yourapp | Go project name (directory/service name prefix) |
myapp-apiserver | yourapp-apiserver | Executable file name |
api.yourdomain.com | api.example.com | API service full domain name |
yourdomain.com | example.com | Cloudflare certificate primary domain |
2759 | 8080 | Go service listening port |