Atomic Deployment of Go Binaries on Ubuntu with Cron
Abstract
This article presents a production-ready deployment strategy for Go applications on Ubuntu servers using GitLab CI/CD and cron scheduling. The approach leverages Go’s static compilation capabilities to eliminate runtime dependencies, while maintaining a persistent cron schedule that automatically executes updated binaries without service interruption. The deployment pipeline uses SSH key authentication to securely transfer compiled binaries to the target server, replacing executable files atomically while active cron jobs continue running. This architecture achieves zero-downtime updates by utilizing absolute file paths, allowing scheduled tasks to automatically load new versions on their next execution cycle. The strategy is particularly well-suited for Golang services and scheduled batch processes, providing a lightweight alternative to container orchestration or complex deployment frameworks while maintaining enterprise-grade security through GitLab CI/CD variables and SSH key management.
Core Components of the Deployment
The deployment architecture relies on four critical, interconnected components:
- The Golang Binary: Self-contained and statically compiled.
- The Ubuntu Server: Hosts the executable and associated configuration (
.env). - The Cron Job: A single, persistent schedule entry that executes the binary.
- GitLab CI/CD: Manages the build and file replacement process.
Step 1: The Golang Advantage (Build and Ship)
The deployment strategy is optimized by Golang’s compilation model.
When the build command is executed:
G
O
O
S=linux
G
O
A
R
C
H=amd64 go build -ldflags="-s -w" -o ./bin/myApp ./cmd/appThe resulting file (./bin/myapp) is a single, statically linked executable containing the entire Go runtime and application logic. This means the target server needs nothing installed except standard Linux libraries—no Go environment, no virtual machine, and no dependencies to manage.
Step 2: The Manual Server Setup (Cron and Environment)
The server side is prepared once and should ideally never be touched again for routine updates.
A. Directory Structure
The executable and its environment file are placed in a consistent, known location, for instance:
- Application Root:
/opt/myapp/ - Executable Path:
/opt/myapp/bin/myapp - Configuration:
/opt/myapp/.env
B. The Persistent Cron Entry
The crucial element is the Cron entry, which is manually configured via crontab -e. It must be written robustly to handle the unique execution environment of Cron, especially the pathing to the configuration file (.env).
The Goal: Run every 2 hours, starting at the top of the hour.
The Command:
0 */2 * * * cd /opt/myapp/ && ./bin/myapp >> /opt/myapp/logs/myapp_run.log 2>&1Why the cd Command is Essential:
Since Cron jobs typically run with the home directory (~) as the working directory, the cd /opt/myapp/ command is prefixed to ensure the Go application's startup process correctly finds the .env file located in the application root, before executing the binary (./bin/myapp).
Step 3: Zero-Downtime File Replacement via GitLab CI
The GitLab CI pipeline automates the secure deployment of the new binary. The power of this setup is that the running cron job is never modified or restarted. The job simply replaces the file that the cron job executes.
A. The Deployment Script (.gitlab-ci.yml)
The following complete GitLab CI configuration defines a single myapp A job that handles the entire process: setting up SSH access, compiling the Go binary, and securely copying it to the target server via SCP.
# .gitlab-ci.yml# This pipeline builds the Go application and securely deploys the binary # to the
Ubuntu server via SCP, ensuring the cron job uses the latest file.image: golang:1.22 # Use a modern
Go image for buildingvariables: # Define the target paths on the server APP_ROOT: "/opt/myapp/"
BIN_PATH: "/opt/myapp/bin/myapp" # Path for the binary built within the GitLab Runner
LOCAL_BINARY: "./bin/myapp"stages: - deploydeploy_myapp: stage: deploy # This job runs only when
code is pushed to the 'main' branch only: - main # --- Setup Script (before_script) --- #
This section sets up the environment for deployment (SSH access) before_script: # 1. Install SSH
client tools and necessary libraries - apt-get update && apt-get install -y openssh-client rsync
- which ssh-agent || ( apt-get install -y openssh-client ) # 2. Configure SSH key for secure
deployment - eval $(ssh-agent -s) # The SSH_PRIVATE_KEY must be configured as a 'File'
variable in GitLab CI settings - chmod 600 $SSH_PRIVATE_KEY - ssh-add $SSH_PRIVATE_KEY
# 3. Add host key verification to known_hosts to prevent interactive prompts - mkdir -p ~/.ssh
- ssh-keyscan $SERVER_HOST >> ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts # --- Main
Execution Script (script) --- script: # 1. Build the Go Binary (using the original command) -
echo "Building Go executable for Linux/amd64..." - GOOS=linux GOARCH=amd64 go build
-ldflags="-s -w" -o $LOCAL_BINARY ./cmd/app # 2. Copy the binary to the server using SCP -
echo "Deploying new binary to $SERVER_HOST..." - scp $LOCAL_BINARY
$SERVER_USER@$SERVER_HOST:$BIN_PATH # 3. Ensure the newly deployed file has execute permissions
- echo "Setting execute permissions on the server..." - ssh $SERVER_USER@$SERVER_HOST
"chmod +x $BIN_PATH" - echo
"Deployment complete. The cron job will pick up the new binary on its next run."B. The Zero-Downtime Advantage
Because the cron job uses an absolute path to the executable (/opt/myapp/bin/myapp), the following occurs:
- If the cron job is running the old version when the deployment occurs, the current run completes normally.
- Once the deployment is finished, the next scheduled execution (at the next top-of-the-hour mark) will automatically load and run the brand-new binary file, achieving an effective zero-downtime update.
This deployment structure is clean, secure (using SSH keys), and highly robust for small to medium-sized GoLang services.
The GitLab CI configuration provided in Step 3 of the Canvas document is designed to never use a plaintext password. Instead, it relies on a more secure method: SSH Key Authentication.
Where to Put the Credentials
You should not use a username and password directly in the .gitlab-ci.yml file. Instead, you must use GitLab CI/CD Variables to store the necessary SSH connection details.
Here is a breakdown of where each piece of information is stored and used:
- Server Host (
$SERVER_HOST):
- Location: Stored as a CI/CD Variable (type: Variable) in your GitLab project settings.
- Used In: The
scpandsshcommands to identify the target machine.
- Server User (
$SERVER_USER):
- Location: Stored as a CI/CD Variable (type: Variable) in your GitLab project settings.
- Used In: The
scpandsshcommands to specify the login user (e.g.,ubuntuordeploy_user).
- SSH Private Key (
$SSH_PRIVATE_KEY):
- Location: This is the most critical part. You must store the private SSH key corresponding to the public key authorized on your Ubuntu server as a CI/CD Variable of the type File.
- Used In: The
before_scriptsection of the GitLab CI job loads this file into the SSH agent usingssh-add, allowing thescpandsshcommands to authenticate without a password.
Summary of Setup
To ensure the GitLab Action job runs securely, you need to configure these three variables in your GitLab project’s settings (under Settings > CI/CD > Variables), as referenced in the deployment script within the Canvas document.
Variable KeyType in GitLabData StoredAuthentication MethodSERVER_HOSTThe server's IP address or domain.SSH Key AuthenticationSERVER_USERThe system user for deployment.SSH Key AuthenticationSSH_PRIVATE_KEYThe raw content of your private key (e.g., starting with -----BEGIN OPENSSH PRIVATE KEY-----).SSH Key Authentication
By using SSH keys and environment variables, you avoid embedding sensitive credentials in your repository or exposing them in the CI logs, which is the industry-standard security practice for deployment.

Conclusion
This deployment strategy demonstrates that robust, production-grade automation doesn’t always require complex orchestration platforms or heavy infrastructure. By combining Go’s static compilation model with Ubuntu’s reliable cron scheduler and GitLab CI/CD’s secure pipeline capabilities, we achieve a deployment architecture that is both elegant and maintainable.
The key strengths of this approach lie in its simplicity and reliability. The manual cron configuration is set once and never touched again, eliminating configuration drift and deployment complexity. The atomic file replacement mechanism ensures zero-downtime updates without requiring process management tools, load balancers, or rolling deployment strategies. SSH key authentication provides enterprise-grade security without exposing credentials in code or logs.
This architecture is particularly valuable for Golang processes, scheduled execution, and batch jobs where the overhead of containerization or platform-as-a-service solutions may be unnecessary. The entire deployment footprint consists of a single binary, a configuration file, and a cron entry — making troubleshooting straightforward and system resources minimal.
For teams managing small to medium-sized scheduled workloads, this pattern offers a pragmatic middle ground: more sophisticated than manual deployment, yet far simpler than Kubernetes or container orchestration. The result is a deployment pipeline that is secure, automated, and maintainable — proving that sometimes the best solution is the one that does exactly what you need, and nothing more.
