C++ Production Deployment: Docker, systemd, Kubernetes,
Introduction: “Works locally, dies on the server”
Typical production issues:
- glibc mismatch between build and run hosts
- OOM in containers without proper limits
- No restart policy on crash
- Logs only on stdout—lost on restart
- 502 during deploy without readiness Topics: multi-stage Docker, systemd on VMs, Kubernetes Deployments/Services, GitHub Actions, rolling / blue-green, Prometheus, JSON logs (e.g. spdlog).
Table of contents
- Common production issues
- Docker deployment
- systemd deployment
- Kubernetes deployment
- CI/CD pipelines
- Monitoring and logging
- Real-world examples
- Performance considerations
- Common mistakes
- Best practices
- Production patterns
1. Common production issues
| Issue | Symptom | Mitigation |
|---|---|---|
GLIBC_x.y not found | Binary won’t start | Build in same base as runtime, or static libstdc++/musl |
| Pod OOMKilled | Container killed | Set memory limits; fix leaks (ASan/Valgrind) |
| Brief 502 on deploy | Downtime during rollout | readinessProbe, maxUnavailable: 0, graceful shutdown |
| Lost logs | No debugging info | Centralize (Loki/ELK), volume mounts |
| Manual deploy mistakes | Inconsistent releases | CI/CD with image tags = git SHA |
| No reboot autostart | Service down after reboot | systemd enable |
| Slow startup | Health checks fail | Optimize initialization, increase timeouts |
| Config drift | Different behavior per env | ConfigMaps/Secrets, version control |
2. Docker deployment
Multi-stage Dockerfile
# Stage 1: Builder
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
git \
libssl-dev \
libboost-all-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build --parallel $(nproc)
# Stage 2: Runtime
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
libssl3 \
libboost-system1.74.0 \
&& rm -rf /var/lib/apt/lists/*
# Non-root user
RUN useradd -m -u 1000 appuser
WORKDIR /app
COPY --from=builder /app/build/myapp /app/myapp
COPY --from=builder /app/config /app/config
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD [./myapp]
docker-compose for local development
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/mydb
- LOG_LEVEL=debug
depends_on:
- db
- prometheus
volumes:
- ./logs:/app/logs
restart: unless-stopped
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheus
volumes:
postgres_data:
Build and push script
#!/bin/bash
set -e
VERSION=$(git rev-parse --short HEAD)
IMAGE="myregistry.io/myapp:${VERSION}"
echo "Building ${IMAGE}..."
docker build -t "${IMAGE}" .
echo "Running tests..."
docker run --rm "${IMAGE}" ./myapp --test
echo "Pushing ${IMAGE}..."
docker push "${IMAGE}"
echo "Tagging as latest..."
docker tag "${IMAGE}" "myregistry.io/myapp:latest"
docker push "myregistry.io/myapp:latest"
echo "Deployed: ${IMAGE}"
3. systemd deployment
Service unit file
# /etc/systemd/system/myapp.service
[Unit]
Description=My C++ Application
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config.json
ExecReload=/bin/kill -HUP $MAINPID
# Restart policy
Restart=on-failure
RestartSec=5s
StartLimitInterval=60s
StartLimitBurst=3
# Graceful shutdown
TimeoutStopSec=30s
KillMode=mixed
KillSignal=SIGTERM
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
MemoryLimit=2G
CPUQuota=200%
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data /opt/myapp/logs
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
Deployment script
#!/bin/bash
set -e
APP_NAME="myapp"
INSTALL_DIR="/opt/${APP_NAME}"
SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
echo "Stopping service..."
sudo systemctl stop ${APP_NAME} || true
echo "Installing binary..."
sudo mkdir -p ${INSTALL_DIR}/bin
sudo cp build/myapp ${INSTALL_DIR}/bin/
sudo chmod +x ${INSTALL_DIR}/bin/myapp
echo "Installing config..."
sudo cp config.json ${INSTALL_DIR}/
echo "Setting permissions..."
sudo chown -R myapp:myapp ${INSTALL_DIR}
echo "Installing service..."
sudo cp myapp.service ${SERVICE_FILE}
sudo systemctl daemon-reload
echo "Starting service..."
sudo systemctl enable ${APP_NAME}
sudo systemctl start ${APP_NAME}
echo "Checking status..."
sudo systemctl status ${APP_NAME}
echo "Deployment complete!"
Graceful shutdown in C++
#include <csignal>
#include <atomic>
std::atomic<bool> shutdown_requested{false};
void signal_handler(int signal) {
if (signal == SIGTERM || signal == SIGINT) {
shutdown_requested = true;
}
}
int main() {
std::signal(SIGTERM, signal_handler);
std::signal(SIGINT, signal_handler);
// Initialize server
Server server;
server.start();
// Main loop
while (!shutdown_requested) {
server.process_events();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Graceful shutdown
std::cout << "Shutting down gracefully...\n";
server.stop(); // Close connections, flush logs
return 0;
}
4. Kubernetes deployment
Deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
version: v1.2.3
spec:
containers:
- name: myapp
image: myregistry.io/myapp:abc123
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
- containerPort: 9090
name: metrics
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: myapp-config
key: log-level
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: logs
mountPath: /app/logs
volumes:
- name: config
configMap:
name: myapp-config
- name: logs
emptyDir: {}
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
Service manifest
apiVersion: v1
kind: Service
metadata:
name: myapp
labels:
app: myapp
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
- port: 9090
targetPort: 9090
protocol: TCP
name: metrics
selector:
app: myapp
Health check endpoints in C++
#include <boost/beast.hpp>
namespace beast = boost::beast;
namespace http = beast::http;
class HealthHandler {
std::atomic<bool> ready_{false};
Database& db_;
public:
explicit HealthHandler(Database& db) : db_(db) {}
void set_ready(bool ready) { ready_ = ready; }
// Liveness: Is the process alive?
http::response<http::string_body> handle_health() {
http::response<http::string_body> res{http::status::ok, 11};
res.set(http::field::content_type, "application/json");
res.body() = R"({"status":"healthy"})";
res.prepare_payload();
return res;
}
// Readiness: Can it handle traffic?
http::response<http::string_body> handle_ready() {
if (!ready_) {
http::response<http::string_body> res{http::status::service_unavailable, 11};
res.set(http::field::content_type, "application/json");
res.body() = R"({"status":"not ready"})";
res.prepare_payload();
return res;
}
// Check dependencies
if (!db_.is_connected()) {
http::response<http::string_body> res{http::status::service_unavailable, 11};
res.set(http::field::content_type, "application/json");
res.body() = R"({"status":"database unavailable"})";
res.prepare_payload();
return res;
}
http::response<http::string_body> res{http::status::ok, 11};
res.set(http::field::content_type, "application/json");
res.body() = R"({"status":"ready"})";
res.prepare_payload();
return res;
}
};
5. CI/CD pipelines
GitHub Actions workflow
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake g++ libboost-all-dev
- name: Build
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
- name: Run tests
run: |
cd build
ctest --output-on-failure
- name: Run sanitizers
run: |
cmake -B build-asan -DCMAKE_CXX_FLAGS="-fsanitize=address -g"
cmake --build build-asan
cd build-asan
ctest --output-on-failure
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Log in to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix={{branch}}-
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/myapp \
myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
6. Monitoring and logging
Prometheus metrics in C++
#include <prometheus/counter.h>
#include <prometheus/gauge.h>
#include <prometheus/histogram.h>
#include <prometheus/registry.h>
#include <prometheus/exposer.h>
class Metrics {
std::shared_ptr<prometheus::Registry> registry_;
prometheus::Exposer exposer_;
prometheus::Family<prometheus::Counter>& requests_total_;
prometheus::Family<prometheus::Histogram>& request_duration_;
prometheus::Family<prometheus::Gauge>& active_connections_;
public:
Metrics()
: registry_(std::make_shared<prometheus::Registry>()),
exposer_("0.0.0.0:9090"),
requests_total_(prometheus::BuildCounter()
.Name("http_requests_total")
.Help("Total HTTP requests")
.Register(*registry_)),
request_duration_(prometheus::BuildHistogram()
.Name("http_request_duration_seconds")
.Help("HTTP request duration")
.Register(*registry_)),
active_connections_(prometheus::BuildGauge()
.Name("active_connections")
.Help("Number of active connections")
.Register(*registry_)) {
exposer_.RegisterCollectable(registry_);
}
void increment_requests(const std::string& method, const std::string& path) {
requests_total_.Add({{"method", method}, {"path", path}}).Increment();
}
void observe_duration(const std::string& method, double duration) {
request_duration_.Add({{"method", method}},
prometheus::Histogram::BucketBoundaries{0.001, 0.01, 0.1, 1.0, 10.0})
.Observe(duration);
}
void set_active_connections(int count) {
active_connections_.Add({}).Set(count);
}
};
Structured logging with spdlog
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
void setup_logging() {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/myapp.log", 1024 * 1024 * 10, 3);
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("myapp", sinks.begin(), sinks.end());
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v");
logger->set_level(spdlog::level::info);
spdlog::set_default_logger(logger);
}
// Structured logging
void log_request(const std::string& method, const std::string& path, int status, double duration) {
spdlog::info(R"({{"event":"request","method":"{}","path":"{}","status":{},"duration":{}}})",
method, path, status, duration);
}
7. Real-world examples
Example 1: Blue-green deployment
#!/bin/bash
# Deploy new version to green environment
kubectl apply -f deployment-green.yaml
# Wait for green to be ready
kubectl wait --for=condition=available --timeout=300s deployment/myapp-green
# Switch traffic
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# Monitor for 5 minutes
sleep 300
# If successful, scale down blue
kubectl scale deployment myapp-blue --replicas=0
Example 2: Canary deployment
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
spec:
replicas: 9
template:
metadata:
labels:
app: myapp
version: stable
spec:
containers:
- name: myapp
image: myapp:v1.0.0
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
spec:
replicas: 1 # 10% traffic
template:
metadata:
labels:
app: myapp
version: canary
spec:
containers:
- name: myapp
image: myapp:v1.1.0
8. Performance considerations
Resource limits tuning
resources:
requests:
memory: "512Mi" # Guaranteed
cpu: "500m" # 0.5 CPU
limits:
memory: "2Gi" # Maximum (OOMKilled if exceeded)
cpu: "2000m" # Throttled if exceeded
Connection pooling
class ConnectionPool {
std::vector<std::unique_ptr<Connection>> pool_;
std::mutex mutex_;
std::condition_variable cv_;
public:
explicit ConnectionPool(size_t size) {
for (size_t i = 0; i < size; ++i) {
pool_.push_back(std::make_unique<Connection>());
}
}
std::unique_ptr<Connection> acquire() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return !pool_.empty(); });
auto conn = std::move(pool_.back());
pool_.pop_back();
return conn;
}
void release(std::unique_ptr<Connection> conn) {
std::lock_guard<std::mutex> lock(mutex_);
pool_.push_back(std::move(conn));
cv_.notify_one();
}
};
9. Common mistakes
Mistake 1: No resource limits
# ❌ BAD: Can consume all node resources
spec:
containers:
- name: myapp
image: myapp:latest
# ✅ GOOD: Set limits
spec:
containers:
- name: myapp
image: myapp:latest
resources:
limits:
memory: "2Gi"
cpu: "2000m"
Mistake 2: Using :latest tag
# ❌ BAD: Unpredictable, can't rollback
image: myapp:latest
# ✅ GOOD: Use git SHA or semantic version
image: myapp:abc123def
Mistake 3: No graceful shutdown
// ❌ BAD: Abrupt termination
int main() {
Server server;
server.run(); // Killed by SIGTERM
}
// ✅ GOOD: Handle signals
int main() {
Server server;
std::signal(SIGTERM, signal_handler);
server.run();
server.graceful_shutdown();
}
10. Best practices
- Use multi-stage builds to minimize image size
- Run as non-root user in containers
- Set resource limits to prevent resource exhaustion
- Implement health checks (liveness + readiness)
- Enable graceful shutdown (handle SIGTERM)
- Use structured logging (JSON format)
- Expose metrics for Prometheus
- Version images with git SHA
- Automate deployments with CI/CD
- Monitor and alert on key metrics
11. Production patterns
Pattern 1: Init containers
spec:
initContainers:
- name: migrate
image: myapp:abc123
command: ['./migrate', '--up']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
containers:
- name: myapp
image: myapp:abc123
Pattern 2: Sidecar logging
spec:
containers:
- name: myapp
image: myapp:abc123
volumeMounts:
- name: logs
mountPath: /app/logs
- name: log-forwarder
image: fluent/fluent-bit:latest
volumeMounts:
- name: logs
mountPath: /app/logs
readOnly: true
Summary
- Docker: Multi-stage builds, non-root user, health checks
- systemd: Restart policies, resource limits, security hardening
- Kubernetes: Rolling updates, probes, resource management
- CI/CD: Automated testing, building, and deployment
- Monitoring: Prometheus metrics, structured logging
- Best practices: Graceful shutdown, versioned images, resource limits
Next: Monitoring dashboard (#50-6)
Previous: Game engine basics (#50-3)
Keywords
C++ deployment, Docker, Kubernetes, systemd, CI/CD, Prometheus, production, DevOps
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete production deployment guide: Multi-stage Docker images, systemd units, Kubernetes probes and rolling updates, C…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]
- C++ 대용량 파일 업로드 완벽 가이드 | S3 멀티파트·MinIO·CDN 연동 [#50-11]
- C++ 실시간 모니터링 대시보드 | Grafana·Prometheus 통합 [#50-6]
Keywords Covered in This Article (Related Search Terms)
This article covers C++, Docker, Kubernetes, systemd, CI/CD, DevOps, Prometheus.