Skip to content

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=0 Generate an executable with statically linked dependencies to avoid missing dynamic libraries at runtime.

  • -ldflags="-s -w": Strip debugging information to significantly reduce binary file size

  • Build 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 myapp

3. 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.log

4. 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.service

Operation 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 -f

5. 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 .htpasswd Members 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/.htpasswd

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:

subdomainUseAccess method
dash.yourdomain.comNuxt console frontendUser browser access
api.yourdomain.comGolang API serviceNuxt server-side calls + direct browser calls

When Nginx to api.yourdomain.com Enable auth_basic After:

  • Nuxt server-side API calls: carry Authorization: Basic Request headers → and the API's own Bearer Token Authentication conflict

  • Swagger / 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 ​

PlanImplementation methodadvantagesshortcoming
Option 1: Nginx path proxyNuxt through /api Proxy path to local http://127.0.0.1:2759, API domain is not exposed externallySimple, no additional configuration requiredThe 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 authenticationNot occupied Authorization Header; supports Swagger documentation access; supports inter-service calls (Service Token); team email domain authorization; one-click disable on launchThe domain needs to be hosted on Cloudflare (the free plan is sufficient).
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.html Unable to access

  • The API cannot be called by independent API clients (such as Postman or external services).

Use Cloudflare Access to replace Nginx basic authentication, completely resolving the conflict issue.

Key configuration points:

subdomainAccess strategyActionRules
dash.yourdomain.comTeam visitAllowEmail with @company.com ending
api.yourdomain.comService invocationService AuthService Token
api.yourdomain.comTeam visitAllowEmail with @company.com Ending

Core strengths:

  • Does not occupy Authorization head: API's Bearer Token Authentication 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 requirementsRecommended Solution
Only protect console (dash.), the API is fully open to the publicNginx Basic Authentication
Protect API (api.) but the Swagger documentation needs to be accessibleCloudflare 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:

CharacteristicsCloudflare Origin CALet's Encrypt
Applicable scenariosThe domain is hosted on Cloudflare.Any domain (general)
Configuration complexitySimpleMedium
Certificate renewal15-year validity period, no renewal required90 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.key

6.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 nginx

7. Cloudflare DNS and SSL Mode Configuration ​

7.1 Add DNS A Record ​

TypeNameContentProxy Status
AapiYour 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/health

9. Common Troubleshooting ​

phenomenonPossible reasonsSolution
Accessing it displays the default Nginx page.sites-enabled there are others in default_server Configuration conflictEnsure that unnecessary items are deleted or removed.default Configuration link
502 Bad GatewayThe 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 overriddenCheck 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 incorrectConfirm set_real_ip_from Includes all Cloudflare ranges, and real_ip_header CF-Connecting-IP; Correct
Certificate error SSL_ERROR_BAD_CERT_DOMAINThe 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 user

11. Summary of Following FHS Principles ​

File typeStorage pathDescription
executable file/usr/local/bin/myapp-apiserverLocally compiled programs are not part of the distribution repository.
configuration file/etc/myapp/config.yamlGlobal configuration directory and files
log file/var/log/myapp/apiserver.logApplication log (append mode)
Work/Persistent Directory/var/lib/myapp/Application status data, working directory
System services/usr/lib/systemd/system/myapp-apiserver.serviceSystem 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:

placeholderExample valueExplanation
myappyourappGo project name (directory/service name prefix)
myapp-apiserveryourapp-apiserverExecutable file name
api.yourdomain.comapi.example.comAPI service full domain name
yourdomain.comexample.comCloudflare certificate primary domain
27598080Go service listening port
Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]